Remove inbound request signature verification (closes #279) #281

Merged
clawbot merged 1 commits from issue-279-remove-inbound-signing into next 2026-08-24 03:25:10 +02:00
Collaborator

Closes #279. Reverses #67 (landed in fcead5d) and closes #241 with it.

The receiver now verifies nothing about an inbound request. The entrypoint UUID in the URL is the authentication secret: possession of it authorises submission. Straight deletion — no flag, no migration, no compatibility path.

What went

  • internal/signature/ — package and tests.
  • Entrypoint.SignatureScheme and Entrypoint.SignatureSecret, the SignatureScheme type and its constants, and SignatureConfigured/SignatureHalfConfigured.
  • verifyInboundSignature in the receiver, and the signature.SanitizeHeaders call above the first write. SanitizeHeaders existed only to strip a scheme's own credential header (GitLab's X-Gitlab-Token) before the header map was stored and forwarded; with no configured credential there is nothing to strip, so the receiver marshals r.Header as received.
  • HandleEntrypointSecret, applyEntrypointSecret, storeEntrypointSecret and the POST /source/{id}/entrypoints/{entrypointID}/secret route.
  • The scheme labelling on EntrypointView (Configured, Scheme, SchemeLabel, SchemeHeader) and its three label constants. The view itself stays — it is still the display projection that keeps a raw row out of a template.
  • The Signature: line, the Configure/Rotate button, the secret form and its showSecret Alpine state in templates/source_detail.html.
  • README: the Inbound Signature Verification section, the two entrypoints columns and their AutoMigrate note, the /secret route row, the tree entries, the security bullet, the resubmit paragraph and pipeline step 3.

No config keys were removed because the feature never added any.

The columns are simply no longer written. Pre-1.0, no installed base, no stored data — a vestigial column in a database nobody has is not worth a migration.

What stayed, deliberately

The receiver's other protections are untouched, and each was exercised on a running instance (below): the 1 MB body cap, the per-IP rate limiter, the 410 for a deactivated entrypoint and the 404 for an unknown UUID.

No UUID masking or redaction was added anywhere. #254 and #179 are untouched.

Three delivery-side comments naming X-Hub-Signature / X-Gitlab-Token are left alone — internal/delivery/redirect.go (origin-scoped header stripping), internal/delivery/target_redact.go (outbound redaction) and internal/server/sentry.go (the Sentry header allowlist). Senders still send those headers; those files are about forwarding and redaction, not about verification, and each statement is still true.

README

Replaced with a short section, The entrypoint URL is the authentication secret: the receiver verifies nothing, the UUID is the credential, treat the URL like an API token, and there is no in-place rotation — delete or deactivate the entrypoint and create a new one.

Verification

make check green on the rebased branch with GOFLAGS=-count=1: 575 tests, 0 cached, lint in Docker reporting 0 issues.

Live instance on port 19310 with an HTTP sink target:

Case Result
Unsigned POST, no signature headers at all 200, delivered to the sink
POST with a deliberately wrong X-Hub-Signature-256 and a wrong X-Gitlab-Token 200, delivered to the sink
Body of 1048575 bytes 200
Body of 1048577 bytes 413 Request body too large
140 POSTs from one IP 120 accepted, then 429 — the default per-IP receiver limit, exactly
POST to a deactivated entrypoint 410 Gone
POST to an unknown UUID 404
POST after reactivating the entrypoint 200

Both accepted payloads were confirmed in the sink's received log, so acceptance and delivery were both proven rather than just the status code.

Grep across the tree finds no surviving signature_scheme, signature_secret, SignatureScheme, SignatureSecret, SanitizeHeaders, HandleEntrypointSecret, verifyInboundSignature or internal/signature reference in code, templates, config or docs.

Note for the reviewer, not addressed here

golangci-lint emits a deprecation warning on every run: gomodguard is deprecated since v2.12.0 in favour of gomodguard_v2. It predates this branch and is out of scope for this issue; it wants its own issue.

