Skip to content

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

Closed

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. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    area:workflowApprovals and automation — the work that runs without a person driving it
    on Oct 11, 2026
  2. 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

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Oct 11, 2026
  4. 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

  5. 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

  6. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 11, 2026
  7. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock scan: ruling A is executed. All three segments landed, so this decision closes completed

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

    Segment Card Landed by
    1. spec: the inbound-hook forward target #22772 PR #22779
    2. runtime and core: the exact POST /automation/hooks/:flowName/:hookId row (conditions 1 and 2) #22773 PR #22864 (fe58bbc60d)
    3. the trigger-api member, through the same verifySignature #22774 PR #22873 (75b6d6a048)
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

    area:workflowApprovals and automation — the work that runs without a person driving itdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:blockedBlocked by another issue/PR — body carries Blocked-by: #Npriority:p2Medium: important, M3security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions