Hardening
Startup guards
The server checks its own configuration and refuses to start on settings that would fail quietly in production. Whenever the issuer host is not loopback, it requires:
| Requirement | Why |
|---|---|
https issuer with no path | Discovery, cookies and the DPoP htu all depend on one fixed origin. |
cache.enabled: true | Otherwise codes and refresh tokens live in one replica's memory. |
keys.provider: file | Otherwise signing keys change on every restart. |
mfa.provider: file | Otherwise TOTP seeds become unreadable after a restart. |
Non-empty security.trusted_proxies | Otherwise client IPs and the original scheme cannot be trusted. |
The Helm chart runs the same checks when it renders, and values.schema.json rejects
unknown keys. See Installation.
Credentials
- Hashing. Passwords and client secrets are hashed with argon2id, so a
database dump is not a dump of credentials. Comparisons run in constant time. The
number of concurrent argon2 verifications is bounded
(
auth.brute_force.max_concurrent_verifies). - Randomness. Every identifier, token and secret comes from
crypto/rand. - Secrets returned once. Generated client secrets and the bootstrap admin secret are returned or written once and never logged.
- Brute force. Per-IP and per-identifier throttles, plus a durable per-account soft lock with backoff. TOTP has its own lockout.
FIPS 140-3
Set fips.required: true and the server refuses to start unless the Go FIPS 140-3
module is active. That takes both a binary built with GOFIPS140=v1.0.0 and
GODEBUG=fips140=on at run time. The repository's Dockerfile and Helm chart do not build
or run that way by default, so FIPS deployments need their own build. An organization created
in FIPS mode derives low-entropy secrets with PBKDF2-HMAC-SHA-256 (600,000 iterations)
instead of argon2id.
HTTP
- Hardened server timeouts on every listener (
read_header_timeout,read_timeout,write_timeout,idle_timeout,max_header_bytes) and a request-body cap (max_body_bytes, 256 KiB). - Security headers on every response:
X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,X-Frame-Options: DENY, a Content-Security-Policy (defaultdefault-src 'none'; frame-ancestors 'none'), and HSTS whensecurity.hstsis on. HSTS is sent only on responses that reach the client encrypted, either directly or through a trusted proxy'sX-Forwarded-Proto. Cache-Control: no-storeon OAuth responses.- CORS is off unless you configure it (
security.cors.*). __Host-prefixed session cookies.- Exact
redirect_urimatching. No wildcards, no prefixes. - Client-supplied URLs the server fetches, such as
jwks_uri, must be https and must not resolve to loopback, private, link-local or shared address space. The address is checked again at dial time, and redirects are not followed. - Errors are RFC 7807 problem+json responses with generic detail and a request id. DSNs, driver errors, stack traces and secrets are logged on the server and never returned.
The control plane
The gRPC AdminService can create clients and generate their secrets, so:
- it uses TLS 1.3 with
RequireAndVerifyClientCertand an allowlist of client identities (server.grpc.tls.allowed_client_identities), - plaintext needs an explicit
server.grpc.allow_insecure: true, and - the Helm chart never publishes it on a NodePort or load balancer.
The server reads its gRPC certificate and client CA once at startup. Restart it before a renewed certificate's predecessor expires; reloading without a restart is tracked in #443.
Checklist
- The admin listener (
:9090) and the gRPC listener (:9091) are reachable only from inside the cluster. -
server.admin.pprofis off. - The gRPC API runs with mTLS and an identity allowlist, and
allow_insecureis unset. - Secrets are delivered as files (
_FILE), not as environment variables. -
security.hsts: truebehind TLS, andtrusted_proxieslists only your ingress. - The bootstrap admin secret file has been read, stored and removed.
-
auth.registration.enabledis on only if you want self-registration. - Audit retention and
fail_closedmatch your compliance needs.
Reporting a vulnerability
Report vulnerabilities privately, following the disclosure process in the repository's
docs/SECURITY.md, and not as public issues.