This guide states the threat model, the token and key design, the sender constraint model, the fail-closed policy, and the admin replacement procedure. The implementation is the authority; the citations point at it.
For a security reviewer and an operator: what the server protects, how, and what
to do when something goes wrong. The standards behind the design are RFC 9700
(BCP 240) and draft-ietf-oauth-v2-1-16 (published 3 September 2026; an
Internet-Draft, not an RFC, at
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1).
trusted_proxy; otherwise they are stripped before any handler
reads them.This section records the container and host attack surface: how an attacker could reach the container or the host through the image, the controls already in place, and the residual risk. It is durable threat-model material, not a backlog; update it whenever the deployment surface changes (a new listener, mount, dependency class, or runtime setting).
| Vector | Existing controls | Residual risk |
|---|---|---|
Application-level RCE in a parser (form bodies, DPoP/JAR/private_key_jwt JWTs, DCR JSON, jwks_uri/request_uri fetches) |
#![forbid(unsafe_code)], no unwrap/panic in request paths, rustls/ring (no OpenSSL), fuzz targets for token-form/DCR-JSON/JAR-claims |
Fuzzing is not exhaustive. |
| Dependency / supply-chain compromise | Committed Cargo.lock, cargo deny/cargo audit in CI, SBOM + provenance + keyless cosign, digest-pinned bases |
A zero-day in a trusted crate. |
| Container escape via kernel or runtime bug | Non-root UID 65532, read-only root filesystem, all capabilities dropped, allowPrivilegeEscalation: false, seccompProfile: RuntimeDefault |
A kernel LPE or runc bug, mitigated but not eliminated. |
| Secret exfiltration from the process environment | Secrets are injected at runtime, not baked into image layers; the pepper/TOTP key live outside the database | An attacker with code execution reads env vars directly (see docs/TODO.md item 7). |
| Exposed management surface | The plaintext health listener (8081) is unauthenticated but not published in the Service (only 8080 is) | A misconfigured Service/Ingress could expose it; it must never be internet-facing. |
| Host access via mounted volumes | /certs is mounted read-only at mode 0400; no Docker socket is mounted |
Operator misconfiguration. |
| Image/registry compromise | Digest pinning and cosign verification in CI | The base manifest still uses a mutable :latest tag that CI rewrites to a digest before rollout (see docs/TODO.md item 8). |
Last reviewed: 2026-10-04. The mitigations to close the residual risks are
tracked in docs/TODO.md.
#![forbid(unsafe_code)] across the crate.unwrap, expect, and panic are denied by lint in request paths./.well-known/… metadata carries
Cache-Control: no-store (+ Pragma: no-cache for legacy clients). An
authorization page, consent page, token body, request handle, or error is
therefore never reused by a browser, the back/forward cache, or a shared
intermediary.GET /login and GET /consent pages (which carry a
session-bound CSRF token and the bound request_id), the /authorize
redirects and error responses, the token endpoint (success and error), PAR,
device authorization, DCR, introspection, and revocation.503 error, which is itself no-store.rotation_request_id derived from the old refresh token hash and the pepper.used and rotated_to, kept for the
refresh lifetime.token.refresh_reuse_detected.See REDIS.md for the barrier modes.
Reference: RFC 9449 (DPoP), RFC 8705 (mTLS).
Every sender-constrained grant requires a freshly verified proof on every use.
| Client class | Required binding |
|---|---|
| Public | DPoP only. A missing proof fails with invalid_grant. |
| Confidential | mTLS and/or DPoP. The confirmation records each proven component. |
cnf propagates to every newly minted token.cnf alone never mints: the proof is re-verified per use.private_key_jwt is client authentication only. It produces no confirmation
and never satisfies sender constraint. This is deliberate.tls_client_auth) or a
certificate thumbprint (self_signed_tls_client_auth), each unique per the
partial indexes in the schema.typ, or has a bad or replayed jti, htm, htu,
iat, or ath.Reference: RFC 9126, RFC 9101.
/authorize must use a PAR request_uri or a signed
request object. This is mandatory, not optional.none and symmetric algorithms are
rejected. iss must equal client_id, aud must be the issuer, and conflicts
with the query string are rejected.request_uri is one-time and client-bound, or exactly pre-registered, and
is fetched only through the SSRF guard (https only, no redirects, global IPs
only, size and time capped).KeyRing holds the token-pepper chain and the TOTP-encryption
chain, newest first.Debug prints key ids only, never key material.jwks_uri. The
keygen command emits keys for test fixtures and client-side DPoP only.The seven security endpoints (authorize, token, introspect, revoke,
par, device_authorization, register) are always fail-closed. An attempt to
configure one as fail-open is a load-time error.
503 temporarily_unavailable with
Retry-After: 1, never a fail-open success.503 while a dependency is down; liveness stays 200
because it touches no dependency.503 rather than an empty list.request_uri is consumed atomically.private_key_jwt jti values are recorded with SET NX, so a
replayed proof or assertion is rejected.S256 only. A verifier cannot be omitted to downgrade.requested ∩ client-registered ∩ user-entitled ∩
enabled, and refresh can only narrow.frame-ancestors 'none'. They use server-side sessions, CSRF protection, and
no open redirect.GET /logout is a server-specific session endpoint (not OAuth 2.1/OIDC
defined). It destroys the session and clears the cookie, then renders a
neutral page or redirects to a post_logout URI. It accepts no query
parameters: the target is read only from session meta, frozen when the
authorization flow was bound, and used only if the owning client is still
active and still registers that exact URI (exact-string match + RFC 8252
loopback). A forced navigation to /logout can therefore only end a session
and, at most, send the browser to that client's own pre-registered URI — never
to an attacker target. There is no post_logout_redirect_uri parameter and no
id_token_hint.No default admin credential ships. migrate seeds zero users and serve never
creates one. The first admin is created by admin bootstrap, which runs inside a
single transaction under an advisory lock, so two concurrent bootstraps cannot
both create an admin. Once any enabled admin exists, a second bootstrap refuses
unless --force is given, which prints a loud warning and emits
admin.bootstrap.force.
The bootstrap admin starts with must_change_password = true and
totp_enabled = false. Its first login yields a restricted session that can only
change the password and enroll TOTP; /authorize, the OAuth endpoints, and
admin-gated actions deny it until both complete.
Replace it immediately:
must_change_password and stamps
password_changed_at.d-oauth-server user create --admin --email <name>@example.com.d-oauth-server user disable --email <bootstrap>@example.com.The step-by-step command runbook lives in RUNBOOK.md. The split is deliberate:
RUNBOOK.md is the task-47 operator procedure, and this section is the task-48
security rationale and checklist.
token.refresh_reuse_detected means a rotated refresh token was replayed.
Treat the grant family as compromised.login.failure for one subject means a credential-stuffing attempt.admin.bootstrap.force means an admin was created despite an existing one.503 on security endpoints means a dependency is down; the server
is failing closed, not open.X-Request-Id header correlates a request to its logs;
audit events never contain a secret.trusted_proxy as narrow as possible.src/crypto/ for token minting, hashing, TOTP, and constant-time compare.src/keys.rs for the key ring.src/protocol/token/types.rs for the sender-constraint matrix.src/jose/dpop.rs, src/jose/jar.rs,
src/protocol/client_auth/assertion.rs for proof verification.src/protocol/mtls.rs for mTLS matching.src/store/mod.rs, src/store/tokens.rs for rotation and tombstones.src/config/policy.rs for the fail-closed policy.src/middleware/ for redaction, rate limiting, and security headers.src/cli/admin.rs for bootstrap.RUNBOOK.md for the operator bootstrap procedure.