Skip to content

Complete production-readiness roadmap: security blockers, standalone binaries, hardening, quality & ops - #2

Merged
aupv9 merged 5 commits into
mainfrom
claude/remaining-features-plan-kmm9qr
Aug 4, 2026
Merged

aupv9 merged 5 commits into
mainfrom
claude/remaining-features-plan-kmm9qr

Conversation

@aupv9

@aupv9 aupv9 commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Implements all four milestones from ROADMAP.md, taking the gateway from "feature-complete library" to production-ready. Full suite passes with go test ./... -race; a follow-up IAM security review of the branch confirmed all previously known Critical/High findings are fixed (and its one new High finding is fixed in the last commit).

M1 — Security blockers (3fca194)

  • Step-up cookie replay bypass: the guard now re-evaluates policy against the saved path with the current token before replaying, drives StateMachine.Complete()/Fail(), and only replays flows that reach StateCompleted. Regression test proves a bronze token can no longer reach a silver-protected resource via the cookie.
  • Real DPoP (RFC 9449): cnf.jkt mapped from introspection and verified against the proof JWK's RFC 7638 thumbprint (RFC test vector covered); ath is mandatory; jti replay cache reuses token.Cache; optional server-issued nonce (DPoP-Nonce + error="use_dpop_nonce").
  • Revocation: jti → token-hash secondary index so JTI revocation evicts the real cache entry; revoke-all/session revocation is per-subject/per-session instead of Flush() on the shared cache.
  • Policy: ** glob requires a path-segment boundary (/api/** no longer matches /api-internal); max_age fails closed for tokens without auth_time.

M2 — Standalone binaries (38edd51)

  • cmd/iam-service: full env-driven wiring; auto-starts in-process LocalAS with demo tokens when IAM_OIDC_DISCOVERY_URL is unset; echo mode without upstream; graceful shutdown.
  • cmd/iam-cli: policy-check (non-zero exit on deny), token, introspect.
  • Integration smoke test builds and boots the real binary (/health, RFC 9470 challenge, SIGTERM).

M3 — Hardening & RFC gaps (c45ffaa)

  • JWT validator enforces WithValidMethods (asymmetric-only by default) plus optional issuer/audience checks.
  • Cross-tenant binding: token issuer must match the resolved tenant's provider issuer.
  • Header hygiene: client-sent X-Tenant-ID / X-Iam-* stripped; verified X-Iam-Subject/Tenant/Acr/Scopes re-injected.
  • Tenant resolution fails closed; single-tenant deployments opt in via new StaticResolver.
  • FAPI 2.0 profile enforcement wired into the guard (GuardConfig.FAPI, IAM_ENABLE_FAPI); cache TTL clamped to remaining token lifetime; CSRF StateID generated into the signed step-up cookie.

M4 — Quality & ops (941deb4)

  • Per-client-IP token-bucket rate limiter at the guard (429 + Retry-After).
  • pkg/core/token coverage 56% → 81%; cache-hit benchmark (~510ns/op).
  • Real-Redis integration test (opt-in via REDIS_ADDR), run in CI via a Redis service container.
  • GitHub Actions CI: build, vet, race tests, coverage artifact, golangci-lint.

Security re-review follow-up (01a879b)

  • Issuer binding now fails closed when the RFC 7662 introspection response omits iss (the field is optional per §2.2, but skipping the check allowed cross-tenant token reuse against ASes that omit it). Regression test added.
  • Admin bearer token compared in constant time.

Testing

  • go test ./... -race green across all packages (incl. Redis integration against a real redis-server).
  • New regression tests: step-up bypass, glob boundary, max_age fail-closed, DPoP thumbprint/replay/nonce/binding, targeted revocation, header hygiene, missing-iss fail-closed, rate limiter, binary boot smoke test.

🤖 Generated with Claude Code

https://claude.ai/code/session_016G5EUFGBr3WGXbLV971QQZ


Generated by Claude Code

claude added 5 commits August 2, 2026 15:14
- policy: ** glob now requires a path-segment boundary so /api/** no
  longer matches /api-internal or /apiv2 (matcher regression tests added)
- policy: max_age fails closed — tokens without auth_time are denied by
  policies that require auth freshness
- stepup: guard re-evaluates policy against the saved path before
  replaying a step-up cookie, drives StateMachine Complete/Fail, and
  only replays flows that reach StateCompleted (bypass regression test)
- dpop: real RFC 9449 key binding — cnf.jkt (RFC 7638 thumbprint)
  verified against the proof JWK after introspection, ath is mandatory,
  jti replay cache reuses token.Cache, optional server nonce (RFC 9449 §8)
- revocation: jti → token-hash secondary index so JTI revocation evicts
  the real cache entry; revoke-all/session eviction is per-subject and
  per-session instead of flushing the shared cache
- introspection: map aud/jti/cnf into CommonClaims, send token_type_hint
- cache: clamp TTL to remaining token lifetime, never cache expired tokens

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016G5EUFGBr3WGXbLV971QQZ
- cmd/iam-service: full gateway wiring from env vars (IAM_ADDR, IAM_REALM,
  IAM_POLICY_FILE, IAM_UPSTREAM_URL, IAM_OIDC_*, IAM_LOG_FORMAT plus admin,
  webhook, cookie, and DPoP options); auto-starts the in-process LocalAS
  with demo tokens when IAM_OIDC_DISCOVERY_URL is unset; echo mode when no
  upstream is configured; graceful shutdown on SIGINT/SIGTERM
- cmd/iam-cli: policy-check (dry-run via simulator, non-zero exit on deny),
  token (signed test JWTs via tokenfactory), introspect (RFC 7662 client)
- tenant: add StaticResolver for explicit single-tenant defaults
- tests/integration: binary boot smoke test (build, /health, RFC 9470
  challenge, SIGTERM graceful exit)
- Makefile: make service / make cli now run the new binaries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016G5EUFGBr3WGXbLV971QQZ
…iring, fail-closed tenants

- jwt: JWTValidator now enforces WithValidMethods (asymmetric algs only by
  default) and optional WithIssuer/WithAudience via ExpectedIssuer /
  ExpectedAudience config
- gateway: assert token issuer matches the resolved tenant's provider
  issuer after introspection (cross-tenant token reuse blocked)
- gateway: strip client-sent X-Tenant-ID and X-Iam-* headers before
  proxying; re-inject X-Iam-Subject/Tenant/Acr/Scopes from verified claims
- gateway: FAPI 2.0 profile enforcement wired via GuardConfig.FAPI;
  CommonClaims implements fapi.TokenClaims (HasDPoP via cnf.jkt,
  HasPARRequestURI, GetAuthAge, GetNonce); IAM_ENABLE_FAPI env in service
- tenant: resolution now fails closed — no implicit "default" fallback;
  single-tenant deployments opt in with StaticResolver; HeaderResolver
  documented as trusted-edge-only
- stepup: BeginChallenge generates the CSRF StateID into the signed cookie
- guard: pass authorization_details through to policy evaluation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016G5EUFGBr3WGXbLV971QQZ
…overage ≥80%

- gateway: per-client-IP token-bucket rate limiter (RateLimitRPS/Burst in
  GuardConfig, IAM_RATE_LIMIT_RPS env), 429 + Retry-After when exceeded
- token: coverage 56% → 81% — introspector round-trip (incl. aud string/array
  and cnf.jkt mapping, token_type_hint assertion), CachedIntrospector hit/
  revoke/expired-clamp, RedisCache round-trip with fake client, revocation
  index JSON round-trip, claims helpers
- token: cache-hit benchmark (~510ns/op, 2 allocs)
- goredis: real-Redis integration test, opt-in via REDIS_ADDR
- ci: GitHub Actions workflow — build, vet, race tests with Redis service
  container, coverage artifact, golangci-lint
- roadmap: mark all four milestones complete
- gofmt across the repo

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016G5EUFGBr3WGXbLV971QQZ
Security re-review flagged that the cross-tenant issuer binding silently
skipped when the RFC 7662 response carried no iss (the field is OPTIONAL
per §2.2), letting a token from a shared/foreign AS be accepted under the
wrong tenant. When the provider declares an issuer, an introspection
response without iss is now rejected; regression test added with a stub
provider returning active claims without iss.

Also compare the admin bearer token in constant time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016G5EUFGBr3WGXbLV971QQZ
@aupv9
aupv9 merged commit 9575edf into main Aug 4, 2026
0 of 2 checks passed
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.

2 participants