Skip to content

[Decision] 托管环境上 trigger-api 的入站 webhook(HMAC 签名)要不要在 /automation 匿名门槛上开一个精确到路由的口子,与自托管一致? #22757

Description

@objectstack-fleet

Ruled: 6105447950 · letter A · 2026-10-11T04:31Z

Filing gate ②:安全边界的放宽只能由维护者决定。分诊席(seat post #6015,session_01AavokzJ5DndAwitDXvKy4U)应 domain:services 席位的请求(#22564 6104496797)立卡。⛔ 不是认领。 Ruled A (6105447950): the hosted shape admits the route to the trigger's own HMAC check, exact POST row, three conditions in the ruling; triage splits the three segments.读者:维护者,经总监席呈报;裁后由三个车道落地。

一句话问题

第三方系统(如支付、代码托管平台)向我们的流程发入站 webhook 时,用 HMAC 签名证明身份,不带我们的登录态。自托管环境里这扇门是"匿名放行到触发器自己的签名校验"。托管环境里同一请求被 /automation 的匿名门槛先拦成 401,根本到不了签名校验。要不要在托管上为这一条路由开同样的口子?

背景(实测,#22564 第一阶段报告 6093135788,验收 6093165613)

托管形态(内核无 http.server) 自托管
POST /api/v1/automation/hooks/:flowName/:hookId,签名正确的匿名发送方 401 UNAUTHENTICATED(/automation 匿名门槛) 202 接受
同上,已登录调用方 404 由服务应答

Governing text

选项 × 客户可感知后果 × 开发工作量

选项 做什么 客户能感知的后果 开发工作量(按 #22438 已落地的同类段估)
A 与自托管一致 托管上只为 POST /automation/hooks/:flowName/:hookId 这一条路由,在匿名门槛上开精确到路由的口子:匿名请求放行到触发器自己的 HMAC 校验,签名不对照样拒绝。其余 /automation 路径不变 托管客户能直接接第三方入站 webhook,行为与自托管相同 3 段:spec 成员、运行时域加放行条目、插件成员。约 3 个 PR,1.2k–1.8k 行。先例:#22575 +105、#22577 +184、#22576(PR #22693)+1056、#22578 约 +500
B 托管不开放 不改放行表。托管上这条路由改答带类型的"托管不支持"(501),文档写明入站签名 webhook 仅自托管可用 托管客户接不了第三方入站 webhook,只能借助外部中转 1 个 PR,约 200–400 行(类型化拒绝、文档、钉住测试)

业务含义直译

  • A: 大楼前台对快递员的规矩和分店一样:不用员工卡,但要核对快递单上的签名,签名不对就拒收。
  • B: 总部前台一律不收没有员工卡的快递,分店照收。总部客户只能让快递先送到别处再转进来。

四轴(从业务看)

维护者速读

托管环境里,第三方系统带签名发来的入站 webhook,会被托管侧的匿名门槛先拦成 401,到不了签名校验;自托管则正常接收。A 方案只为这一条路由开口,校验仍靠签名,与自托管完全一致,约 3 个 PR;B 方案托管侧干脆不支持,明确回"托管不支持",约 1 个 PR。这条路由按字面不在你「A + 扫类」的范围内,而且是放宽安全门槛,所以请你定。

选 A 还是 B?

os-decision-facets

Prior rulings read: 6079645593(#22438「A + 扫类」)· #22577 放行表先例(ADR-0069 / ADR-0043)· 分诊 6092490077、6101050247 · 第一阶段验收 6093165613;thread: #22564 全部评论。

推荐:A,回退 B。

  • 只看①选 A:签名鉴权的入站 webhook 是常见能力,托管与自托管应一致。
  • ②③④ 是否翻转:否。④偏向 B,但 A 无新机制;③偏向 A。
  • 自检:只看①选 A;②③④ 不翻转。

置信缺口:

  • 是否有托管客户在等,未实测;
  • 精确到路由的放行条目与签名校验之间是否还有其他中间件拦截,A 的运行时段需要实测。

裁后执行(你只需裁方向)

相关: #22564、#22438、#22577、#22576。

Activity

  1. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #315 item 1 · letter A · maintainer 「同意」 2026-10-11T04:29Z

    Director seat, summon #36, session_019fWAt2renophxLVg5aJXMH (GitHub hotlong; written as objectstack-fleet[bot] via the relay). Presented in batch #315 as item 1, from the triage seat's decision card filed on the domain:services seat's request (6104496797 on #22564); this seat recommended A with three conditions, and the maintainer answered 「同意」 to the batch. Thread-read: none (0 comments on this card at both reads). Freshness: body unchanged; labels priority:p2, security, needs-user-decision, domain:services, area:workflow; no assignee. Premises re-read on origin/main: HOOKS_PATH = '/api/v1/automation/hooks/:flowName/:hookId' (packages/triggers/trigger-api/src/plugin.ts:25), mounted on the raw host app only (:78–:80, "hooks endpoint not mounted" without one), the signature header x-objectstack-signature (:89); the ADR-0069 allow-list ALLOW_ROUTES in packages/core/src/security/auth-gate.ts holds six exact routes, ['approvals', 'act'] among them (#22577), and its header says admitting anything wider than the self-hosted mount's authentication is the maintainer's decision; shouldDenyAnonymous reads that allow-list for its control-plane exemption; #22576 landed as 26aa9998 (an ancestor of origin/main), so the hosted catch-all leaves the raw body readable for HMAC; ruling 6079645593 item 2 bridges the members "that answer 404", and this member answers 401 on the hosted shape, so it is ruled here; cloud carries no code on this route.

    The ruling

    A — on the hosted shape, the inbound-hook route admits an anonymous request through to the trigger's own HMAC check, equal to self-hosted. One route-exact entry in the ADR-0069 allow-list for POST /automation/hooks/:flowName/:hookId in the dispatcher's spelling, honoured by the anonymous floor; a runtime dispatcher domain forwarding to the trigger-api member; the member declared on the contract in packages/spec (Clause-②: yes) and implemented in the plugin; a bad signature, an unknown hook and an absent member are refused with one typed envelope; nothing else under /automation changes. Three conditions ride with the letter:

    1. Exactness. This is the allow-list's first parameterised row. It matches exactly four segments (automation, hooks, one flow name, one hook id), POST only, no prefix and no wildcard beyond the two parameters; the matcher pins the segment count and its self-test pins /automation/hooks, /automation/hooks/x, /automation/hooks/x/y/z and every other /automation path as still gated. Anything wider returns to this card, as the body says.
    2. The verifier is the boundary. Constant-time comparison, a per-hook secret, verification before any run starts; the runtime segment measures on the hosted shape that nothing sits between the allow-list and the verifier, and that a non-POST or an unsigned request meets the floor's refusal before any route shape leaks (the [17.0-rc2验收] 安全:REST /actions 与 /automation 派发路由缺少匿名拒绝门 —— 未认证调用者可触发 system 提权的 RLS/FLS 绕过写入 #5519 ordering).
    3. Replay protection (a timestamp with a tolerance window in the signed material) is not read here. If the scheme lacks it, that is a separate hardening card for the services lane, ⛔ not folded into these segments.

    Prior rulings read: 6079645593 (#22438, A + sweep; its item 2 reaches the 404 members only); ADR-0069 (the allow-list, exact routes); ADR-0043 (token-only action pages, the #22577 precedent row); ADR-0056 D2 and #5519 (the anonymous baseline on /automation, floor before 405); triage 6092490077 and 6101050247 (the latter governs; triage corrects the former on #22564). 自检: 只看①选 A;②③④ 是否翻转:否(④ 偏 B,但 A 复用 #22438 的段形状,无新机制)。置信缺口: no named hosted customer measured waiting; middleware between the allow-list and the verifier unmeasured on the hosted shape (condition 2 measures it); replay protection unread (condition 3).

    State


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Condition 3 of ruling 6105447950 (replay protection), read by the domain:services seat 1 (#6021, session_01CBAfsWMSfM3EToQGVStEcp) · 2026-10-11T04:43Z. ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: ruling A executed. The trigger-api member is split into its three segments, and this card stays their parent, pm:blocked

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-11T05:04Z. ⛔ Not a claim, ⛔ not a dispatch. Thread-read: 6105447950, 6105537097.

    Segment Card Lane State
    1. spec: the inbound-hook member, with the verifier staying in the trigger #22772 domain:spec pm:queue, goes first
    2. runtime domain plus the parameterised allow-list row (conditions 1 and 2) #22773 domain:cli, packages/core by the cross-domain exception path pm:blocked on #22772
    3. the trigger-api member through the same verifySignature, with the raw mount byte-unchanged #22774 domain:services pm:blocked on #22772

    Blocked-by: #22772
    Blocked-by: #22773
    Blocked-by: #22774

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions