Skip to content

feat(exousia): live JWT secret/TTL via config reload with immediate rotation (#529 step 3) - #583

Merged
forkwright merged 1 commit into
mainfrom
audit/529-step3
Jul 6, 2026
Merged

feat(exousia): live JWT secret/TTL via config reload with immediate rotation (#529 step 3)#583
forkwright merged 1 commit into
mainfrom
audit/529-step3

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Step 3 of the #529 reactive-config migration — live JWT with immediate rotation (operator-locked semantics).

ExousiaServiceImpl stored a frozen ExousiaConfig captured at construction, so a jwt_secret or TTL change via SIGHUP had no effect until restart. It now holds horismos::Section<ExousiaConfig> and takes exactly one self.config.get() snapshot at the top of each operation (login, refresh, validate_bearer), using that single snapshot for both the secret and the TTLs — a torn-config guard so a mid-rotation login can never mint with secret A but TTL B.

Rotation semantics (immediate, no dual-secret grace):

  • Rotate exousia.jwt_secret (secrets.toml or HARMONIA__EXOUSIA__JWT_SECRET) + SIGHUP. Reload validation still enforces ≥32 bytes / non-placeholder, so an invalid rotation is rejected atomically and the old secret stays in force.
  • Verification is live: validate_bearer checks HS256 against the current secret, so every in-flight access token signed with the old secret fails immediately → TokenInvalid → HTTP 401 UNAUTHORIZED. This is deliberate — rotating a compromised secret must kill outstanding bearers at once.
  • Sessions survive: refresh tokens are opaque random values (sha256-hashed in the DB, not signed with jwt_secret), so the refresh transaction still verifies and mints the next access token under the new secret. A client that refreshes on 401 recovers without re-login.
  • TTL changes are mint-forward: exp is baked into claims at mint; a TTL change affects only tokens/refresh-rows created after the reload. Already-issued expirations are honored.
  • A reload whose applied paths include exousia.jwt_secret logs warn!("exousia.jwt_secret rotated — all outstanding access tokens are now invalid").

Production wiring passes config_handle.section(|c| &c.exousia); all 10 test/static constructors use Section::fixed(...).

Client-contract follow-up (out of scope): clients that auto-refresh only on TOKEN_EXPIRED will treat a rotation's UNAUTHORIZED as logout — the desktop/theatron 401 handling should refresh on any 401 with a valid refresh token. Tracked separately.

Gate: kanon gate --full green — fmt, check, clippy (-D warnings), nextest 1969 passed, deny, lint (0/0). 5 new tests (all on a live ConfigManager/Section pair): old-token-invalidated-immediately, refresh-across-rotation, TTL-mint-forward, middleware 401-UNAUTHORIZED, and a full-router archon integration test. Gate-Passed stamped.

Refs #529

…otation (#529)

Gate-Passed: kanon 0.1.5 +stages:fmt,check,clippy,nextest,lint sha:f744a4776cc3ffadd111e57b42f55236fd96dbd1
@forkwright
forkwright merged commit 77f75f7 into main Jul 6, 2026
12 checks passed
@forkwright
forkwright deleted the audit/529-step3 branch July 6, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant