Skip to content

feat(dashboard): internal cluster SA-token auth lane for the read API (ADR-0043) - #354

Merged
thejefflarson merged 1 commit into
mainfrom
dashboard-internal-sa-auth
Nov 8, 2026
Merged

thejefflarson merged 1 commit into
mainfrom
dashboard-internal-sa-auth

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

What

Give protector's read-only dashboard JSON API a mesh-internal auth lane so an in-cluster caller (the cluster repo's fixer) authorized by its Kubernetes ServiceAccount-token JWT can read /api/*.json without a Cloudflare Access JWT it cannot mint in-cluster.

This is pure addition on top of ADR-0030's edge OIDC path, read-only, and fail-closed. The edge Access path and every mutating/admission/engine path are untouched. New ADR: docs/adr/0043. Mirrors the pattern watcher shipped for its /api (watcher PR #253 / its ADR-0022).

How

  • Second verifier, same primitive. Reuses the ADR-0030 Verifier (algorithm pinned to RS256 — the alg-confusion close; iss/aud/exp/nbf/sub all required) pointed at the cluster issuer/JWKS via a new jwks::ClusterJwksFetcher that trusts the cluster CA and presents protector's own projected SA token, read fresh on every fetch, to the authenticated …/openid/v1/jwks discovery endpoint. The same bounded-body / single-flight / rate-limited JwksStore protects this fetch.
  • One gate, two lanes, tried in order (enforce): when the lane is configured and an Authorization: Bearer token is present it is tried first —
    • verified + allowlisted sub → admitted read-only, at the floor Redacted tier (a cluster SA token carries no tier claim), subjected to the same minimum-tier gate as the edge path;
    • verified but non-allowlisted → hard 401 (never a 302, never a fall-through);
    • unverifiable → falls through to the unchanged edge path (the internal lane never fails open; an in-cluster caller sends no Cf-Access-Jwt-Assertion, so the edge path then rejects it).
  • Fail-closed everywhere. An empty allowlist, an unreadable cluster CA, or an unreachable cluster JWKS all admit no internal caller; the edge path is left intact in every case. The lane rides on the Oidc-mode Enforcer, so it is active only when the edge OIDC issuer is configured.

Config (env, all PROTECTOR_DASHBOARD_INTERNAL_*)

Variable Default Meaning
…_AUD (unset ⇒ lane off) Required audience; its presence enables the lane.
…_ALLOWED_SUBS (empty ⇒ lane off) Comma-separated exact sub allowlist.
…_ISSUER https://kubernetes.default.svc.cluster.local Expected iss.
…_JWKS_URL <issuer>/openid/v1/jwks JWKS endpoint (direct, no discovery).
…_TOKEN_PATH /var/run/secrets/kubernetes.io/serviceaccount/token protector's own SA token, read fresh per fetch.
…_CA_PATH /var/run/secrets/kubernetes.io/serviceaccount/ca.crt Cluster CA trusted for the JWKS fetch.

Tests

New unit tests (auth::internal::tests) + integration tests (enforce_tests) mirror watcher's internal-lane suite: allowlisted admit; valid-but-non-allowlisted 401; unverifiable fall-through; wrong aud/iss/alg/expiry reject; JWKS-unreachable fail-closed; and the edge Bearer path + no-token 401 still hold with the lane configured. cargo fmt --check clean, full cargo nextest run green (1273 passed).

Cluster-side follow-ups (separate repo)

The cluster repo's protector chart sets PROTECTOR_DASHBOARD_INTERNAL_AUD=protector and the allowlist to system:serviceaccount:fixer:fixer (companion PR). fixer's chart must then mount a projected serviceAccountToken with audience: protector and present it as the bearer to protector-dashboard:8080.

🤖 Generated with Claude Code

https://claude.ai/code/session_013cYVqzH7Xfwea7fAozdQK7

…(ADR-0043)

Add a mesh-internal auth lane to the dashboard read surface, ALONGSIDE the
edge Cloudflare Access path (ADR-0030): an in-cluster caller authorized by its
Kubernetes ServiceAccount-token JWT (cluster issuer, RS256, a required `aud`,
and a `sub` allowlist) can read `/api/*.json` without an edge Access JWT it
cannot mint in-cluster — the lane the cluster repo's `fixer` needs.

Pure addition, read-only, fail-closed:
- reuses the ADR-0030 `Verifier` (alg pinned to RS256 — the alg-confusion close;
  iss/aud/exp/nbf/sub all required) pointed at the cluster JWKS via a new
  `ClusterJwksFetcher` that trusts the cluster CA and presents protector's own
  projected SA token, read fresh on every fetch, to the authenticated
  `openid/v1/jwks` discovery endpoint;
- one gate, two lanes tried in order: a verified + allowlisted SA token is
  admitted read-only at the floor `Redacted` tier; a verified-but-non-allowlisted
  one is a hard 401 (never a 302, never a fall-through); an unverifiable one
  falls through to the UNCHANGED edge path (the internal lane never fails open);
- fails closed everywhere: an empty allowlist, an unreadable cluster CA, or an
  unreachable cluster JWKS all admit no internal caller, and the edge path and
  every mutating/admission path are untouched.

Config via `PROTECTOR_DASHBOARD_INTERNAL_*` (aud enables the lane; allowed subs;
issuer/jwks/token/ca default to the in-pod projected-token locations). The lane
rides on the Oidc-mode Enforcer, so it is active only when the edge OIDC issuer
is configured.

New unit + integration tests mirror watcher's internal-lane suite (allowlisted
admit; valid-non-allowlisted 401; unverifiable fall-through; wrong aud/iss/alg/
expiry reject; JWKS-unreachable fail-closed) and prove the edge Bearer path and
no-token 401 still hold with the lane configured.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013cYVqzH7Xfwea7fAozdQK7
@thejefflarson
thejefflarson force-pushed the dashboard-internal-sa-auth branch from bf642c5 to 3b1a1cd Compare November 8, 2026 05:19
@thejefflarson
thejefflarson merged commit 521ffd0 into main Nov 8, 2026
5 checks passed
@thejefflarson
thejefflarson deleted the dashboard-internal-sa-auth branch November 8, 2026 05:26
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