Task · TSO-0098

Webhook router caches its tokenless and auth-mix decision across secret rotation

Description

NewRouter computes r.tokenless and r.invalidAuthMix ONCE at construction from route.Server.currentSecret() (internal/webhook/webhook.go:326-343). TSO-0058 made that secret hot-reloadable, so the value it was derived from now changes at runtime while the derived decision does not.

An empty-to-non-empty rotation therefore leaves the router permanently in tokenless mode: it keeps applying the loopback browser-shaped rejection path (webhook.go:391-398) instead of verifying the signature it now has. The reverse transition leaves it believing a route is signed when the secret has been emptied. Rotating between two non-empty secrets, the ordinary case, is unaffected, which is why the wave tests did not catch it.

Either evaluate tokenless and invalidAuthMix per request from the current provider value, or reject a provider whose value crosses the empty boundary at all - both are defensible, but a security decision cached across the rotation feature built to change it is not. Found by the post-Wave-3 sharded CodeRabbit pass.

Acceptance Criteria

Definition of Done

Implementation Plan

Implementation Notes

Final Summary

Webhook routing now recomputes tokenless/authenticated mode from the current reloadable secret on every request. A regression failed before the fix and passed across both empty/non-empty rotation directions; focused race tests and review passed.

View the source file on GitHub