v0.4.3-beta
Spooky 0.4.3-beta — Asymmetric JWT & JWKS
Patch release since 0.4.2-beta. JWT validation moves beyond a shared secret to public-key verification, with keys pinned locally or fetched from an issuer's JWKS endpoint.
Added
RS256andES256validation — joinsHS256on the local, synchronous request path. Signature checking never makes a network call.- Static public keys —
auth.jwt.static_keys[]as PEM (kind: pem) or JWK (kind: jwk), each with akidandalg. - JWKS key sources —
auth.jwt.jwks_urlfetches keys into a background-refreshed cache. Tune withjwks_refresh_interval_secs(300),jwks_cache_ttl_secs(900),jwks_stale_if_error_secs(3600),jwks_request_timeout_ms(2000). An unknownkidtriggers a rate-limited refresh, not a per-request fetch. - Explicit cache states —
never_fetched,fresh,stale,refresh_failed_retained,quarantined_retained,empty_unusable. The*_retainedstates still serve last-known-good keys; onlyempty_unusablerejects everything. - Startup gating —
jwks_startup_behavior: require_ready(default) fails startup rather than admitting traffic with no keys; reload activation preflights the same condition.allow_degradedboots and retries in the background. - Metrics —
spooky_jwt_validation_failures_total{reason},spooky_jwt_algorithm_rejections_total{algorithm}, and per-sourcespooky_jwks_*counters and gauges (refresh outcomes, age, state, key count, refresh timestamps). - Runtime introspection —
/admin/runtimegainedjwks.sources[]and per-upstream JWT provider state underauth.providers[].
Changed
- JWT validation restructured into a canonical pipeline — JOSE parse → algorithm policy → key resolution → signature → claims — with a stable rejection vocabulary (
algorithm_not_allowed,missing_verification_key,key_source_unavailable,ambiguous_verification_key,issuer_mismatch,audience_mismatch,token_expired). - Key type is re-checked at verification time, so an asymmetric key can never satisfy an
HS256token and vice versa.alg: nonenever maps to a verification mode. allowed_algorithmsandrequire_kidare explicit policy, configured independently of the key material present.
Fixed
- Retained cache entries survive the full TTL window instead of being dropped, so a transient issuer outage no longer causes an immediate auth outage.
- Two JWKS sources sharing a URL but differing in policy merge deterministically instead of depending on iteration order.
- Tokens with no
kidare accepted only when exactly one algorithm-compatible key survives filtering — multiple candidates are rejected as ambiguous rather than guessed at.
Security
- JWKS telemetry is labelled by an opaque
jwks_source_id, never the URL, and endpoints in logs and/admin/runtimeare stripped of query strings. Credentials in a JWKS URL cannot leak through/metrics, logs, or the snapshot. - RSA keys shorter than 2048 bits are rejected, static or JWKS-published.
- A refresh failure never widens access — last-known-good keys validate until
jwks_stale_if_error_secselapses, then requests are rejected, not admitted. - Startup rejects incoherent policy:
secretwithoutHS256in the allowlist,HS256with an emptysecret, or no key material at all.
Upgrade Notes
Nothing breaks. allowed_algorithms defaults to HS256 only, static_keys is empty, and jwks_url is unset, so an existing config behaves exactly as it did.
jwks_startup_behavior defaults to require_ready. It stays dormant until you set jwks_url — but once you do, a node whose issuer is unreachable fails to boot rather than starting degraded. Use allow_degraded if you'd rather come up without keys.
static_keys, jwks_url, allowed_algorithms, require_kid, issuers, audiences, and the jwks_* fields. Roll the binary back before the config.
Migration: add allowed_algorithms and key material alongside your existing secret, watch spooky_jwt_algorithm_rejections_total while both are live, then drop HS256 and clear the secret.
Known Limitations
- No
RS384/RS512,PS*,EdDSA, or non-P-256 curves - JWKS is direct-URL only — no discovery-document resolution
- The cache is process-local, not shared across instances
- A source removed or repointed by reload isn't evicted for the process lifetime
- A token matching both a static key and a JWKS key is rejected as ambiguous
- Pre-GA — soak test before broad rollout