Closes https://git.eeqj.de/sneak/webhooker/issues/279. Reverses https://git.eeqj.de/sneak/webhooker/issues/67 (landed in `fcead5d`) and closes https://git.eeqj.de/sneak/webhooker/issues/241 with it. The receiver now verifies nothing about an inbound request. The entrypoint UUID in the URL is the authentication secret: possession of it authorises submission. Straight deletion — no flag, no migration, no compatibility path. ## What went - `internal/signature/` — package and tests. - `Entrypoint.SignatureScheme` and `Entrypoint.SignatureSecret`, the `SignatureScheme` type and its constants, and `SignatureConfigured`/`SignatureHalfConfigured`. - `verifyInboundSignature` in the receiver, and the `signature.SanitizeHeaders` call above the first write. `SanitizeHeaders` existed only to strip a scheme's own credential header (GitLab's `X-Gitlab-Token`) before the header map was stored and forwarded; with no configured credential there is nothing to strip, so the receiver marshals `r.Header` as received. - `HandleEntrypointSecret`, `applyEntrypointSecret`, `storeEntrypointSecret` and the `POST /source/{id}/entrypoints/{entrypointID}/secret` route. - The scheme labelling on `EntrypointView` (`Configured`, `Scheme`, `SchemeLabel`, `SchemeHeader`) and its three label constants. The view itself stays — it is still the display projection that keeps a raw row out of a template. - The `Signature:` line, the Configure/Rotate button, the secret form and its `showSecret` Alpine state in `templates/source_detail.html`. - README: the `Inbound Signature Verification` section, the two `entrypoints` columns and their `AutoMigrate` note, the `/secret` route row, the tree entries, the security bullet, the resubmit paragraph and pipeline step 3. No config keys were removed because the feature never added any. The columns are simply no longer written. Pre-1.0, no installed base, no stored data — a vestigial column in a database nobody has is not worth a migration. ## What stayed, deliberately The receiver's other protections are untouched, and each was exercised on a running instance (below): the 1 MB body cap, the per-IP rate limiter, the `410` for a deactivated entrypoint and the `404` for an unknown UUID. No UUID masking or redaction was added anywhere. https://git.eeqj.de/sneak/webhooker/issues/254 and https://git.eeqj.de/sneak/webhooker/issues/179 are untouched. Three delivery-side comments naming `X-Hub-Signature` / `X-Gitlab-Token` are left alone — `internal/delivery/redirect.go` (origin-scoped header stripping), `internal/delivery/target_redact.go` (outbound redaction) and `internal/server/sentry.go` (the Sentry header allowlist). Senders still send those headers; those files are about forwarding and redaction, not about verification, and each statement is still true. ## README Replaced with a short section, `The entrypoint URL is the authentication secret`: the receiver verifies nothing, the UUID is the credential, treat the URL like an API token, and there is no in-place rotation — delete or deactivate the entrypoint and create a new one. ## Verification `make check` green on the rebased branch with `GOFLAGS=-count=1`: **575 tests, 0 cached**, lint in Docker reporting `0 issues`. Live instance on port 19310 with an HTTP sink target: | Case | Result | | ---- | ------ | | Unsigned POST, no signature headers at all | `200`, delivered to the sink | | POST with a deliberately wrong `X-Hub-Signature-256` and a wrong `X-Gitlab-Token` | `200`, delivered to the sink | | Body of 1048575 bytes | `200` | | Body of 1048577 bytes | `413 Request body too large` | | 140 POSTs from one IP | 120 accepted, then `429` — the default per-IP receiver limit, exactly | | POST to a deactivated entrypoint | `410 Gone` | | POST to an unknown UUID | `404` | | POST after reactivating the entrypoint | `200` | Both accepted payloads were confirmed in the sink's received log, so acceptance and delivery were both proven rather than just the status code. Grep across the tree finds no surviving `signature_scheme`, `signature_secret`, `SignatureScheme`, `SignatureSecret`, `SanitizeHeaders`, `HandleEntrypointSecret`, `verifyInboundSignature` or `internal/signature` reference in code, templates, config or docs. ## Note for the reviewer, not addressed here `golangci-lint` emits a deprecation warning on every run: `gomodguard` is deprecated since v2.12.0 in favour of `gomodguard_v2`. It predates this branch and is out of scope for this issue; it wants its own issue.
clawbot added 1 commit 2026-08-24 03:14:26 +02:00
Remove inbound request signature verification (closes #279)
All checks were successful
check / check (push) Successful in 3m10s
cf48b465a6
The receiver verified an optional per-entrypoint HMAC or shared token
before accepting a request. That is removed outright: the entrypoint
UUID in the URL is the authentication secret, and possession of it
authorises submission. This reverses the feature added in fcead5d.

Deletes the internal/signature package, the signature_scheme and
signature_secret columns from Entrypoint along with their accessors,
the per-entrypoint secret form and its POST route, and the scheme
labelling in EntrypointView. Pre-1.0 with no installed base, so the
columns simply stop being written; there is no migration and no
compatibility path.

Header sanitisation goes with it. SanitizeHeaders existed to strip a
scheme's own credential header before the header map was stored and
forwarded; with no configured credential there is nothing to strip, so
the receiver marshals the headers as received.

The receiver's other protections are untouched: the 1 MB body cap, the
per-IP rate limiter, the 410 for a deactivated entrypoint and the 404
for an unknown UUID.
clawbot added the needs-review label 2026-08-24 03:14:32 +02:00
clawbot self-assigned this 2026-08-24 03:14:34 +02:00
Author
Collaborator

PASS. Independent review: DoD of #279 met; make check green from a clean clone with GOFLAGS=-count=1 (1203 tests, 0 (cached), lint in Docker ran 53s, 0 issues); CI green on cf48b46; merges clean into next; no dead route, dead export or stale template/fixture; no attribution trailers.

Anomalies and disclosures, none blocking:

  1. Stale comment, internal/delivery/redirect_test.go:173 — "and would lose the inbound signature the receiver verifies". The receiver verifies nothing now, so that clause is false. The PR body claims the three delivery-side comments naming these headers were audited and "each statement is still true"; this is a fourth, and it is not. Nit-level (the rest of that rationale holds and the test itself is correct), but it should read as something like "and would lose the sender's forwarded headers".

  2. SanitizeHeaders ruled on — read on 032f265: it cloned the header map and did exactly one thing, clone.Del(info.Header) for a scheme whose header is the credential itself, returning the clone unchanged for GitHub or an unknown scheme. No normalisation, no size bounding, no hop-by-hop removal. Removing it wholesale took nothing else.

  3. Forwarded sender headers are consistency, not a new hole — verified live: with a wrong X-Gitlab-Token and X-Hub-Signature-256 on the wire, both reach the operator-configured target exactly as X-Gitlab-Event and every other sender header always did (the strip only ever fired for an entrypoint with the GitLab scheme configured, a state that no longer exists). Redirect-hop protection from #233 / #243 is intact and was proven, not assumed: a target redirecting 127.0.0.1:19442 -> 127.0.0.2:19443 delivered X-Gitlab-Token, X-Hub-Signature-256 and the operator's own X-Op-Cred to hop 1 and none of the three to hop 2. TestDelivery_CrossOriginRedirectDropsOriginScopedHeaders also ran uncached. Exposure is bounded to the configured target.

  4. internal/handlers/entrypoint_view.go exists only because of fcead5d and now projects every non-relational field of the model. Keeping it was declared and is consistent with delivery.TargetView; noted, not a defect.

  5. README conflict resolution verified#269's internal/reqtls.IsTLS wording survives byte-identical in all four places it appears; only the adjacent signature bullet was replaced.

  6. "No config keys were removed because the feature never added any" is truefcead5d touched no file under internal/config.

  7. Receiver guards re-proven on a live instance, not taken from the PR body: 1048575 bytes -> 200, 1048577 -> 413 Request body too large; 200 sustained POSTs from one IP -> 114 200 then 86 429 (120/min per IP per entrypoint, six already spent in that window); deactivated entrypoint -> 410, reactivated -> 200; unknown UUID -> 404; unsigned and wrongly-signed POSTs -> 200 and both confirmed in the sink's log.

Follow-up for the manager, out of scope here: TODO.md:32 and TODO.md:302 still describe inbound signature verification as landed, and line 302 keeps "Stripe HMAC signature verification" as a Future Step premised on the GitHub and GitLab schemes. TODO.md says issue branches must not touch it, so this PR was right to leave it; it needs fixing on next by whoever maintains that file.

**PASS.** Independent review: DoD of https://git.eeqj.de/sneak/webhooker/issues/279 met; `make check` green from a clean clone with `GOFLAGS=-count=1` (1203 tests, 0 `(cached)`, lint in Docker ran 53s, 0 issues); CI green on `cf48b46`; merges clean into `next`; no dead route, dead export or stale template/fixture; no attribution trailers. Anomalies and disclosures, none blocking: 1. **Stale comment, `internal/delivery/redirect_test.go:173`** — "and would lose the inbound signature the receiver verifies". The receiver verifies nothing now, so that clause is false. The PR body claims the three delivery-side comments naming these headers were audited and "each statement is still true"; this is a fourth, and it is not. Nit-level (the rest of that rationale holds and the test itself is correct), but it should read as something like "and would lose the sender's forwarded headers". 2. **`SanitizeHeaders` ruled on** — read on `032f265`: it cloned the header map and did exactly one thing, `clone.Del(info.Header)` for a scheme whose header is the credential itself, returning the clone unchanged for GitHub or an unknown scheme. No normalisation, no size bounding, no hop-by-hop removal. Removing it wholesale took nothing else. 3. **Forwarded sender headers are consistency, not a new hole** — verified live: with a wrong `X-Gitlab-Token` and `X-Hub-Signature-256` on the wire, both reach the operator-configured target exactly as `X-Gitlab-Event` and every other sender header always did (the strip only ever fired for an entrypoint with the GitLab scheme configured, a state that no longer exists). Redirect-hop protection from https://git.eeqj.de/sneak/webhooker/issues/233 / https://git.eeqj.de/sneak/webhooker/issues/243 is intact and was proven, not assumed: a target redirecting `127.0.0.1:19442` -> `127.0.0.2:19443` delivered `X-Gitlab-Token`, `X-Hub-Signature-256` and the operator's own `X-Op-Cred` to hop 1 and none of the three to hop 2. `TestDelivery_CrossOriginRedirectDropsOriginScopedHeaders` also ran uncached. Exposure is bounded to the configured target. 4. **`internal/handlers/entrypoint_view.go` exists only because of `fcead5d`** and now projects every non-relational field of the model. Keeping it was declared and is consistent with `delivery.TargetView`; noted, not a defect. 5. **README conflict resolution verified** — https://git.eeqj.de/sneak/webhooker/issues/269's `internal/reqtls.IsTLS` wording survives byte-identical in all four places it appears; only the adjacent signature bullet was replaced. 6. **"No config keys were removed because the feature never added any" is true** — `fcead5d` touched no file under `internal/config`. 7. Receiver guards re-proven on a live instance, not taken from the PR body: 1048575 bytes -> `200`, 1048577 -> `413 Request body too large`; 200 sustained POSTs from one IP -> 114 `200` then 86 `429` (120/min per IP per entrypoint, six already spent in that window); deactivated entrypoint -> `410`, reactivated -> `200`; unknown UUID -> `404`; unsigned and wrongly-signed POSTs -> `200` and both confirmed in the sink's log. Follow-up for the manager, out of scope here: `TODO.md:32` and `TODO.md:302` still describe inbound signature verification as landed, and line 302 keeps "Stripe HMAC signature verification" as a Future Step premised on the GitHub and GitLab schemes. `TODO.md` says issue branches must not touch it, so this PR was right to leave it; it needs fixing on `next` by whoever maintains that file.
clawbot merged commit 37b59f8822 into next 2026-08-24 03:25:10 +02:00
clawbot deleted branch issue-279-remove-inbound-signing 2026-08-24 03:25:10 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#281