Configuring nauthera-server
Docs version v1 (main)Nauthera is pre-1.0, in a design-partner programUpdated
The server loads its configuration in this order of precedence:
defaults < YAML file < environment < secret files. Point it at a file with
--config (or NAUTHERA_SERVER_CONFIG). Environment variables win over the YAML file,
and a secret file named by a _FILE variable wins over both. See
Secrets from files.
nauthera-server --config /etc/nauthera/config.yamlThe annotated reference is config.example.yaml in the nauthera-server repository.
Environment variables
Every key has the prefix NAUTHERA_SERVER_. To get the variable name, upper-case the
dotted key and replace . and - with _:
server.http.read_header_timeout -> NAUTHERA_SERVER_SERVER_HTTP_READ_HEADER_TIMEOUT
database.dsn -> NAUTHERA_SERVER_DATABASE_DSN
tracing.sample_ratio -> NAUTHERA_SERVER_TRACING_SAMPLE_RATIODurations are Go duration strings ("15s", "30m"). Lists are comma-separated.
Prefer the file. The file is decoded strictly, so a mistyped key is a startup error that names it. A mistyped environment variable is silently ignored, and you run on a default you believe you overrode. The Helm chart renders
configto a file for this reason.
Secrets from files
Keys that hold secrets also accept a mounted file. Append _FILE to the variable name:
NAUTHERA_SERVER_DATABASE_DSN_FILE=/var/run/secrets/db/uri
NAUTHERA_SERVER_DATABASE_PASSWORD_FILE=/var/run/secrets/db/password
NAUTHERA_SERVER_CACHE_PASSWORD_FILE=/var/run/secrets/redis/password
NAUTHERA_SERVER_CACHE_SENTINEL_PASSWORD_FILE=/var/run/secrets/redis/sentinel-password
NAUTHERA_SERVER_MFA_ENCRYPTION_KEY_FILE=/var/run/secrets/mfa/key
NAUTHERA_SERVER_KEYS_ENCRYPTION_KEY_FILE=/var/run/secrets/keys/kek
NAUTHERA_SERVER_MAIL_SMTP_PASSWORD_FILE=/var/run/secrets/smtp/password
NAUTHERA_SERVER_AUTH_CAPTCHA_SECRET_FILE=/var/run/secrets/captcha/secretA file's contents, with surrounding whitespace trimmed, outrank both the YAML file and the plain environment variable. An empty file therefore blanks a value set in either place. A file that cannot be read stops the server at startup.
The retired-key lists (mfa.retired_keys, keys.retired_keys) have no _FILE form, so
pass them through the environment. audit. is a path, not a
secret value, so it has no _FILE form either.
Top-level sections
| Section | What it configures |
|---|---|
server | The three listeners: server.http (:8080, timeouts, body cap, TLS), server.admin (:9090, pprof), server.grpc (:9091, mTLS with tls.client_ca_file and tls.allowed_clients, both certificate and CA read at each handshake, allow_insecure and reflection), plus shutdown_timeout and drain_delay. |
logging | level (debug, info, warn or error), format (json or console), development. |
database | PostgreSQL, the only database: driver (postgres, the default and only accepted value), a dsn or discrete host, port, user, password, name, sslmode (verify-full by default, which checks the database's certificate and name; any libpq mode is accepted) and sslrootcert (the CA file a database with a private CA needs), pgbouncer, pool sizing, the session bounds statement_timeout (30s), lock_timeout (10s) and idle_ (1m), migrate, and runtime_role, the role the server serves requests as when it does not own the schema. See Backups & Upgrades. |
cache | A Redis-compatible cache: enabled, mode (standalone at address, or sentinel and cluster at addresses, with master_name for Sentinel), username, password, sentinel_password, replica_acks and replica_ack_timeout (replicas that must confirm each write), db, tls and tls_ca_file (the CA the Redis certificate is verified against, read only with tls), timeouts and pool size. Required for any issuer that is not loopback. The server refuses to start against a Redis whose maxmemory-policy may evict keys. |
metrics | enabled, path (default /metrics on the admin listener). |
rate_limit | Per-IP limits: enabled, rps, burst, backend (memory, a token bucket per replica, or redis, a fixed window shared by every replica) and window. While Redis cannot answer, the redis backend limits each replica on its own rps and burst bucket, and nauthera_ reads 1. See Rate Limits & Lockout. |
security | trusted_proxies, trusted_, client_ip.mode and client_ip.header, network_zones, hsts, hsts_max_age, frame_deny, content_security_policy, sign_in_opener_policy (same-origin by default; unsafe-none for a relying party that signs in through a popup and reads window.opener), cors.*. See The client address and Network zones. |
issuer | url and the token lifetimes: access_token_ttl (10m), id_token_ttl (10m), refresh_token_ttl (720h), auth_code_ttl (60s). |
keys | Signing keys: provider (memory for development, or file), signing_key_path, rsa_signing_key_path, verification_key_paths and rsa_ for extra public keys to publish during a rotation, default_alg, plus the per-organization key-encryption key encryption_key and its retired_keys. See Keys & Secrets. |
tracing | OpenTelemetry over OTLP/gRPC: enabled, endpoint, insecure, sample_ratio, environment. |
auth | brute_force.* (lockout, per-IP and per-identifier throttles, and max_concurrent_verifies, the process-wide limit on password hashing), session.* (cookie, idle and absolute TTL, history_retention), pending.*, session_, registration.enabled, password.* (a new password's min_length, 8, and max_length, 128, at most 1024; refuse_common; and breach_lookup.*, an opt-in Pwned Passwords range check that sends only the first five characters of the password's SHA-1: enabled, url, timeout, fail_open), captcha.* (below), password_reset.*, email_verification.* and invitation.* (link validity and max_requests), and admin.* (subjects, bootstrap_secret_path, max_auth_age). See Sessions, Consent & Logout. |
mfa | The deployment's second-factor policy (required, methods, passwordless, remember_browser_for, spare_zones, rules; below), the one-time-password seed cipher (provider, memory or file, encryption_key, retired_keys), issuer (the label shown in authenticator apps), drift, max_failures, lock_duration, recovery_code_count, and passkey.* (below). See Multi-Factor Authentication. |
orgs | default_slug, the main organization's slug (default), and main.*, settings applied to the main organization at every startup (below). |
audit | enabled, level, include and exclude globs, fail_closed, buffering, refusal coalescing, retention (9600h), partition_ahead_months, partition_maintenance (internal, or external when nauthera-server maintain runs as its own job), signed chain checkpoints (checkpoint_interval, checkpoint_key_path, and checkpoint_key_required, which refuses to start without a key), and export.* for forwarding the stored log. See Audit Log. |
fapi | require_, which requires PAR deployment-wide. |
fips | required, which puts the main organization under the FIPS policy and refuses to start unless the Go FIPS 140-3 module is on and pinned to a frozen version. A binary built with GOFIPS140=v1.0.0 passes with no GODEBUG setting; an ordinary build run with GODEBUG=fips140=on is refused. See Hardening. |
mail | The deployment's outbound relay: smtp.host, smtp.port (587), smtp.tls (starttls, implicit, or none for a relay without TLS), smtp., smtp.username, smtp.password, from.address, from.name, reply_to, local_name, timeout; and organizations, per-organization relays by slug, whose credentials are read from username_file and password_file at every delivery. |
geoip | database, an optional GeoLite2-City or GeoIP2-City file that lets a new-sign-in alert name where the sign-in came from. |
The client address
Per-IP throttles, the rate limiter and the audit log key on the client address.
security.client_ip.mode says where it comes from:
| Mode | Address |
|---|---|
socket | The connecting peer. |
x-forwarded-for | X-Forwarded-For from a peer in trusted_proxies, walked right to left. |
header | The one header client_ip.header names (X-Real-IP, True-Client-IP or CF-Connecting-IP), from a peer in trusted_proxies. |
Unset, the mode is x-forwarded-for when trusted_proxies is set and socket
otherwise; any mode but socket needs trusted_proxies. Every workload inside a trusted
range can name its own client address, so list your proxies' own addresses. A range
covering every address (0.0.0.0/0, ::/0) is refused at startup, and one wider than
trusted_ (/16 for IPv4, /48 for IPv6) is reported with a warning.
Behind a proxy, socket makes every client share one bucket; the dashboard's overview
then tells platform administrators which proxies it saw and what to set.
Network zones
security.network_zones names networks a sign-in policy can refer to: a lowercase name
(letters, digits, hyphens) mapped to IPv4 and IPv6 ranges in CIDR notation. It is a map,
so it can be set only in the file:
security:
network_zones:
office: ["203.0.113.0/24", "2001:db8:1::/48"]
vpn: ["198.51.100.0/24"]A sign-in is in a zone when its client address, as client_ip reads it, falls in one of
the ranges. Every sign-in's audit record, completed or refused, names its zones
(network_zones). An organization declares its own zones beside these and may not reuse
a name. A range that covers every address of its family, or a name that is not a
lowercase label, refuses startup.
The second-factor policy
The first six mfa keys are the deployment's second-factor policy. It is a floor: each
organization, and then each client, may tighten it and never loosen it. An organization
or client sets its own through the admin API, the control plane, or its
Organization or OIDCClient
resource.
| Key | Default | Meaning |
|---|---|---|
mfa.required | false | A user with no permitted factor is sent through enrolment before a session is issued, at sign-in and at self-service registration. |
mfa.methods | [totp, passkey] | The second-factor methods offered at all: totp, passkey and email (an emailed code). Empty means totp and passkey; email is offered only when listed. |
mfa.passwordless | false | Opens sign-in with a passkey alone, for every organization and client that also opens it. Refused at startup when mfa.methods lists methods and passkey is not one. |
mfa.remember_browser_for | 0s | A user who holds a second factor is not asked for it again on a browser that passed one for them within this window, while their password and factors are unchanged. 0 remembers no browser; at most 4320h. Organizations and clients may shorten it or always challenge. |
mfa.spare_zones | [] | The security.network_zones in which a user who holds a second factor is not asked for it. Only these deployment zones can relax the challenge. An undeclared name refuses startup. |
mfa.rules | [] | Rules that narrow the policy for the sign-ins that meet them (file only). |
A sign-in spared its second factor, by a remembered browser or a zone, is single-factor:
its session and ID token carry the first factor's amr and no mfa, and its audit record
says mfa_ or mfa_spared: network_zone. An identity provider
with spareSecondFactor spares the sign-ins through it outside this policy
(mfa_; see
IdentityProvider). An organization's
admin console always asks for the second factor, because its step-up needs a sign-in
that used it.
Each rule has the fields name, outside_zones, inside_zones, new_network,
new_browser, methods and require. It holds when every condition it sets does: the client address is in none of
outside_zones or in one of inside_zones, the network is one the user has never
completed a sign-in from, or the browser has not passed a second factor for the user. It
then narrows the methods to methods, requires a second factor (require, which no
exemption spares), or both. A rule cannot loosen anything.
mfa:
spare_zones: [office]
rules:
- name: passkey-off-site
outside_zones: [office, vpn]
methods: [passkey]
require: trueThe audit record of a completed or interrupted sign-in names the rules that held
(mfa_rules). A rule with no name, a repeated name, no condition, no effect, an unknown
method or an undeclared zone refuses startup.
Passkeys
| Key | Default | Meaning |
|---|---|---|
mfa.passkey.rp_id | "" | The relying-party ID passkeys are scoped to: the issuer's host, or a registrable domain it is under. Empty serves no passkey ceremony. A public suffix, a domain the issuer is not under, or an IP-address issuer is refused at startup. |
mfa.passkey.rp_name | Nauthera | The name an authenticator shows when it creates a credential. |
mfa.passkey.origins | [] | The origins (scheme://host[:port]) a ceremony may run on, each under rp_id. Empty means the issuer's origin alone. |
mfa. | preferred | required, preferred or discouraged. Under required, a ceremony whose authenticator did not verify the user is refused. |
mfa.passkey.timeout | 2m | How long a ceremony and its challenge last, 30s to 10m. |
mfa. | 720h | Where passwordless sign-in is open, a user who signs in with a password and holds no passkey is offered one. Declining is remembered this long, at least 24h. |
See Passkeys.
Captcha
Unless auth.captcha.provider is none, registration, password-reset requests and
verification-link resends need a captcha solution, and so does a sign-in once the address
typed, or the caller's IP, has failed sign_in_after times recently. A missing, wrong,
expired or reused solution is refused with 400 captcha_failed.
| Key | Default | Meaning |
|---|---|---|
auth.captcha.provider | altcha | altcha is the built-in proof-of-work; turnstile (Cloudflare Turnstile), hcaptcha and recaptcha (Google reCAPTCHA v2) are hosted providers; none asks for no captcha. |
auth. | 1000 | The largest counter a challenge hides, from 1 to 100000. Solving costs the browser max_number / 2 PBKDF2 derivations on average, and checking costs the server one; the default takes a desktop browser well under a second. |
auth. | 5m | How long a challenge can be solved and sent back, from 1m to 1h. |
auth.captcha.site_key | "" | A hosted provider's public site key. Required by a hosted provider. |
auth.captcha.secret | "" | A hosted provider's secret key, also from _FILE. Required by a hosted provider. In the Helm chart, secrets.captchaSecret. |
auth. | 3 | Recent failed sign-ins, by the address typed or from the caller's IP, after which sign-in asks for the captcha too. 0 never asks at sign-in. |
altcha calls no third party, so it works air-gapped, and uses only FIPS-approved
primitives. Its challenges are signed with a key derived from mfa.encryption_key, so
every replica accepts the others', and each is spent once in the shared cache. A hosted
provider must be reachable from the server (through HTTPS_PROXY when set). While it
cannot be reached, every request that needs the captcha is refused with
503 unavailable, so pick altcha where availability matters more than the provider's
bot scoring.
Invitations
An administrator can create a user with invite, which creates the account without a
password and emails a single-use link to /accept-invitation, where the user chooses a
password. The organization needs an email relay. auth.invitation.validity (168h, from
1m to 30 days) is how long a link works, and auth. (5) how
many links one account is sent within one validity window, by an administrator or on the
user's own request; an administrator's resend beyond it is refused with 429. A user who
waits on an invitation and signs in, signs up, or asks for a reset or verification link is
emailed a new invitation instead (see
Emailed links). See
Admin API & Users.
The main organization
The main organization (slug default) is created by a migration. Each orgs.main
setting given is applied to it at every startup, before it is served, and recorded as an
organization.updated by the system. The admin API refuses to change a setting given
here (409, naming it); one left out stays editable. The control plane never changes the
main organization. In the Helm chart these are config.orgs.main.*.
| Key | Meaning |
|---|---|
orgs.main.display_name | The name the hosted UI shows, 1 to 200 characters. |
orgs.main.fapi_mode | Holds the main organization's clients to the FAPI 2.0 Security Profile. It is read only at startup, so this key is the way to change it. Without an ES256 key the server refuses to start in FAPI mode. |
orgs. | Keeps an account with an unverified address out of a session until it follows an emailed link. |
orgs.main.branding | The main organization's default branding document, as text (YAML or JSON), because the loader lowercases a nested object's keys. Every sign-in page under it wears it, with a client's own branding laid over it field by field. An unsafe document stops startup; an empty one clears it. |
None of them has a default: left out, the setting is whatever the admin API last set.
External identity providers for social sign-in have no configuration keys. Each
organization declares its own through an
IdentityProvider resource or nauthera-cli create identity-provider, and the admin API and the dashboard list them.
Production guards
Every issuer must be an https URL with a host and no path, query or fragment. There
is no exception for local development: the session cookie's Secure attribute follows
the issuer's scheme, so a plaintext issuer would run in a configuration production never
uses. A plaintext listener behind a TLS-terminating proxy is fine, because the issuer
names what clients reach.
When the issuer host is not loopback, the server also refuses to start unless:
cache.enabledistrue(otherwise authorization codes, pushed requests and other single-use state would live in one replica's memory), andkeys.providerisfileandmfa.providerisfile(otherwise keys would be regenerated on every restart).
localhost and names under .localhost count as loopback, so local development gets a
real HTTPS issuer without having to run Redis.
On any issuer, loopback included, mfa.provider: memory is also refused when
mfa.required is true.
Three mistakes are not caught at startup:
- A
--configpath that does not exist is ignored, and the server runs on defaults and environment variables. - Without
keys.encryption_key, each replica only logs a warning and makes up its own key in memory. An organization created meanwhile works only on the replica that created it, until that replica restarts. See Keys & Secrets. - An empty secret file is not refused. It blanks its key, and for
keys.encryption_keythat means the in-memory key above. See Secrets from files.
Example: minimal production file
issuer:
url: https://auth.example.com
database:
driver: postgres
host: postgres.example.svc
user: nauthera
name: nauthera
sslmode: verify-full
migrate: true
cache:
enabled: true
address: redis.example.svc:6379
tls: true
keys:
provider: file
signing_key_path: /etc/nauthera/keys/ec/tls.key
rsa_signing_key_path: /etc/nauthera/keys/rsa/tls.key
mfa:
provider: file
security:
trusted_proxies: ["10.244.1.7/32"]
client_ip:
mode: x-forwarded-for
hsts: true
server:
grpc:
tls:
enabled: true
cert_file: /etc/nauthera/grpc/tls.crt
key_file: /etc/nauthera/grpc/tls.key
client_ca_file: /etc/nauthera/grpc/ca.crt
allowed_clients:
- identity: "dns:nauthera-operator"
operations: [clients, brandings, identity_providers, ldap_directories]
organizations: [default]
namespaces: ["*"]
audit:
checkpoint_key_path: /etc/nauthera/audit/checkpoint.pem
checkpoint_key_required: trueSupply the database password, the MFA encryption key and the key-encryption key through
the _FILE variables above. Each allowed_clients entry must name the organizations
and namespaces it may act in, or the server refuses to start.