Security hardening¶
A production checklist to lock down a HelixIAM deployment, plus the high-assurance protocol profiles it supports.
Production checklist¶
Work through this before you serve real traffic.
Secrets & keys¶
DB_ENCRYPTION is immutable after first boot
Keep DB_ENCRYPTION strong, store it securely, back it up, and never change it once the system has booted. Rotating or losing it makes every encrypted column — and every database backup — unrecoverable. See Backup & disaster recovery.
- Adding a key to an install that ran without one? Secret rows written in plaintext then fail to read. Use the time-boxed
HELIX_DB_ENCRYPTION_MIGRATE_PLAINTEXT/HELIX_DB_ENCRYPTION_MIGRATE_UNTILwindow and unset it once the rows are rewritten — see the upgrade guide. - Legacy encryption is retired. Values in the old AES/ECB format are re-encrypted to AES-GCM at every startup and then no longer read. Check the first start's log for a readiness WARN naming rows it could not read; only then consider
HELIX_DB_ENCRYPTION_ALLOW_LEGACY=truewhile you investigate.
Back up the realm keys
Realm signing keys are stored in PostgreSQL, encrypted with DB_ENCRYPTION. Back up the database and keep the DB_ENCRYPTION value safe: losing either loses the keys and invalidates every issued token. See Realm keys. To keep a realm's keys in an HSM or in Vault / OpenBao instead, see Key custody (HSM / KMS).
Accounts & access¶
- Set
HELIX_ADMIN_PASSWORDfor any non-local install. If you leave it unset, the first boot generates a strong password and writes it (mode0600) toHELIX_ADMIN_PASSWORD_FILE(default$TMPDIR/helixiam-admin-password,/tmp/helixiam-admin-passwordin the image); it is never logged. Read it, sign in, and change it. - Change the admin password from any bootstrap default, and rotate it on a schedule.
- Keep the
dev-openadmin bypass OFF — it is off by default and must stay off in production. - Role names starting with
FACTOR_(any case) are reserved for sign-in factors: such a role grants nothing and is refused on create. The query under Rename roles namedFACTOR_…lists existing ones. - Apply least-privilege admin RBAC so each administrator holds only the permissions they need.
Network & endpoints¶
- Use real HTTPS hostnames for
IDP_BASE_URLandSP_BASE_URLso issuer, redirect and metadata URLs are correct and trusted.
Reverse proxies and host names¶
The server trusts forwarded headers only from proxies you name, and answers only for its own host names. Both matter for the OIDC issuer, the Secure-cookie decision, every absolute URL, and the client IP used by rate limits, the audit log, risk signals and CAPTCHA.
| Setting | Helm | Default | What it does |
|---|---|---|---|
HELIX_TRUSTED_PROXIES |
config.trustedProxies |
empty | IPs or CIDR ranges (IPv4/IPv6) whose X-Forwarded-For and Forwarded / X-Forwarded-Proto / -Host / -Port / -Prefix are honoured; from anyone else they are removed |
HELIX_ALLOWED_HOSTS |
config.allowedHosts |
empty | host names answered besides the hosts of IDP_BASE_URL and SP_BASE_URL; name = any port, name:port = that port |
- Behind a TLS-terminating ingress, load balancer, Gateway or mesh, set
HELIX_TRUSTED_PROXIESto the addresses it connects from (for example the ingress controller's pod CIDR). Left empty, the server sees plain HTTP (issuer and redirects becomehttp://) and every client shares the proxy's single sign-in rate-limit bucket. The server logs an ERROR, at most once a minute, when anhttpsIDP_BASE_URLis reached over HTTP. - Never list addresses clients can reach the server from directly.
- The proxy must append to
X-Forwarded-For.X-Real-IPis never read. From a trusted proxy,X-Forwarded-Foris read from the right; the first address that is not a trusted proxy is the client. - Outside the
devprofile, any otherHostgets400. A host ofIDP_BASE_URL/SP_BASE_URLis accepted only on that URL's port. List in-cluster service names that relying parties call inHELIX_ALLOWED_HOSTS. Probes keep working: the connection's own address andlocalhostfrom a loopback peer (on the arriving or default port), and/actuator/health/**on any host. - Only
server.forward-headers-strategy=framework(the default) is supported.
Rate limits¶
Sensitive endpoints are rate limited per client IP and endpoint group. The budgets are counted once for the whole cluster, in PostgreSQL or Redis (see High availability); a single replica with in-memory state counts on its own. Up to and including 1.0.0-rc.12 each replica counted separately, so the effective limit was the configured one times the number of replicas. Excess requests get 429.
| Endpoints | Burst | Refill / minute | Properties |
|---|---|---|---|
sign-in: /login, OTP/flow submit, password reset, registration |
20 | 20 | helix.ratelimit.login.* |
/oauth2/token (all grants) |
120 | 120 | helix.ratelimit.token.* |
EU Digital Identity Wallet: /oid4vp/** |
120 | 120 | helix.ratelimit.wallet.* |
starting a wallet login (GET /broker/<wallet alias>) |
30 | 30 | helix.ratelimit.wallet-start.* |
Each group has .burst and .refill-per-minute. In-cluster callers usually share one egress or pod IP: raise helix.ratelimit.token.* for many backend services on one node, or route them through a trusted proxy that forwards their address. helix.ratelimit.enabled=false turns the limiter off (only behind another rate-limiting layer). The limit is per IP, not per OAuth client.
Outbound calls (SSRF)¶
Admins, realm admins and anyone who can register a client choose URLs the server will call. Every such call — webhooks, SCIM, HTTP email/SMS/push, SMTP relays, LDAP binds, OIDC/eID broker and client JWKS, logout, wallet status lists and CRL/OCSP — passes an egress guard: loopback, private, link-local and cloud-metadata destinations are refused (IPv6 forms that embed an IPv4 address are judged by that address), the host is resolved once and the connection goes to the checked address (no DNS rebinding), redirects are not followed, and TLS is verified against the configured host name.
- Allow only what you need. Internal targets go in
HELIX_EGRESS_ALLOWED_PRIVATE_HOSTSashost:port; internal LDAP directories inHELIX_FEDERATION_LDAP_ALLOWED_HOSTS; extra SMTP ports inHELIX_MESSAGING_SMTP_ALLOWED_PORTS. Don't use the deprecatedHELIX_EGRESS_ALLOW_PRIVATE=true. - Client JWKS URLs must be https (
private_key_jwt); a client stored with anhttpJWKS URL cannot authenticate outsidedev. Check the startup log forClientAuthMethodAuditwarnings. - APNs push goes only to Apple's hosts (
api.push.apple.com,api.sandbox.push.apple.com). - Each feature group has its own concurrency limit and per-realm share (
helix.outbound.fetch.<group>.*), so one realm's slow endpoints cannot hold up another realm's logins.
Data stores and network¶
- Change database and Redis credentials from their defaults, and do not expose PostgreSQL or Redis to untrusted networks.
- Forward the audit log to your SIEM for monitoring and incident response.
Verify the hardening¶
Don't take the checklist on trust — probe the running server. These checks use anonymous endpoints (no cookie, no token); see Authenticating to the API for $HELIX_URL / $REALM.
The admin API rejects anonymous callers¶
An unauthenticated call to any /admin/** route must return 401, never data:
Anything other than 401/403 here means the admin surface is reachable without a session — stop and fix your reverse proxy / network policy before serving traffic.
Discovery advertises the expected profile¶
The realm's discovery document is the authoritative statement of what the issuer supports. Confirm the high-assurance building blocks are present — S256 PKCE, sender-constraint auth methods, mTLS-bound tokens, and a PAR endpoint:
curl -s "$HELIX_URL/realms/$REALM/.well-known/openid-configuration" | jq '{
issuer,
code_challenge_methods_supported,
token_endpoint_auth_methods_supported,
tls_client_certificate_bound_access_tokens,
pushed_authorization_request_endpoint
}'
{
"issuer": "http://localhost:8080/realms/master",
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": [
"client_secret_basic", "client_secret_post", "client_secret_jwt",
"private_key_jwt", "tls_client_auth", "self_signed_tls_client_auth"
],
"tls_client_certificate_bound_access_tokens": true,
"pushed_authorization_request_endpoint": "http://localhost:8080/realms/master/oauth2/par"
}
Check the issuer is your real HTTPS hostname (from IDP_BASE_URL), that code_challenge_methods_supported is ["S256"] (never plain), and that private_key_jwt / tls_client_auth are offered for high-assurance clients. See OpenID / FAPI conformance for the full advertised feature set.
The server answers only for its own host names¶
Outside the dev profile, a request for an unknown Host is refused before any issuer or URL is derived from it:
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: evil.example' \
"$HELIX_URL/realms/$REALM/.well-known/openid-configuration"
A 200 here means the server runs with the dev profile, or the name is in HELIX_ALLOWED_HOSTS. Then confirm the discovery issuer above starts with https://: an http:// issuer behind a TLS proxy means HELIX_TRUSTED_PROXIES is missing.
High-assurance protocol profiles¶
For regulated and high-value clients, HelixIAM supports advanced, sender-constrained and bank-grade OAuth/OIDC profiles:
- FAPI — mutual-TLS-bound tokens, signed request objects, and JWT-secured response mode (JARM).
- DPoP — sender-constrained tokens bound to a client-held key.
- PAR — pushed authorization requests, so request parameters never travel through the browser.
- CIBA — decoupled, backchannel authentication for out-of-band approval flows.
Enable these per client when the relying party requires a high-assurance profile.