Skip to content

feat(webhook): Standard Webhooks senders now authenticate (fixes #47451, salvage #47849) - #92024

Open
teknium1 wants to merge 1 commit into
mainfrom
centaur-port/standard-webhooks
Open

feat(webhook): Standard Webhooks senders now authenticate (fixes #47451, salvage #47849)#92024
teknium1 wants to merge 1 commit into
mainfrom
centaur-port/standard-webhooks

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

The generic webhook gateway now authenticates Standard Webhooks senders (webhook-id / webhook-timestamp / webhook-signature) — providers using that spec (GitLab and a growing set of SaaS webhooks) previously had every delivery to a secret-configured route rejected as an unrecognized signature.

Salvages #47849 by @HwangJohn (fixes #47451), rebased onto current main with the stale test-file snapshot dropped and hardening tests added on top.

Changes

  • gateway/platforms/webhook.py: accept webhook-* headers as aliases into the existing Svix-compatible validator — same signed content {id}.{timestamp}.{raw_body} and v1,<base64-hmac-sha256> format, same ±300s replay window, fail-closed when any field is missing; webhook-id also feeds the delivery-ID dedupe cache; module docstring documents the scheme.
  • tests/gateway/test_webhook_adapter.py: valid-signature accept, tampered-body reject, expired-timestamp reject, incomplete-headers fail-closed, and webhook-id dedupe (hardening tests for replay-window and fail-closed added during salvage).
  • website/docs/user-guide/messaging/webhooks.md: Standard Webhooks listed under signature validation and idempotency.

Validation

Check Result
tests/gateway/test_webhook_adapter.py 41 passed
tests/gateway/test_webhook_signature_rate_limit.py 1 passed
E2E (real aiohttp app, secret-configured route) valid sig → 202, tampered body → 401, replayed webhook-id → duplicate
scripts/audit_pr_attribution.py --fix all emails mapped

Ported context: paradigmxyz/centaur#1380 shipped the same protocol support this week (Rust, standardwebhooks crate) with the identical design points — five-minute tolerance, v1 space-separated signatures for key rotation, fail closed on missing secret. Ours reuses the existing Svix validator, which already implements all of that, so the diff stays at ~30 LOC of adapter code.

Infographic

Standard Webhooks support

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 3a63116 — feat(webhook): accept standard webhook signatures

⚠️ Warnings

OSV vulnerability scan · View job

7 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 3m33s vs 3m56s (-9.7%). 11 job(s) slower, 13 faster, 1 unchanged.

  • Python tests / Run tests slice 10/12: +55.0s
  • Docs Site / docs-site-checks: -27.0s
  • Python tests / Run tests slice 1/12: +26.0s
  • Python tests / Run tests slice 7/12: +23.0s
  • Python tests / Run tests slice 9/12: -15.0s

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery platform/webhook Webhook / API server sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Aug 22, 2026

@andrexibiza andrexibiza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking review (GitHub will not permit this identity to set REQUEST_CHANGES without explicit repository review access).

1. GitLab's documented dual-token migration becomes a 401 regression

_validate_signature() now treats the presence of any svix-* / webhook-* field as authoritative and returns immediately through _validate_svix_signature() before the existing X-Gitlab-Token branch. That is not backward-compatible with the migration state GitLab explicitly documents.

GitLab 19.x supports a legacy Secret token (X-Gitlab-Token) and a separate Standard Webhooks Signing token (whsec_<base64>). Their docs recommend configuring both during migration, then updating the receiver, then removing the old token: https://docs.gitlab.com/user/project/integrations/webhooks/#signing-tokens

Hermes currently has only one route secret. A deterministic existing-install sequence is therefore:

  1. Hermes route is configured with the existing legacy secret old-secret.
  2. GitLab sends X-Gitlab-Token: old-secret; current main accepts it.
  3. Operator follows GitLab's zero-downtime migration guidance and additionally enables a different whsec_... signing token. GitLab now sends both header families.
  4. On this PR, webhook-id / webhook-timestamp / webhook-signature select the Standard/Svix branch first.
  5. Hermes computes that HMAC using the route's old-secret, not GitLab's distinct signing token, so validation fails and returns 401. The still-valid X-Gitlab-Token: old-secret is never consulted.

That means enabling GitLab's stronger auth mechanism can break an already-working Hermes endpoint before the operator has any way to supply the second credential. The PR's tests only cover Standard-only headers with a synthetic raw secret; they do not cover the exact mixed-header transition GitLab ships and recommends.

This should not be repaired by blindly falling back from a failed Standard signature to a weaker scheme: that recreates downgrade ambiguity. The route needs an explicit auth decision/credential model. At minimum, preserve the existing GitLab-token route when that is the configured credential and add a regression for X-Gitlab-Token + webhook-* with distinct secrets. Preferably, compose this on the explicit provider/auth authority already being built below.

2. This reintroduces header-inferred auth in the exact area the repository is already consolidating

There is already an open, exact-head-green authority PR for this defect class: #85318 owns gateway/platforms/webhook_auth.py, including an explicit gitlab_standard mode, and deliberately does not infer the verifier from attacker-controlled headers. #90995 is the open convergence spine that binds provider + verifier once and explicitly says the next adapter composition must consume #85318 rather than copy verifier behavior. #90236 owns provider-native delivery identity / idempotency, and #85640 remains terminal assembly.

This PR instead adds another Standard Webhooks implementation directly to the legacy inline validator and extends the legacy global delivery-ID precedence chain. It also allows mixed families field-by-field (svix-id + webhook-timestamp + webhook-signature, etc.) because each component is selected independently with or. That is not an auth bypass without the key, but it is exactly the provider/verifier identity ambiguity #85318/#90995 were created to eliminate.

So I would not merge this as a parallel authority. Preserve @HwangJohn's #47849 implementation/provenance, but fold its wire-header support and tests into the canonical composition path: #90236 intake identity → #85318 explicit verifier → #90995 bound provider/envelope → #85640 assembly. Once the superseding candidate lands, #47849 can be closed as superseded rather than losing credit.

Verification / exact object

Reviewed head: 3a63116aa5781ed44e661a3b05f03d6e153f4c2c.

The PR records base/merge-base 7d6db4efb885856078e4d19f804035226df81e0d; current main is now fc7523ca31eeb6eff9114afe384c2cf6380359df (head is 1 ahead / 3 behind current main). I re-read the relevant current-main webhook.py hunk and those intervening commits have not already solved this selection problem.

Exact-head hosted workflows are green: CI 32548369568, Docker 32548369211, Nix 32548369190. There were no submitted reviews or inline review threads before this review. Green CI does not cover the dual-token migration case above, and the current-main merge candidate still needs fresh verification after the auth/topology repair.

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

Nice fit for the existing Svix validator — Standard Webhooks is wire-compatible (same {id}.{timestamp}.{raw_body} signed content and v1,<base64-hmac> format), so routing webhook-* headers into _validate_svix_signature instead of adding a parallel validator keeps a single crypto path to audit. A few observations:

Correctness

  • The or-fallbacks in _validate_signature (gateway/platforms/webhook.py, the svix_id/svix_timestamp/svix_signature assignments around L1092–1094) mix header families per-field: a sender sending svix-id + webhook-timestamp + webhook-signature would still validate if signed consistently. Cryptographically harmless, but when validation fails it may be hard to tell which family was picked — consider a debug-level log of all six header values on the failure path to speed up misconfigured-sender triage.
  • Delivery-id dedupe chain in _handle_webhook slots webhook-id between svix-id and X-Request-ID — right priority order, and test_webhook_id_used_as_delivery_id_for_deduplication pins it. Good.

Tests

  • test_validate_standard_webhooks_incomplete_headers_fail_closed is the most valuable addition here — it pins that webhook-id alone selects the strong path and never falls through to the weaker generic/V1 branches. Keep it even though it looks redundant beside the happy-path test.
  • Two possible additions: (a) both svix-signature and webhook-signature present with different values, documenting that the svix-* side wins via or precedence; (b) a whsec_-prefixed secret combined with webhook-* headers — Standard Webhooks docs always show whsec_… secrets, and while the svix-* path has raw/whsec_ variants covered, the new happy-path test only exercises a raw secret.

Docs

  • The code comment says GitLab uses this scheme; most deployed GitLab instances still default to X-Gitlab-Token. A "(recent GitLab versions)" qualifier on the Standard Webhooks row in website/docs/user-guide/messaging/webhooks.md would save readers wondering why their GitLab webhooks don't send webhook-* headers.

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

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have platform/webhook Webhook / API server sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Support GitLab Standard Webhooks signing token (webhook-signature)

5 participants