Skip to content

v0.4.3-beta

Choose a tag to compare

@nishujangra nishujangra released this 06 Aug 04:18
· 248 commits to master since this release
bd001dd

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

  • RS256 and ES256 validation — joins HS256 on the local, synchronous request path. Signature checking never makes a network call.
  • Static public keysauth.jwt.static_keys[] as PEM (kind: pem) or JWK (kind: jwk), each with a kid and alg.
  • JWKS key sourcesauth.jwt.jwks_url fetches keys into a background-refreshed cache. Tune with jwks_refresh_interval_secs (300), jwks_cache_ttl_secs (900), jwks_stale_if_error_secs (3600), jwks_request_timeout_ms (2000). An unknown kid triggers a rate-limited refresh, not a per-request fetch.
  • Explicit cache statesnever_fetched, fresh, stale, refresh_failed_retained, quarantined_retained, empty_unusable. The *_retained states still serve last-known-good keys; only empty_unusable rejects everything.
  • Startup gatingjwks_startup_behavior: require_ready (default) fails startup rather than admitting traffic with no keys; reload activation preflights the same condition. allow_degraded boots and retries in the background.
  • Metricsspooky_jwt_validation_failures_total{reason}, spooky_jwt_algorithm_rejections_total{algorithm}, and per-source spooky_jwks_* counters and gauges (refresh outcomes, age, state, key count, refresh timestamps).
  • Runtime introspection/admin/runtime gained jwks.sources[] and per-upstream JWT provider state under auth.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 HS256 token and vice versa. alg: none never maps to a verification mode.
  • allowed_algorithms and require_kid are 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 kid are 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/runtime are 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_secs elapses, then requests are rejected, not admitted.
  • Startup rejects incoherent policy: secret without HS256 in the allowlist, HS256 with an empty secret, 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.

⚠️ One-way config compatibility: 0.4.3 reads a 0.4.2 config, but 0.4.2 rejects 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