Skip to content

v0.21.0

Choose a tag to compare

@github-actions github-actions released this 10 Jun 13:40
33d3ba5

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: PortalJWTVerifier that validates Ed25519-signed JWT proofs. Verifies signature against a kid-indexed public key map, iss == "butler-portal", aud == "butler-server", exp within 60 seconds of iat plus 5 seconds of clock-skew slop on the TTL cap, nbf, jti anti-replay (in-memory cache, lazy sweep, bounded by the 60s TTL), and resolves sub against the User CRD via UserService.GetUserByEmail.
  • internal/auth/middleware.go: new PortalProofVerifier interface; new branch in SessionMiddleware BEFORE SessionService.ValidateSession that routes tokens whose iss claim is butler-portal to 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 behind cfg.AllowHeaderImpersonation.
  • internal/config/config.go: two new env vars: BUTLER_PORTAL_PUBKEY (default empty; PEM-encoded Ed25519 public keys for the new verification path) and BUTLER_ALLOW_HEADER_IMPERSONATION (default true; gates the existing impersonation sub-block).
  • internal/api/router.go: constructs PortalJWTVerifier only when the pubkey is set; wires both new fields into SessionMiddlewareConfig.

Operator action required

None for v0.21.0. The default configuration matches pre-Stage-1 behavior:

  • BUTLER_PORTAL_PUBKEY defaults to empty. No PortalJWTVerifier is constructed; the new branch is structurally bypassed.
  • BUTLER_ALLOW_HEADER_IMPERSONATION defaults to true. The existing X-Butler-User-Email override 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-Email to portal-minted Ed25519 proofs. PortalSigner mints proofs; AuthManager retires. 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_PUBKEY configured on butler-server with the matching public key. End-to-end signed-proof carrier in production.
  • Stage 4: BUTLER_ALLOW_HEADER_IMPERSONATION flipped to false. 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 to ValidateSession unchanged.
  • Back-compat flag: AllowHeaderImpersonation=true preserves the legacy override behavior; false skips 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