Skip to content

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_UNTIL window 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=true while 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_PASSWORD for any non-local install. If you leave it unset, the first boot generates a strong password and writes it (mode 0600) to HELIX_ADMIN_PASSWORD_FILE (default $TMPDIR/helixiam-admin-password, /tmp/helixiam-admin-password in 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-open admin 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 named FACTOR_… 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_URL and SP_BASE_URL so issuer, redirect and metadata URLs are correct and trusted.
IDP_BASE_URL=https://login.example.com
SP_BASE_URL=https://login.example.com

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_PROXIES to the addresses it connects from (for example the ingress controller's pod CIDR). Left empty, the server sees plain HTTP (issuer and redirects become http://) and every client shares the proxy's single sign-in rate-limit bucket. The server logs an ERROR, at most once a minute, when an https IDP_BASE_URL is reached over HTTP.
  • Never list addresses clients can reach the server from directly.
  • The proxy must append to X-Forwarded-For. X-Real-IP is never read. From a trusted proxy, X-Forwarded-For is read from the right; the first address that is not a trusted proxy is the client.
  • Outside the dev profile, any other Host gets 400. A host of IDP_BASE_URL / SP_BASE_URL is accepted only on that URL's port. List in-cluster service names that relying parties call in HELIX_ALLOWED_HOSTS. Probes keep working: the connection's own address and localhost from 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_HOSTS as host:port; internal LDAP directories in HELIX_FEDERATION_LDAP_ALLOWED_HOSTS; extra SMTP ports in HELIX_MESSAGING_SMTP_ALLOWED_PORTS. Don't use the deprecated HELIX_EGRESS_ALLOW_PRIVATE=true.
  • Client JWKS URLs must be https (private_key_jwt); a client stored with an http JWKS URL cannot authenticate outside dev. Check the startup log for ClientAuthMethodAudit warnings.
  • 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:

curl -s -o /dev/null -w '%{http_code}\n' "$HELIX_URL/admin/realms/$REALM/users"
401

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"
400

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.

See also