v0.21.0
v0.21.0
Stage 1 of the P0 #3 impersonation fix. Adds Ed25519 portal-JWT verification as a new authentication path in SessionMiddleware. The legacy X-Butler-User-Email header-impersonation behavior is preserved behind BUTLER_ALLOW_HEADER_IMPERSONATION (default true), so production behavior is unchanged.
This release changes NOTHING observable. Console / CLI / portal traffic all flow through the same code paths as v0.20.0. The new portal-JWT branch is dormant unless BUTLER_PORTAL_PUBKEY is configured, and the legacy impersonation path is unchanged under the default flag. v0.21.0 adds a verification capability; the cutover that retires header impersonation is Stage 4.
What this release adds
internal/auth/portal_jwt.go:PortalJWTVerifierthat validates Ed25519-signed JWT proofs. Verifies signature against a kid-indexed public key map,iss == "butler-portal",aud == "butler-server",expwithin 60 seconds ofiatplus 5 seconds of clock-skew slop on the TTL cap,nbf,jtianti-replay (in-memory cache, lazy sweep, bounded by the 60s TTL), and resolvessubagainst the User CRD viaUserService.GetUserByEmail.internal/auth/middleware.go: newPortalProofVerifierinterface; new branch inSessionMiddlewareBEFORESessionService.ValidateSessionthat routes tokens whoseissclaim isbutler-portalto the new verifier; claimed-is-terminal boundary guard that rejects rather than falls through if the verifier reports claimed without a session; legacy header-impersonation sub-block wrapped behindcfg.AllowHeaderImpersonation.internal/config/config.go: two new env vars:BUTLER_PORTAL_PUBKEY(default empty; PEM-encoded Ed25519 public keys for the new verification path) andBUTLER_ALLOW_HEADER_IMPERSONATION(defaulttrue; gates the existing impersonation sub-block).internal/api/router.go: constructsPortalJWTVerifieronly when the pubkey is set; wires both new fields intoSessionMiddlewareConfig.
Operator action required
None for v0.21.0. The default configuration matches pre-Stage-1 behavior:
BUTLER_PORTAL_PUBKEYdefaults to empty. NoPortalJWTVerifieris constructed; the new branch is structurally bypassed.BUTLER_ALLOW_HEADER_IMPERSONATIONdefaults totrue. The existingX-Butler-User-Emailoverride fires for platform-admin sessions exactly as before.
Operators do not need to set either env var for this release.
Stage roadmap
- Stage 1 (this release): butler-server gains the verification capability. Additive. Zero behavior change.
- Stage 2: butler-portal swaps its proxy carrier from legacy admin Bearer +
X-Butler-User-Emailto portal-minted Ed25519 proofs.PortalSignermints proofs;AuthManagerretires. Portal-side change only; this release in butler-server already accepts the proofs once configured. - Stage 3: butler-portal-live pins the new portal chart + image; new SealedSecret carries the portal's signing key;
BUTLER_PORTAL_PUBKEYconfigured on butler-server with the matching public key. End-to-end signed-proof carrier in production. - Stage 4:
BUTLER_ALLOW_HEADER_IMPERSONATIONflipped tofalse. The legacy header-impersonation sub-block becomes a no-op. After a soak window, the sub-block is removed entirely in a butler-server cleanup PR.
Verification
All three load-bearing invariants in this release are mutation-proven non-vacuous:
- Iss-routing console-safety: a token with
iss != "butler-portal"(every console / CLI / legacy admin token) falls through toValidateSessionunchanged. - Back-compat flag:
AllowHeaderImpersonation=truepreserves the legacy override behavior;falseskips it. - Claimed-is-terminal: when the verifier reports a claimed but invalid proof, the middleware returns 401 instead of falling through to
ValidateSession.
Rollback
Revert butler-server image and chart to v0.20.0. No data migration, no configuration cleanup required.
Full Changelog: v0.20.0...v0.21.0