Authority DPoP + mTLS Rollout Plan (Sprint 100)
Last updated: 2026-05-30 (reconciled against src/Authority implementation)
This is the rollout plan and reference for sender-constrained tokens in the Stella Ops Authority service: DPoP proof-of-possession and mutual-TLS (mTLS) binding. It is written for Authority and Security Guild engineers driving the rollout, and for operators wiring the config, telemetry, and alerts. The phase checklists below track delivery state; the Verification Matrix, Telemetry, and config tables double as the operational reference once a phase ships.
See also: authority-scopes.md, authority-threat-model.md, and the Authority module docs under docs/modules/authority/.
Objectives
- Enforce DPoP sender constraints (
AUTH-DPOP-11-001). - Bind high-assurance clients/audiences to mTLS tokens (
AUTH-MTLS-11-002). - Provide telemetry + runbooks so plugins (SEC2/SEC3/SEC5) can validate enforcement without regressions.
Implementation note: sender-constraint enforcement is per-client and per-audience, not per-tenant. A client opts into DPoP/mTLS via its
senderConstraintproperty (seeetc/authority.yaml), and DPoP/mTLS can additionally be forced for configured audiences (security.senderConstraints.dpop.nonce.requiredAudiencesandsecurity.senderConstraints.mtls.enforceForAudiences, defaultssigner/attestorandsigner).
Phase 1 · Config & Telemetry
- [x] Extend
etc/authority.yamlwith thesecurity.senderConstraints.dpopsection. Bound options areenabled,allowTemporaryBypass,proofLifetime(default 2m),allowedClockSkew(default 30s),replayWindow(default 5m),allowedAlgorithms(defaultES256,ES384), and a nestednonceblock (enabled,ttldefault 10m,maxIssuancePerMinutedefault 120,store,redisConnectionString,requiredAudiencesdefaultsigner/attestor). Source:StellaOps.Configuration.AuthorityDpopOptions/AuthorityDpopNonceOptions. The .NET binding path isAuthority:Security:SenderConstraints:Dpop. - [x] Emit DPoP audit events through
IAuthEventSink. The event types areauthority.dpop.proof.valid,authority.dpop.proof.challenge,authority.dpop.proof.bypass,authority.dpop.proof.disabled, andauthority.dpop.proof.error(seeValidateDpopProofHandler). Each carriessender.constraint=dpopplus, when available,dpop.jkt(key thumbprint),dpop.jti,dpop.issued_at,dpop.audience,dpop.reason_code, and (sensitive)dpop.nonce.presented/dpop.nonce.issued. There is no singleauthority.dpop.requestlog line; reconcile dashboards to the per-outcome event types above. - [x] Add the
IDpopNonceStoreabstraction. Implementations:InMemoryDpopNonceStore(Testing / local harness),MessagingDpopNonceStore(Valkey-backed, production durable path wired byAddAuthorityDpopRuntime),RedisDpopNonceStore(legacy alias), andDisabledDpopNonceStore(DPoP off). Outside Testing, when DPoP is enabledAddAuthorityDpopRuntimerequiresAuthority:Security:SenderConstraints:Dpop:Nonce:Store=valkey(or theredislegacy alias) and a non-emptyRedisConnectionString, otherwise startup throws. - [x] Integration tests live in
StellaOps.Authority.Tests/OpenIddict/ClientCredentialsAndTokenHandlersTests.cs. Relevant cases includeValidateDpopProof_AllowsSenderConstrainedClient,ValidateDpopProof_RequiresSenderConstraint_WhenAudienceMatchesConfiguration,ValidateDpopProof_IssuesNonceChallenge_WhenNonceMissing, andValidateDpopProof_AllowsBypass_WhenEnabled. (The previously-citedAuthorityTokenTests.DPoPNonceRequired/AuthorityTokenTests.DPoPMustMatchCnFclasses do not exist.)
Phase 2 · Enforcement & Fallback
- [x] Reject
/tokenrequests lacking a DPoP proof when the client’ssenderConstraintisdpopor the requested audience matchesnonce.requiredAudiences.ValidateDpopProofHandlerrejects with OpenIddict errorinvalid_clientand emits aWWW-Authenticate: DPoP error="invalid_dpop_proof"(orerror="use_dpop_nonce"for a nonce challenge) header; nonce challenges also set theDPoP-Nonceresponse header. - [x] Persist
cnf.jkt(DPoP) /cnf["x5t#S256"](mTLS) on issuance and re-attach thecnfclaim during token and introspection validation so downstream services validate the sender. The DPoP key thumbprint is stored inAuthorityTokenDocument.SenderKeyThumbprint; thecnfclaim is applied byAuthoritySenderConstraintHelper/EnsureSenderConstraintClaims(TokenValidationHandlers). - [x] Add emergency bypass flag (
security.senderConstraints.dpop.allowTemporaryBypass) for sealed recap drills; default disabled.AddAuthorityDpopRuntimerestricts this flag to Development/Testing local harnesses — enabling it in any other environment throws at startup.- When enabled, Authority emits the
authority.dpop.proof.bypassaudit event (outcomeSuccess,dpop.reason_code=bypass), sets theauthority.dpop_result=bypassandauthority.sender_constraint=dpop_bypassrequest tags, and issues tokens without a sender constraint (theSenderConstraintPropertyis left unset, so nocnfis attached) so downstream services know sender constraints were relaxed. Reset immediately after the drill.
- When enabled, Authority emits the
Phase 3 · mTLS Binding
- [x] Validate the presented client certificate on
/token(client_credentials and refresh grants) when the client’ssenderConstraintismtlsor the audience matchesmtls.enforceForAudiences, then store the SHA-256 hex thumbprint inAuthorityTokenDocument.SenderCertificateHex(persisted as thesender_cert_hextoken property inPostgresTokenStore). The certificate is checked against the client’s registeredCertificateBindings(thumbprint, subject, serial, issuer, SANs, validity window withrotationGrace) plus optional CA allow-list, subject patterns, and SAN-type rules (AuthorityClientCertificateValidator). - [x] Re-validate the certificate hash on the
/tokenand/introspectendpoints:EnsureMtlsBinding(TokenValidationHandlers) compares the presented cert hash againstSenderCertificateHexand rejects withinvalid_tokenon mismatch, incrementingauthority_mtls_mismatch_total. - [ ] Document bootstrap/rotation in
docs/modules/authority/AUTHORITY.md+docs/security/dpop-mtls-rollout.md(this file).
NOT VERIFIED IN CODE: this plan previously claimed cert-hash validation on a
/fresh-authendpoint. Authority’sMtlsValidationHandleronly runs for the Token and Introspection endpoints (OpenIddictServerEndpointType.Token/IntrospectioninTokenValidationHandlers); there is no Authority/fresh-authtoken endpoint that performs sender-certificate validation. “Fresh auth” in Authority is a Console concept (thestellaops:fresh_authclaim + a 300s window enforced byStellaOpsScopeAuthorizationHandlerforobs:incidentand pack-approval scopes), unrelated to mTLS binding. Drop the/fresh-authreference or confirm a planned endpoint before re-asserting it.
Verification Matrix
| Scenario | Test/Command | Expected |
|---|---|---|
| DPoP required, proof missing | ValidateDpopProof_IssuesNonceChallenge_WhenNonceMissing (in ClientCredentialsAndTokenHandlersTests) | Rejected with invalid_client; WWW-Authenticate: DPoP error="use_dpop_nonce" and a DPoP-Nonce header; authority_dpop_nonce_miss_total increments. |
| Nonce missing/expired/invalid | ValidateDpopProof_RequiresSenderConstraint_WhenAudienceMatchesConfiguration + nonce-consume paths | Nonce challenge reissued (nonce_missing / nonce_expired / nonce_invalid reason codes); audit event authority.dpop.proof.challenge. |
| mTLS cert missing | ValidateAccessTokenHandler_Rejects_WhenMtlsCertificateMissing | invalid_token; authority_mtls_mismatch_total{reason="missing_certificate"} increment. |
| mTLS cert mismatch | ValidateAccessTokenHandler_Rejects_WhenMtlsCertificateMismatch | invalid_token; authority_mtls_mismatch_total{reason="thumbprint_mismatch"} increment. |
| Bypass enabled | ValidateDpopProof_AllowsBypass_WhenEnabled | Not rejected; no cnf; authority.dpop.proof.bypass audit event. |
The
--filter-style invocation above does not run with this repo’s test host; MTP ignores--filter. Run targeted cases via the test.exerunner againstStellaOps.Authority.Testsrather thandotnet test --filter.
Telemetry & Alerting
- Metrics:
authority_dpop_nonce_miss_totalandauthority_mtls_mismatch_total. Both carry areasontag. The mTLS mismatch reasons emitted byRecordMtlsMismatcharecontext_missing,missing_certificate, andthumbprint_mismatch(underscores, not hyphens). The DPoP nonce-miss reasons includemissing_proof,invalid_proof,invalid_key,nonce_missing,nonce_expired, andnonce_invalid. - Audit events (via
IAuthEventSink, not a single log line):authority.dpop.proof.valid/.challenge/.bypass/.disabled/.error. Per-request OpenTelemetry tags includeauthority.sender_constraintandauthority.dpop_result. (The previously-citedauthority.security.senderConstraintstructured log does not exist.) - Alerts: Page DevOps when nonce miss > 5% or mTLS mismatches > 0 over 10 min.
Dependencies
- Authority Core & Security Guild owners.
- DevOps to provide sealed-mode CI coverage (
DEVOPS-AIRGAP-57-002). - Plugin Standard Guild to consume new telemetry once rolled out.
Communication
- Daily async update in
#guild-authoritythread referencing this plan. - Link this document from
docs-archive/implplan/SPRINT_0100_0001_0001_identity_signing.mdnotes once Phase 1 merges.
