Spec: Workload identity — production hardening
Status: DRAFT. Built one PR at a time; each item is independently shippable.
1. Problem & outcome
The modular workload-identity layer + SPIRE bundle
(spec-modular-workload-identity.md,
spec-shield-identity-bundle.md) are functional
and unit-tested, but not production-ready: the X.509 SVID validation is
forgeable, oidc_sa is unverified against a real issuer, the SPIRE bundle is
PoC-config and never executed, and the “only-Envoy-reaches-Shield” trust boundary
is documentation, not enforcement.
Outcome: each gap closed by a scoped, tested PR, so the oidc_sa + Envoy-mTLS
path is genuinely safe for production, and the SPIFFE direct path is unblocked.
Non-goals
- Not adding new identity features — hardening only.
- Not making SPIRE HA/Postgres mandatory — ship a production profile, keep the PoC profile for dev.
2. Plane & latency contract
Data plane. The one hot-adjacent change is the X.509 verify (item 1), which runs
only inside SPIFFEMiddleware on requests carrying an SVID header — not on
/guardrails/*, cap/mint, tools/call. Verification is CPU-only (no I/O); a
signature check is sub-millisecond. Off hot path.
3. Data model
No new Redis state. Config/env only (SPIRE production profile, XFCC-trust flag).
4. API / interface
No new public endpoints. Internal: validate_x509_svid gains real chain
verification; a new middleware guard restricts XFCC trust to a configured proxy
source.
5. Security & backward compatibility
- Item 1 makes validation stricter — a forged cert that used to pass now fails (that’s the point). Faithful SVIDs (CA-signed, unexpired) still pass, so legitimate callers are unaffected. No opt-out (it’s a security fix).
- Item 4 (XFCC trust) defaults to the current behavior unless the deployment sets the trusted-proxy flag — opt-in, non-breaking.
6. Packaging & deploy
No new pip deps (cryptography>=42 already pins the API used). SPIRE production
profile is config files under deploy/identity/. No Dockerfile.admin change.
7. Failure modes & edge cases
- Expired SVID, wrong CA, self-signed forgery, missing SAN, malformed PEM, bundle with multiple CAs, intermediate chains → all must reject except the genuinely-valid case.
- Empty/absent trust bundle → reject (fail-closed), never accept unverified.
8. Test plan (Definition of Done)
Per item below. Overall: full suite green in a clean venv; CI passes; the bundle smoke passes on a Docker host.
The hardening items (build order)
PR 1 — Real X.509 SVID chain verification ⟵ hard blocker, build first
core/oauth/spiffe.py::validate_x509_svid currently does a name-only issuer
check (spiffe.py:211) — a self-signed cert with a
copied issuer DN is accepted (confirmed PoC; task_02d25da6).
- Fix: verify the leaf is cryptographically signed by a CA in the trust
bundle (
cert.verify_directly_issued_by(ca)), and enforce validity dates (not_valid_before/after). Keep the trust-domain + allowlist checks. - Tests (
tests/test_spiffe_x509.py): CA-signed valid → accept; self-signed copied-DN forgery → reject; expired → reject; wrong-CA → reject; no SAN → reject; multi-CA bundle picks the right signer. Closestask_02d25da6.
PR 2 — oidc_sa hardening
- Make JWKS resolution non-blocking (thread-offload or cached-async) so a slow issuer can’t stall the event loop; keep the PyJWKClient cache.
- Add a real-issuer integration test (mock OIDC server serving discovery + JWKS) exercising discovery, not just an injected key.
- Document k8s-issuer setup (projected SA token audience).
PR 3 — SPIRE production profile
deploy/identity/prod/: Postgres datastore, HA server (3 replicas), dropinsecure_bootstrap, real node attestation (k8s_psat/aws_iid/x509pop), upstream CA / KMSUpstreamAuthority, bundle rotation via spiffe-helper.- Document the SQLite/join_token profile as dev-only.
PR 4 — Enforce the XFCC trust boundary
- A middleware/config guard: when
SHIELD_TRUSTED_PROXY_ONLY=true, acceptX-Forwarded-Client-Cert(and derived SPIFFE identity) only from a configured source (proxy IP / mTLS peer), and strip it otherwise — so a direct client can’t spoof XFCC even if it reaches Shield. Default off (non-breaking). - Tests: spoofed XFCC from a non-proxy source ignored; from the proxy honored.
PR 5 — Bundle integration test in CI
- A CI job that boots the compose bundle (or
kind+ Helm) and runssmoke_identity_bundle.sh(no-cert refused, forged SVID rejected, XFCC stripped). Gates regressions the unit suite can’t.
PR 6 — mtls provider audit + hardening
- Audit
MTLSMiddlewarevalidation rigor; apply the same real-verification bar as PR 1 if it relies on name-only checks; add tests.
PR 7 — Security review of the issuance gate
/security-reviewon the token-issuance path change; address findings.
Sequence & gating
1 → unblocks SPIFFE (must land first). 2 → makes the no-SPIFFE path prod-safe.
3–5 → make the bundle real and regression-guarded. 6–7 → close the remaining
audit gaps. A deployment is prod-ready for the oidc_sa+Envoy path after 1, 2,
4, 5; for the direct-SVID path also after 3, 6.