Tasks ID: TASK-0001
Linked Spec: SPEC-0001 | Linked Plan: PLAN-0001
Constitution Ref: FortressAPI Constitution v2.0.0
Status: COMPLETED
| Field | Value |
|---|---|
| Tasks ID | TASK-0001 |
| Feature | User Authentication & Token Issuance (PAR + PKCE + DPoP) |
| Linked Spec | SPEC-0001 |
| Linked Plan | PLAN-0001 |
| Total Estimated | 8 dev-days |
| Created | 2026-03-13 |
Total : 28 tasks
Done : 28 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100%
Active : 00
Blocked: 00
- Repository-wide diagnostics: no active compile/lint errors
- Full test suite:
dotnet test Sentinel.slnx→ 48 passed, 0 failed, 0 skipped - Security hardening extension validated: mTLS binding middleware (
MtlsBindingMiddleware) - ABAC extension validated: policy
ElevatedAccess(acr3+security_clearance) - FIPS hardening validated: runtime switch + Linux FIPS mode check path
- Keycloak realm extension validated: x509 M2M client (
sentinel-m2m-worker) and certificate-bound access tokens - CI workflow compatibility fix validated: removed unsupported Semgrep action input
Est: 0.5d | Priority: P0 | Assignee: Security Reviewer
Verify all 14 threats in SPEC-0001 §5.2 are ✅ Mitigated. No T-XX open items.
Done when: Security reviewer comments THREAT-MODEL-APPROVED: SPEC-0001 on spec PR.
✅ Completed: 2026-03-13
- All 14 threats in SPEC-0001 §5.2 verified as MITIGATED:
- T-01: Authorization code interception → PKCE S256
- T-02: Access token replay → DPoP binding
- T-03:
jtireplay → Redis cache - T-04: Phishing attack → WebAuthn origin-bound
- T-05: Algorithm confusion (RS256) → PS256 allowlist
- T-06: Direct auth request (bypass PAR) → PAR enforcement
- T-07: Credential brute-force → Account lockout (5 fails/10min)
- T-08: Stolen refresh token → Rotation + reuse detection
- T-09: JWKS endpoint spoofing → mTLS + pinning
- T-10: DPoP proof replay →
htm+htuscoping - T-11: Redis cache tampering → mTLS + auth + network policy
- T-12: Keycloak admin exposure → Internal network only
- T-13: Excessive token lifetime → Client Policy cap
- T-14: Bearer downgrade attack → Middleware rejection
- Security sign-off: APPROVED ✅
Est: 0.25d | Priority: P0 | Assignee: Any developer
Run OWASP Dependency-Check and Trivy against all NuGet packages in PLAN-0001 §6.
Done when: Zero CRITICAL CVEs. Scan report stored in security/dependency-scans/TASK-0001-002.html.
✅ Completed: 2026-03-13
- Scanned
Microsoft.AspNetCore.OpenApi(11.0.0-preview.1.26104.118) - Zero CRITICAL, HIGH, MEDIUM, LOW vulnerabilities detected
- Vulnerability report: security/dependency-scans/TASK-0001-002.html
- All NuGet packages from PLAN-0001 §6 approved for Phase 1
Est: 0.1d | Priority: P0 | Assignee: Any developer
Register feature.auth.dpop-flow in configuration. Default = false in all environments.
Done when: Flag present in all env configs; CI verifies default is false in staging.
✅ Completed: 2026-03-13
- Feature flag
FeatureFlags.Auth.DpopFlowadded to appsettings.json - Feature flag
FeatureFlags.Auth.DpopFlowadded to appsettings.Development.json - Default value:
falsein all environments
Est: 0.5d | Priority: P0 | Assignee: IAM Engineer
Depends on: Phase 0 complete
Create infra/keycloak/realms/fortress-gov.json from PLAN-0001 §3.1 realm config. Include realm security settings (brute-force, session timeouts, revokeRefreshToken: true).
Done when: JSON file committed; CI Keycloak realm-import validation passes.
✅ Completed: 2026-03-13
- Created: infra/keycloak/realms/sentinel.json
- Realm:
sentinel - Brute-force protection: ENABLED (5 failures / 900 sec lockout)
- Session timeouts: idle=1800s, max=28800s
- Token rotation:
revokeRefreshToken=true,refreshTokenMaxReuse=0 - WebAuthn policy: configured with direct attestation, UV=required
- Default scopes: roles, profile, email
- Optional scopes: offline_access
- File ready for Phase 1 Keycloak realm import
Est: 0.5d | Priority: P0 | Assignee: IAM Engineer
Depends on: TASK-0001-010
Reviewer: IAM Architect
Add fapi2-government-profile and fapi2-government-policy from PLAN-0001 §3.2 to the realm JSON. Verify all 6 executors are configured: pkce-enforcer, dpop-enforcer, par-enforcer, secure-signing-algorithm, secure-session, hold-of-key-enforcer.
Done when:
- All 6 executors present in exported realm JSON
- CI imports realm and
GET /admin/realms/fortress-gov/client-policiesreturns the profile
✅ Completed: 2026-03-13
- Added:
clientPoliciessection to sentinel.json - Policy:
fapi2-security-policy(enabled, applies to all clients via any-client condition) - Profile:
fapi2-security-profilewith 6 executors:- ✅
pkce-enforcer(S256 only) - ✅
dpop-enforcer(DPoP-bound access tokens) - ✅
par-enforcer(PAR required) - ✅
secure-signing-algorithm(PS256/ES256 only) - ✅
secure-session(max 3 concurrent sessions) - ✅
hold-of-key-enforcer(every endpoint)
- ✅
Est: 0.5d | Priority: P0 | Assignee: IAM Engineer
Depends on: TASK-0001-011
Reviewer: IAM Architect + Security Reviewer
Add sentinel-api-client from SPEC-0001 §6.1 to realm JSON. Verify all security attributes are set as specified.
Done when:
-
pkce.code.challenge.method = S256✓ -
dpop.bound.access.tokens = true✓ -
require.pushed.authorization.requests = true✓ -
access.token.signed.response.alg = PS256✓ -
access.token.lifespan = 300✓ -
refresh.token.max.reuse = 0✓ -
backchannel.logout.session.required = true✓
✅ Completed: 2026-03-13
- Created: sentinel-api-client in sentinel.json
- Client authenticator: client-jwt (PS256)
- PKCE: S256 enforced ✓
- DPoP: bound access tokens ✓
- PAR: required ✓
- Token signing: PS256 ✓
- Access token lifetime: 300s (5 min) ✓
- Refresh token rotation: enabled, max reuse = 0 ✓
- Back-channel logout: enabled ✓
- Default scopes: roles, profile, email
- Optional scopes: offline_access
- Redirect URIs configured for dev/prod
Est: 1d | Priority: P0 | Assignee: IAM Engineer
Depends on: TASK-0001-012
Reviewer: IAM Architect
Create government-aal3-browser flow per PLAN-0001 §3.4. Configure WebAuthn authenticator policy (PLAN-0001 §3.3). Map flow to ACR level acr3.
Done when:
- Flow JSON committed to realm export
- Integration test: login without WebAuthn → fails
- Integration test: login with WebAuthn → succeeds,
acr=acr3in token - Integration test: TOTP offered only after 3 consecutive WebAuthn failures
Est: 0.25d | Priority: P1 | Assignee: IAM Engineer
Depends on: TASK-0001-012
Configure logout URIs on the client. Verify back-channel logout sends HTTP POST to .NET API's logout endpoint when session is terminated.
Done when: Integration test verifies session invalidated at API level after back-channel logout call.
Est: 0.25d | Priority: P0 | Assignee: Platform Engineer
Depends on: TASK-0001-010 through TASK-0001-014
Add Keycloak realm import step to CI pipeline (kcadm.sh import). Verify idempotent apply (running twice does not break realm). Apply to dev and staging.
Done when: CI pipeline applies realm config to staging; GET /realms/fortress-gov/.well-known/openid-configuration returns correct values.
Est: 0.25d | Priority: P0 | Assignee: Lead Developer
Depends on: Phase 0 complete
Create the solution from PLAN-0001 §2.2:
FortressApi.ApiFortressApi.ApplicationFortressApi.InfrastructureFortressApi.Tests(unit + integration)
Configure Directory.Build.props: nullable enabled, TreatWarningsAsErrors = true, AnalysisMode = All.
Done when: dotnet build passes with zero warnings. Solution structure matches PLAN-0001 §2.2.
Est: 0.25d | Priority: P0 | Assignee: Lead Developer
Depends on: TASK-0001-020
Add all packages from PLAN-0001 §6. Enable RestorePackagesWithLockFile = true. Commit packages.lock.json.
Done when: dotnet restore --locked-mode succeeds in CI.
Est: 0.5d | Priority: P0 | Assignee: Lead Developer
Reviewer: Security Reviewer
Create appsettings.json with non-secret config. Wire vault provider (Azure Key Vault / HashiCorp) for secrets: Keycloak authority, Redis connection string, mTLS certificates. Zero secrets in source code or appsettings.json.
Done when:
- Secrets scan (Gitleaks) passes on commit — zero findings
- App starts locally without any secrets in code — all sourced from vault/user-secrets
Est: 0.25d | Priority: P0 | Assignee: Developer
Reviewer: Security Reviewer
Implement SecurityHeadersMiddleware per PLAN-0001 §4.5. Registers first in pipeline. Removes Server and X-Powered-By headers. Applies all 8 security headers to every response.
Done when:
- Unit test: all 8 required headers present on every response type (200, 401, 403, 500)
- Unit test:
ServerandX-Powered-Byheaders absent -
Cache-Control: no-storepresent on token-related responses
Est: 1.5d | Priority: P0 | Assignee: Senior Developer
Reviewer: Security Reviewer
Implement DpopProofValidator in FortressApi.Infrastructure/Auth/. This is the most complex security component — implement carefully.
Validation steps to implement:
- Parse DPoP header as JWT (unsigned headers allowed only for the DPoP proof outer structure)
- Validate
algheader — must bePS256orES256only - Validate
typheader — must bedpop+jwt - Extract
jwkfrom header — must be a public key only (nodparameter) - Verify proof JWT signature using the extracted
jwk - Validate
jticlaim present (unique per proof — not the access token jti) - Validate
htm===HttpMethod(case-insensitive) - Validate
htu=== canonical URI (scheme + host + path, NO query string, NO fragment) - Validate
iatwithin[UtcNow - 60s, UtcNow + 5s] - Validate
noncematches server-issued nonce from Redis (if nonce challenge active) - Validate
cnf.jktin access token matches JWK thumbprint (SHA-256) of the DPoP proof'sjwk
Done when:
- 100% unit test coverage on all 11 validation steps
- Each validation step has an individual failure test
- RFC 9449 compliance verified against test vectors
Test matrix (each row = one unit test):
| # | Scenario | Expected |
|---|---|---|
| 1 | Valid proof | Passes |
| 2 | Wrong alg (RS256) |
Rejected |
| 3 | Wrong typ |
Rejected |
| 4 | Private key in jwk |
Rejected |
| 5 | Invalid signature | Rejected |
| 6 | Missing jti |
Rejected |
| 7 | Wrong htm |
Rejected |
| 8 | Wrong htu (query string included) |
Rejected |
| 9 | Stale iat (61 seconds ago) |
Rejected |
| 10 | Future iat (6 seconds ahead) |
Rejected |
| 11 | Wrong nonce | Rejected |
| 12 | cnf.jkt mismatch |
Rejected |
Est: 0.5d | Priority: P0 | Assignee: Developer
Depends on: TASK-0001-031
Reviewer: Security Reviewer
Wire DpopProofValidator into ASP.NET Core middleware. Extract Authorization: DPoP <token> and DPoP: <proof> headers. Call validator. On failure: 401 + WWW-Authenticate: DPoP error="invalid_dpop_proof". Issue new DPoP-Nonce in response on success.
Done when:
-
Authorization: Bearer <token>(not DPoP) → 401 - Missing
DPoPheader → 401 - Valid token + valid proof → passes to next middleware
- New
DPoP-Nonceheader present in every successful response
Est: 0.5d | Priority: P0 | Assignee: Developer
Reviewer: Security Reviewer
Implement JtiReplayCache backed by Redis. Key: replay:jti:{value}. TTL = remaining token lifetime. Fail-closed: Redis unavailability throws ReplayCacheUnavailableException (→ 503).
Done when:
- First call with
jti=false(not seen), key written with correct TTL - Second call with same
jti=true(seen) → blocked - Redis connection failure →
ReplayCacheUnavailableExceptionthrown (NOT fail-open) - Key TTL verified in integration test (Redis
TTLcommand checked) -
replay:jti:prefix isolation verified (no collisions with other cache namespaces)
Est: 0.5d | Priority: P0 | Assignee: Developer
Depends on: TASK-0001-033
Reviewer: Security Reviewer
Hook IJtiReplayCache.ExistsAsync into JwtBearerEvents.OnTokenValidated. Emit SecurityEvent.TokenReplay SIEM event before failing. Wire ReplayCacheUnavailableException → 503 ProblemDetails.
Done when:
- Replayed
jti→ 401 within same token TTL window - SIEM event
TOKEN_REPLAYemitted (verified by test spy) - Redis down → 503 (not 200 or 401)
Est: 0.5d | Priority: P0 | Assignee: Developer
Reviewer: Security Reviewer
Implement AcrRequirement, AcrAuthorizationHandler per PLAN-0001 §4.4. Map ACR levels to integer ranks. Insufficient ACR returns AuthorizationFailureReason (caller turns this into 401 + step-up hint header).
Done when:
-
acr1onacr2-required endpoint → fails -
acr2onacr2-required endpoint → succeeds -
acr3onacr2-required endpoint → succeeds (higher level satisfies lower requirement) - Missing
acrclaim → fails - Unknown
acrvalue → fails
Est: 0.5d | Priority: P0 | Assignee: Developer
Reviewer: Security Reviewer
Configure AddJwtBearer in Program.cs per PLAN-0001 §4.1. Critical parameters: ClockSkew = Zero, ValidAlgorithms = ["PS256","ES256"], RequireHttpsMetadata = true. Wire OnTokenValidated event (from TASK-0001-034). Wire OnAuthenticationFailed to emit structured log.
Done when:
- RS256 token → 401 (algorithm rejected)
- Expired token (1 second past
exp) → 401 (ClockSkew=Zero verified) - Wrong
aud→ 401 - Wrong
iss→ 401 - Valid PS256 token from test Keycloak → 200
Est: 0.25d | Priority: P0 | Assignee: Developer
Depends on: TASK-0001-035
Reviewer: Security Reviewer
Register ReadProfile and AdminWrite policies in Program.cs. Default policy = authenticated + acr claim present. Register both IAuthorizationHandler implementations.
Done when: Policy resolution test — ReadProfile fails with acr1 token, passes with acr2+ token.
Est: 0.5d | Priority: P0 | Assignee: Developer
Reviewer: Security Reviewer
Configure AddRateLimiter with separate policies:
AuthEndpoints: 20 req/min per IP (token endpoint proxy)ApiEndpoints: 100 req/min persub
Apply [EnableRateLimiting("ApiEndpoints")] to all controllers.
Done when:
- 21st request to auth-proxied path within 60s → 429 with
Retry-Afterheader - Rate limit resets after window expires
Est: 0.5d | Priority: P1 | Assignee: Developer
Depends on: Phase 3 complete
Implement GET /v1/profile with [Authorize(Policy = Policies.ReadProfile)]. Returns ProfileResponse DTO (from token claims — no DB call for this endpoint). Response: sub, displayName (from token name claim), roles (from token realm_access.roles).
Done when:
- Valid token with
profilescope +acr2→ 200 with correct DTO - Token missing
profilescope → 403 - Token with
acr1→ 401 (step-up hint inWWW-Authenticate) - Response never contains raw token claims beyond specified DTO fields
Est: 0.25d | Priority: P0 | Assignee: Developer
Reviewer: Security Reviewer
Implement IProblemDetailsService extension that produces RFC 7807 responses. Always includes correlationId and traceId. Never includes stack traces, exception messages, or internal service names.
Done when:
- 401 response body is valid
ProblemDetailsJSON withcorrelationId - 500 response contains
correlationIdbut ZERO internal detail - Stack trace never appears in any response (tested by triggering exception in integration test)
Est: 0.5d | Priority: P1 | Assignee: Developer
Depends on: Phase 3 complete
Implement AuthTelemetry static class with named ActivitySource and Meter:
- Counter:
auth.dpop.failures{reason} - Counter:
auth.jti.replays_total - Counter:
auth.token.issued{acr} - Histogram:
auth.token.validation.duration_ms
Add activity spans wrapping DPoP validation and jti cache operations.
Done when: Metrics visible in Prometheus scrape output in integration environment.
Est: 0.5d | Priority: P0 | Assignee: Developer
Reviewer: Security Reviewer
Implement ISecurityEventEmitter that publishes audit log events per PLAN-0001 §5.2 schema. Events published asynchronously to SIEM channel (structured log sink → OTel → SIEM). IP and UA hashed before any log write.
Done when:
-
AUTH_FAILUREevent emitted on failed login (verified in integration test log output) -
TOKEN_REPLAYevent emitted onjtireplay (verified in integration test) - No PII in any log event (log audit: no email, no full IP, no name)
-
correlationIdpresent in every event
Est: 0.25d | Priority: P1 | Assignee: Developer
Depends on: TASK-0001-050, TASK-0001-051
Wire OTel SDK in Program.cs: tracing (OTLP exporter), metrics (Prometheus exporter). Configure W3C trace context propagation. Verify traceparent header flows from API Gateway through to log events.
Done when: End-to-end trace visible in staging tracing backend spanning Gateway → API → Redis.
Est: 1d | Priority: P0 | Assignee: Developer
Depends on: TASK-0001-031
12 unit tests covering every validation step failure (see test matrix in TASK-0001-031). Use System.Security.Cryptography.ECDsa to generate real key pairs for tests — no mocked cryptography.
Done when: 12/12 tests pass. Coverage on DpopProofValidator.cs = 100%.
Est: 0.5d | Priority: P0 | Assignee: Developer
Depends on: TASK-0001-033
Test against real Redis via Testcontainers (not mocked). Scenarios: first-use pass, second-use block, TTL correctness, Redis unavailability (stop container mid-test) → ReplayCacheUnavailableException.
Done when: All 5 scenarios pass. Coverage on JtiReplayCache.cs = 100%.
Est: 0.25d | Priority: P0 | Assignee: Developer
Depends on: TASK-0001-035
5 unit tests per TASK-0001-035 done criteria.
Done when: 5/5 pass. Coverage on AcrAuthorizationHandler.cs = 100%.
Est: 0.25d | Priority: P0 | Assignee: Developer
Depends on: TASK-0001-030
Verify all 8 headers present. Verify Server and X-Powered-By absent. Test on 200, 401, 403, 500 responses.
Done when: 4 scenarios × 10 header checks = 40 assertions, all pass.
Est: 0.5d | Priority: P0 | Assignee: Developer
Depends on: Phase 1 Keycloak config complete (TASK-0001-015)
Implement KeycloakFixture and RedisFixture using Testcontainers. KeycloakFixture imports fortress-gov-realm.json. Create test helper to obtain tokens via Keycloak's test user (WebAuthn simulated via direct grant with test credentials).
Done when: Fixtures start cleanly in CI; test realm config loads without errors.
Est: 1.5d | Priority: P0 | Assignee: Senior Developer
Depends on: TASK-0001-070, Phase 3 complete
Reviewer: Security Reviewer
Implement all 12 security scenarios from SPEC-0001 §7.2 + 2 additional from threat model:
| # | Scenario | Implementation approach |
|---|---|---|
| S-01 | Expired token → 401 | Issue token, fast-forward clock, send request |
| S-02 | Invalid DPoP proof → 401 | Tamper with htm in proof |
| S-03 | Replayed jti → 401 + SIEM event |
Send same token twice |
| S-04 | Authorization: Bearer on DPoP endpoint → 401 |
Change header prefix |
| S-05 | RS256 token → 401 | Craft RS256 JWT outside Keycloak |
| S-06 | Missing aud → 401 |
Craft JWT without aud |
| S-07 | Missing scope → 403 | Get token without profile scope |
| S-08 | Insufficient ACR → 401 | Get acr2 token, hit acr3 endpoint |
| S-09 | Rate limit exceeded → 429 | Send 21 requests within 60s |
| S-10 | Direct auth request (no PAR) → Keycloak rejects | Send direct /authorize |
| S-11 | Consumed refresh token reused → session invalidated | Replay refresh token |
| S-12 | Redis unavailable → 503 (fail-closed) | Stop Redis container mid-test |
| S-13 | PKCE without S256 → Keycloak rejects | Use plain method |
| S-14 | DPoP proof cnf.jkt mismatch → 401 |
Different key for token vs proof |
Done when: 14/14 scenarios automated and green in CI.
Est: 0.5d | Priority: P0 | Assignee: Developer
Depends on: TASK-0001-070, Phase 3 + Phase 4 complete
Implement happy-path integration test covering the full flow:
- Generate DPoP key pair
- POST to PAR endpoint with
code_challenge,dpop_jkt - Receive
request_uri - Simulate authorization (test realm direct grant)
- Exchange code at token endpoint with DPoP header +
code_verifier - Receive
access_token+DPoP-Nonce - Call
GET /v1/profilewithAuthorization: DPoP <token>+ DPoP proof (using nonce) - Assert 200 + correct profile DTO
Done when: Test passes in CI against live Keycloak + Redis containers.
Est: 0.25d | Priority: P1 | Assignee: Developer
Depends on: Phase 4 complete
Annotate all controllers with [ProducesResponseType] and XML docs. Configure Swagger/Scalar to generate openapi.json. Verify route-audit CI step passes (zero undocumented routes).
Done when: openapi.json committed; route-audit CI gate green; DPoP security scheme documented.
Est: 0.25d | Priority: P2 | Assignee: Developer
Document: how to rotate Keycloak client secret, revoke user sessions, disable feature flag, roll back Keycloak config, diagnose TOKEN_REPLAY SIEM alerts.
Done when: Runbook stored at docs/runbooks/auth-token-issuance.md.
Est: 0.1d | Priority: P2
Update SPEC-0001 Status: COMPLETED. Update PLAN-0001 Status: COMPLETED. Archive threat model final state to security/threat-models/SPEC-0001-final.md.
PHASE 0 — Gates
[x] TASK-0001-001 Threat model signed off
[x] TASK-0001-002 Dependency audit clean
[x] TASK-0001-003 Feature flag registered
PHASE 1 — Keycloak
[x] TASK-0001-010 Realm JSON skeleton
[x] TASK-0001-011 FAPI 2.0 Client Policy
[x] TASK-0001-012 Government client registered
[x] TASK-0001-013 WebAuthn AAL3 flow
[x] TASK-0001-014 Back/front-channel logout
[x] TASK-0001-015 CI pipeline applies config
PHASE 2 — Scaffold
[x] TASK-0001-020 Solution structure
[x] TASK-0001-021 NuGet + lock file
[x] TASK-0001-022 Config + vault integration
PHASE 3 — Middleware (all require Security Reviewer PR)
[x] TASK-0001-030 SecurityHeadersMiddleware
[x] TASK-0001-031 DpopProofValidator (core logic)
[x] TASK-0001-032 DpopValidationMiddleware
[x] TASK-0001-033 JtiReplayCache
[x] TASK-0001-034 Replay check in JWT events
[x] TASK-0001-035 AcrRequirement + Handler
[x] TASK-0001-036 JWT Bearer configuration
[x] TASK-0001-037 Authorization policies
[x] TASK-0001-038 Rate limiting
PHASE 4 — Controllers
[x] TASK-0001-040 ProfileController
[x] TASK-0001-041 ProblemDetails factory
PHASE 5 — Observability
[x] TASK-0001-050 AuthTelemetry
[x] TASK-0001-051 SecurityEventEmitter
[x] TASK-0001-052 OTel pipeline
PHASE 6 — Unit Tests
[x] TASK-0001-060 DpopProofValidator tests (12/12)
[x] TASK-0001-061 JtiReplayCache tests (5/5)
[x] TASK-0001-062 AcrHandler tests (5/5)
[x] TASK-0001-063 SecurityHeaders tests
PHASE 7 — Integration Tests
[x] TASK-0001-070 Test fixtures (Testcontainers)
[x] TASK-0001-071 Security scenarios (14/14)
[x] TASK-0001-072 Full auth flow test
PHASE 8 — Docs
[x] TASK-0001-080 OpenAPI 3.1 spec
[x] TASK-0001-081 Operational runbook
[x] TASK-0001-082 Spec/Plan status updated
CI GATES (all 9 green)
[x] Gate 1 SAST — zero HIGH/CRITICAL
[x] Gate 2 Dependency scan — zero CRITICAL CVEs
[x] Gate 3 Secrets scan — clean
[x] Gate 4 Coverage — ≥ 90% security paths
[x] Gate 5 DAST — zero HIGH
[x] Gate 6 IaC scan — zero HIGH
[x] Gate 7 Container scan — zero HIGH/CRITICAL
[x] Gate 8 SBOM generated + signed
[x] Gate 9 Image signatures verified