Stability checkpoint v0.7.1 — multi-issuer ALLOWLIST via MultiIssuerVerifier (ADR-0041) Closes the v0.7 multi-issuer ticket. Comma-separated SECURED_CLAUDE_IDP_ISSUER now becomes an ALLOWLIST ; the broker accepts JWTs from any of N IdPs by routing each token to the correct OIDCVerifier based on its `iss` claim. - feat(security): multi-issuer ALLOWLIST via MultiIssuerVerifier (ADR-0041) (aee6fa1) CI : - ✅ MR pipeline #165 green — https://gitlab.com/benoit.besson/secured-claude/-/pipelines/2491945046 - ✅ Main pipeline #166 green — https://gitlab.com/benoit.besson/secured-claude/-/pipelines/2491956024 Local test pass : - ✅ uv run pytest -q → 227 tests pass (was 216 in v0.7.0 ; +11 multi-issuer tests) - ✅ Coverage 90.96 % total (gate fail_under=90 % held) - ✅ uv run ruff check src tests → clean - ✅ uv run ruff format --check src tests → 32 files already formatted - ✅ uv run mypy src → strict mode clean (13 source files) - ✅ gitleaks detect → no leaks found Regression check vs v0.7.0 : - ✅ Single-issuer SECURED_CLAUDE_IDP_ISSUER still returns bare OIDCVerifier (back-compat) - ✅ OIDCVerifier untouched — wrapper class chosen over in-place refactor - ✅ Existing _jwks_ts mutation pattern in tests still works - ✅ All v0.6.x + v0.7.0 features still active : TTL cache + bearer + max-stale-age + mTLS + JWT validation - ✅ docker-compose.yml still bind-mount-free - ✅ 5 cosign-signed images still cover binary + policy bytes (carryover) - Claude Code wrapped, every tool call gated by Cerbos PDP, audited in SQLite hash-chain log (carryover) - ✨ Multi-issuer ALLOWLIST : token's `iss` claim must match one of N configured issuers ; routing decision is unverified-then-pinned (signature still validated against the matched issuer's actual JWKS). - ✨ 4 deployment patterns unlock : multi-tenant SaaS (per-tenant Auth0 / Okta / Keycloak), M&A migration windows (both IdPs co-existing), hybrid cloud (AD + GitHub Actions OIDC + GCP/AWS WI), DR failover (primary + secondary IdPs). - ✨ Fail-closed allowlist : iss missing / not in allowlist → reject + WARNING log line for visibility. - 5 cosign-signed images cover the FULL supply chain — carryover - ⏭ N/A — IdP shape change, no new domain features - ✨ Hybrid-cloud-friendly : token from corporate AD, GitHub Actions, GCP Workload Identity, AWS IAM Roles Anywhere — all accepted via the same comma-separated env knob. - Per-issuer JWKS caches independent ; first request per tenant pays one-time discovery + JWKS fetch ; subsequent requests hit per-tenant cache. - ✨ WARNING log line on routing-misses (allowlist rejects) — operators tailing logs see attempted impersonation. - 227 tests pass (was 216 in v0.7.0 ; +11 multi-issuer covering routing / allowlist / empty-iss / env parsing) - Coverage 90.96 % (oidc.py 91 %, gateway.py 100 %) - mypy strict clean ; ruff + format clean - bandit / pip-audit / grype / trivy / gitleaks all clean (carryover) - 8 stages green on tag pipelines : lint / test / security / build / smoke / publish / release - 5-image multi-arch (amd64 + arm64) ; cosign keyless OIDC signing on all 5 - Main pipeline 20/20 green - ✨ ADR-0041 added (41 ADRs total ; was 40 in v0.7.0) - Wrapper-class pattern (MultiIssuerVerifier wraps N OIDCVerifiers) — chose over in-place refactor for back-compat + zero existing-test breakage. Both classes share `verify_token` shape so gateway doesn't care. - Unverified-then-pinned routing : iss extracted without sig check (lookup decision), then full validation with matched verifier (trust decision). Standard PyJWT pattern. - ⏭ N/A — broker is CLI + HTTP only - ✨ SECURED_CLAUDE_IDP_ISSUER now accepts comma-separated values (single-value still works) — operators upgrade by adding more issuers, no other changes - Whitespace-tolerant : `a, b` works ; trailing slashes normalised away ; empty entries silently dropped - 0 new env vars — same SECURED_CLAUDE_IDP_ISSUER reused - Per-issuer audience / bearer / mTLS config not supported — all wrapped verifiers share the same SECURED_CLAUDE_OIDC_AUDIENCE / SECURED_CLAUDE_IDP_BEARER_TOKEN / SECURED_CLAUDE_IDP_CLIENT_CERT_PATH. Operators with mixed-auth needs (one IdP needs bearer on JWKS, another doesn't) need v0.7+. - Subdomain wildcards (`https://*.auth0.com`) not supported — explicit allowlist only ; future ADR if a deployment proves the wildcard pattern is needed. - Background JWKS refresh thread (proactive re-fetch before TTL expiry) deferred to v0.7.x+. - Prometheus counter for routing-misses / cache-drops / mTLS events deferred to v0.7.x+. - mTLS for agent ↔ broker still v0.8+ (this rev covers broker ↔ IdP only). - v0.7.2 — Prometheus counters for routing-misses + cache-drops + mTLS events (operational signal) - v0.7.3 — background JWKS refresh thread (only if request-latency-during-refresh becomes a concern) - v0.8 — agent ↔ broker mTLS (replace JWT with cert-based auth on the hook connection ; bigger architectural piece)