feat(server): origin-side Cloudflare Access JWT verification (JEF-473)#118
Merged
thejefflarson merged 1 commit intoJul 22, 2026
Conversation
watcher's read surface (UI shell + /api) was gated only by Cloudflare Access at the edge, with the origin trusting that traffic arrived through Access (ADR 0013). An Access-policy slip, a tunnel/ingress misconfig, or direct in-cluster access to the pod would then reach /api unauthenticated. Add origin-side defense-in-depth: a new `access_jwt` module validates the edge-issued Access JWT against the team's JWKS — RS256 signature, `iss` (team domain), `aud` (Access application AUD tag), and expiry — and an axum middleware in `app_with_access` enforces it on the UI shell + /api. The verifier is transport-agnostic (token + expected iss/aud in, claims out), so JEF-471/472's /mcp Bearer auth can reuse it. The origin only validates, never mints. Route policy: /v1 OTLP ingest and /healthz are NEVER gated — in-cluster collectors and kubelet probes carry no token, and gating them would break ingest and readiness. Config + fail-open: enforcement is wired in only when WATCHER_ACCESS_TEAM_DOMAIN + WATCHER_ACCESS_AUD are set, so local dev and non-Access deploys are unchanged. The JWKS is cached and refreshed hourly; a refresh failure with a warm cache serves last-known keys, and a cold cache with an unreachable certs endpoint fails OPEN with a loud warning (the edge stays the primary gate) rather than taking the whole read surface down on a transient Cloudflare blip. ADR 0013 is amended to record this. Tests: 8 unit tests over the verifier (valid / wrong-aud / wrong-iss / expired / tampered-sig / unknown-kid / garbage / cold-cache-unavailable, using a locally-signed JWK) and 2 smoke tests proving /api + UI shell reject a missing/invalid token when configured, a valid token passes, and /v1 + /healthz stay open. cargo fmt --check, cargo clippy --all-targets, and cargo test --locked all green (91 tests). Closes JEF-473 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
thejefflarson
deleted the
thejefflarson/jef-473-origin-side-cloudflare-access-jwt-verification-across-the
branch
July 22, 2026 05:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
watcher's read surface (UI shell +
/api) was gated only by Cloudflare Access at the edge; the origin trusted that every request arrived through Access (ADR 0013). An Access-policy slip, a tunnel/ingress misconfig, or direct in-cluster access to the pod would then reach/apiunauthenticated.This adds origin-side defense-in-depth: the origin now verifies the edge-issued Access JWT itself (it never mints one).
Changes
server/src/access_jwt.rs— a transport-agnosticVerifier: validates a raw JWT against the team's JWKS (RS256 signature,iss= team domain,aud= Access application AUD tag, expiry).verify(token) -> claimstakes the token + configured iss/aud, so JEF-471/472's/mcpBearer auth can reuse it. JWKS is cached and refreshed hourly.lib.rs—access_guardaxum middleware +app_with_access(pool, Option<Arc<Verifier>>). The middleware wraps only the UI shell +/api.app(pool)now delegates toapp_with_access(pool, None), so every existing call site is unchanged.main.rs— builds the verifier from env (Verifier::from_env) and passes it in.Route policy (exact)
/api/*Cf-Access-Jwt-Assertion/v1/*OTLP ingest/healthzConfig + fail-open
Enforcement is wired in only when both
WATCHER_ACCESS_TEAM_DOMAINandWATCHER_ACCESS_AUDare set — unset means no enforcement, so local dev / non-Access deploys are unchanged. On a JWKS refresh failure with a warm cache, last-known keys are served; on a cold cache with an unreachable certs endpoint the middleware fails open with a loud warning (the edge remains the primary gate) rather than taking the whole read surface down on a transient Cloudflare blip. This availability-biased trade for a secondary layer is documented in ADR 0013.Tests
/api+ UI shell reject missing/invalid/wrong-aud/expired tokens (401) and a valid token passes (200), while/v1ingest +/healthzstay open; unconfigured →/apiopen.cargo fmt --check,cargo clippy --all-targets,cargo test --lockedall green (91 tests).Notes for the architect
app()change to wrapping the UI+/api routers, mymain.rschange to the Access config block, and myCargo.tomladd tojsonwebtoken(separate fromrmcp). Did not createmcp.rsor touchapi.rshandlers.tests/smoke.rschanges are append-only.🤖 Generated with Claude Code