Security Hardening and Defaults
Docs version v1 (main)Nauthera is pre-1.0, in a design-partner programUpdated
Startup guards
The server checks its own configuration and refuses to start on settings that would fail quietly in production. For every issuer, it requires:
| Requirement | Why |
|---|---|
https issuer with no path | Discovery, cookies and the DPoP htu all depend on one fixed origin, and the session cookie is Secure only on an https issuer. |
mfa.provider: file when mfa.required: true | Otherwise TOTP seeds become unreadable after a restart, and with mandatory MFA every user depends on them. |
Whenever the issuer host is not loopback, it also requires:
| Requirement | Why |
|---|---|
cache.enabled: true | Otherwise authorization codes live in one replica's memory, and a code issued by one replica cannot be redeemed at another. |
keys.provider: file | Otherwise signing keys change on every restart. |
mfa.provider: file | Otherwise TOTP seeds become unreadable after a restart. |
On any issuer it also refuses a security.trusted_proxies range that covers every
address, a forwarded security.client_ip.mode without trusted_proxies, a
server. listener on an address that is not loopback, and a Redis
whose maxmemory-policy may evict keys.
The Helm chart runs the same checks when it renders, and values.schema.json rejects
unknown keys. See Installation.
Not everything is checked. An unset keys.encryption_key only logs a warning, and an
empty mounted secret file is not refused: it sets its key to an empty value. See
Errors & Troubleshooting for
what fails quietly.
Credentials
- Hashing. Passwords and MFA recovery codes are hashed with argon2id, or with
PBKDF2-HMAC-SHA-256 under the FIPS policy (see FIPS 140-3). Secrets the
server generates itself, client secrets and registration access tokens, are 256 random
bits, so they are stored as a SHA-256 digest: a slow derivation adds nothing against a
value nobody can enumerate. An older argon2id hash of a client secret still verifies,
and is replaced by the digest the first time it does. A password or recovery code is
never checked against such a digest: a stored SHA-256 value for one is refused as
malformed, even when it matches. Email links for password reset, address verification
and invitations, and refresh tokens, are stored only as a digest too. An identity
provider's client secret is sealed with AES-256-GCM under
keys.encryption_keyand never returned. A database dump is not a dump of credentials, and comparisons run in constant time. - Derivation budget. Every password-grade derivation in the process, sign-in,
setting a password, and redeeming or issuing recovery codes, draws from one budget of
auth.(4) slots, each argon2id derivation using about 64 MiB. A request that gets no slot withinbrute_ force. max_ concurrent_ verifies verify_acquire_timeout(2s) is refused with429instead of queueing, so memory stays bounded under load. - Randomness. Every identifier, token and secret comes from
crypto/rand. - Secrets returned once. Generated client secrets are returned once and never logged.
The admin API returns a new organization's
{slug}-admin-m2msecret once, asadmin_m2m_secret. For the main organization, that secret is the bootstrap admin secret: it is written once toauth., and discarded when that path is empty, which is the default. The gRPCadmin. bootstrap_ secret_ path CreateOrganizationcall returns no secret, so the{slug}-admin-m2mclient of an organization created that way cannot be used. - Brute force. Per-IP and per-identifier throttles, plus a durable per-account soft
lock with backoff: each further failure doubles the next lock, up to
lockout_max. An identifier no account holds locks on the same attempt, so the answers do not reveal whether an account exists. TOTP codes and recovery codes each have their own lock. - Captcha. Registration and every request for an emailed link ask for a captcha, and
sign-in does too once the address typed or the caller's IP failed
auth.(3) times recently. The default provider is the built-in ALTCHA proof-of-work, which needs no third party. See Rate Limits & Lockout.captcha. sign_ in_ after - Account status. An account an administrator suspended or blocked cannot sign in, and cannot obtain or renew a token through any grant. See Rate Limits & Lockout and Multi-Factor Authentication.
- Administrators. No administrator can change the status, credentials, email address
or username of their own account or of one more privileged than theirs, or delete it,
through the admin API; such a call answers
403 protected_account. See Admin API & Users.
The client address
Every per-IP throttle, the rate limiter and the audit log key on the client address, so
behind a reverse proxy the server must know which peer to believe. security.client_ip.mode
picks the source: socket (the connecting peer), x-forwarded-for (walked right to left
through security.trusted_proxies), or header (one of X-Real-IP, True-Client-IP or
CF-Connecting-IP, from a trusted peer). Unset, it is x-forwarded-for when
trusted_proxies is set and socket otherwise.
List your proxies' own addresses: every workload inside a trusted range can name its own
client address. A range wider than security. (/16 IPv4,
/48 IPv6) is logged at startup. When traffic looks like it arrives through a proxy the
configuration does not name, or a trusted range is wider than it needs to be, the
dashboard's overview tells platform administrators which proxies it saw and what to set;
the same finding is served at GET /. A trusted proxy is
also trusted to set X-Request-ID and traceparent. See
Configuration.
FIPS 140-3
Set fips.required: true and the server refuses to start unless the Go FIPS 140-3
module is on and pinned to a frozen version. Build with GOFIPS140=v1.0.0: such a binary
runs in FIPS mode by default, with no GODEBUG setting. An ordinary build run with
GODEBUG=fips140=on is refused, because its module is not pinned, and so is a pinned
build run with GODEBUG=fips140=off. The repository's Dockerfile does not build that
way, so FIPS deployments need their own build.
An organization under the FIPS policy derives new passwords and recovery codes with
PBKDF2-HMAC-SHA-256 (600,000 iterations) instead of argon2id. Hashes written earlier keep
verifying as argon2id and are not rehashed. fips.required: true puts the main
organization under the policy, and every other organization follows only its own
fips_mode, which the admin API's POST /v1/admin/orgs, the gRPC CreateOrganization
call and an Organization manifest's fipsMode set at creation. A process that fails
the check above refuses to create or serve such an organization. See
Organizations.
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: DENYand a Content-Security-Policy. HTML pages set fixed policies of their own thatsecurity.does not change. For example, the sign-in pages usecontent_ security_ policy default-src 'self'withframe-ancestors 'none', and the session management iframe at/check_session_iframesendsframe-ancestors *, so any page can frame it, whether or not session management is enabled. Every response that is not an HTML page carriessecurity.(defaultcontent_ security_ policy default-src 'none'; frame-ancestors 'none'). HSTS is sent whensecurity.hstsis on, and 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. - URLs the server fetches, a client's
jwks_uriand an identity provider's discovery document, token endpoint and keys, 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. Through anHTTPS_PROXY, that dial-time check runs on the dial to the proxy, so a proxy on a private address is refused too. A proxy on a public address resolves the names itself, so its own egress policy has to keep these fetches off internal addresses. - Back-channel logout tokens are sent only to a
backchannel_logout_uriheld to the same rule, with the address checked again at dial time. Through a proxy, the server resolves the relying party's name itself and asks the proxy to connect only to an address it checked, so a relying party on a private address is refused with or without a proxy. The proxy's own address is not checked, so it can be on the private network. - Errors carry no internal detail. The OAuth endpoints answer with the RFC 6749 error
object,
/registerwith the RFC 7591 one, and every other endpoint,/userinfoincluded, with an RFC 9457 problem document. A request refused before its handler runs, such as by the rate limit, gets a problem document on every endpoint, though a browser navigation can get an HTML error page instead. Every response carries anX-Request-IDheader. DSNs, driver errors, stack traces and secrets are logged on the server and never returned. See Errors & Troubleshooting.
The control plane
The gRPC AdminService can create clients and generate their secrets, so:
- it uses TLS 1.3 with
RequireAndVerifyClientCert, and admits only the identities inserver., each matched by a typed certificate SAN (grpc. tls. allowed_ clients uri:<uri>ordns:<name>, never the Common Name), granted only the operations it lists (organizations,clients,brandings,identity_providers,ldap_directories,broker_memberships,email_domains), and bounded to theorganizations(by slug) andnamespacesit lists, where*covers every one. An entry that leaves either empty where one of its operations needs it refuses startup, - plaintext needs an explicit
server., and only on a loopback address,grpc. allow_ insecure: true - with
audit.fail_closedon, an organization change is refused while the audit log cannot record it, and - the Helm chart never publishes it on a NodePort or load balancer.
The server reads its gRPC certificate, key and client CA again at each handshake, so a renewal takes effect without a restart. A replacement it cannot use is logged once, and the last good material stays in use.
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 that grants each identity
only the operations, organizations and namespaces it needs, and
allow_insecureis unset. - Secrets are delivered as files (
_FILE), not as environment variables. -
security.hsts: truebehind TLS, andtrusted_proxieslists only your proxies' own addresses. - Redis runs with
noevictionand persistence, with a replica per primary throughcache.mode: sentinelorcluster. - The bootstrap admin secret file has been read, stored and removed.
-
auth.is on only if you want self-registration.registration. enabled - Audit retention and
fail_closedmatch your compliance needs. - On PostgreSQL, the server runs as an append-only role (
appendOnlyAuditin the chart), andaudit.is set. See Audit Log.checkpoint_ key_ path
Reporting a vulnerability
Report vulnerabilities privately, following the disclosure process in the repository's
docs/SECURITY.md, and not as public issues. The security page has the
address to write to, what is in scope and when to expect an answer.