This document covers runtime behaviour of the four auth-gate types
(static_token, basic_auth, jwt, signed_url) with focus on
cache lifecycle and timing-oracle resistance.
Both static_token and basic_auth gates verify the presented credential
via a constant-time compare implemented for CloudFront Functions and
Cloudflare Workers.
Properties:
- Always iterates at least 64 positions regardless of credential length — short tokens (the common case) take constant time
- For tokens longer than 64 chars, iteration scales with
max(|a|, |b|)(same behaviour as Go'shmac.Equal) - Length mismatch does NOT short-circuit — length is included in the accumulator
- Works identically for both platforms
This blocks the byte-by-byte timing oracle described in threat model §12.
jwt gates with algorithm: RS256 fetch JWKS from jwks_url. Three
cache windows are maintained:
| Window | Default | Bound | Meaning |
|---|---|---|---|
| Fresh | 600 s (AWS) / cache_ttl_sec (Cloudflare) |
— | Serve from cache without fetching |
| Stale-if-error | 3600 s | 0..86400 | After refresh fails, keep serving last-known keys |
| Negative cache | 60 s | 0..600 | After refresh fails, skip re-fetching for this window |
Configure globally under firewall.jwks:
firewall:
jwks:
allowed_hosts:
- idp.example.com
stale_if_error_sec: 3600
negative_cache_sec: 60For Cloudflare Worker builds, firewall.jwks.allowed_hosts is required when
any RS256 JWT gate is configured. Workers cannot inspect DNS resolution targets
before fetch, so the compiler pins JWKS hosts at build time. Lambda@Edge also
uses the allowlist when present and additionally rejects DNS-resolved JWKS IPs
in loopback, private, or link-local ranges at runtime.
JWKS responses are capped at 256 KiB and 100 keys before parsing/caching.
For RS256, both runtimes select JWKs by matching kid, requiring
kty: RSA, and accepting either an omitted alg field or alg: RS256.
JWKs with a conflicting alg are ignored; the JWT header algorithm allowlist
remains the authority for accepted token algorithms.
| State | Network call? | Outcome |
|---|---|---|
| Fresh cache hit | No | Serve cached keys |
| Fresh expired + IdP OK | Yes | Refresh + serve new keys |
| Fresh expired + IdP fails + within stale-if-error | Yes (once) | Serve stale cached keys; log warning |
| Fresh expired + IdP fails + outside stale-if-error | Yes (once) | Reject JWT verification |
| Within negative-cache window + stale available | No | Serve stale cached keys; skip fetch |
| Within negative-cache window + no cache | No | Reject JWT verification |
kid not in cache |
Yes (once) | Invalidate + refetch — handles IdP key rotation |
- IdP outage doesn't cause 100% 401 as long as some tokens were
verified within
stale_if_error_secbefore the outage and the isolate / container kept the cache. - Key rotation completes transparently on the first request with the
new
kid— cache invalidates, refetches, finds the new key. - Broken IdP (persistent 5xx or DNS failure) is rate-limited to one
attempt per
negative_cache_sec. Edge functions stop hammering the IdP after the first failure.
- Edge functions are stateless per-invocation in worst case (new cold starts, isolate recycle). Cache hit ratio depends on traffic volume and CDN affinity.
- Negative cache is per-isolate / per-container. Two isolates may both hit the IdP once during an outage — the protection is best-effort.
See docs/signed-urls.md for signed-URL specifics, including exact_path,
nonce_param, and single-use enforcement at origin.