Skip to content

Install

HelixIAM ships as two images — helix-iam-server (OAuth2/OIDC/SAML + admin API, owns PostgreSQL) and the console — plus PostgreSQL and, optionally, Redis.

Requires a HelixIAM licence

The images, the Helm chart, the compose file and the server jar are delivered to licensed customers: you receive registry credentials and the release files from KubeDNA B.V. For a licence, contact [email protected]. Sign in to the registry first (docker login ghcr.io, and helm registry login ghcr.io for the chart).

Docker Compose (evaluation)

The compose file provided with your licence (pinned to v1.1.0-rc.14) runs the signed release images (server, console, PostgreSQL and Mailpit; sessions in PostgreSQL, no Redis). In the directory with the file:

docker login ghcr.io
docker compose up -d
  • Server / IdP: http://localhost:8080 (e.g. /realms/master/.well-known/openid-configuration)
  • Admin console: http://localhost:8090 (demo login admin / admin)
  • Mailpit (outbound email): http://localhost:8025

HELIX_ADMIN_PASSWORD, DB_ENCRYPTION and HELIX_VERSION can be set in a .env file next to it, before the first start: see the Quickstart.

Demo configuration only

The compose stack uses a demo encryption key unless you set one, plain http and cookies without the Secure flag. Do not reuse it anywhere real.

Kubernetes (Helm)

The chart is published as an OCI artifact, oci://ghcr.io/melmarzgioui/helixiam/charts/helixiam (Helm 3.8 or newer), with the same version as the release and signed with cosign like the images (see the Quickstart for cosign verify). It deploys helix-iam-server and, with console.enabled=true, the admin console. PostgreSQL and Redis are not bundled — point the chart at your own instances (Redis is optional, see below). Secrets are referenced from a Secret you create, never inlined. The images need a pull secret with your registry credentials (kubectl create secret docker-registry helixiam-registry --docker-server=ghcr.io …), passed as imagePullSecrets.

# 1. Create the referenced Secret (key names are configurable in values.yaml).
#    Set DB_PASSWORD to the password of the "helix" user on your PostgreSQL (the value below is an example).
#    db-encryption and admin-password are generated: keep db-encryption stable for the life of the install.
DB_PASSWORD='example-change-me'
kubectl create secret generic helixiam-secrets \
  --from-literal=db-username=helix \
  --from-literal=db-password="$DB_PASSWORD" \
  --from-literal=db-encryption="$(openssl rand -hex 16)" \
  --from-literal=admin-password="$(openssl rand -base64 18)"

# 2. Install
helm install helixiam oci://ghcr.io/melmarzgioui/helixiam/charts/helixiam --version 1.1.0-rc.14 \
  --set config.idpBaseUrl=https://idp.example.com \
  --set config.spBaseUrl=https://console.example.com \
  --set database.host=postgres.db.svc \
  --set redis.host=redis.redis.svc \
  --set secrets.existingSecret=helixiam-secrets \
  --set 'imagePullSecrets[0].name=helixiam-registry'

Required values: config.idpBaseUrl, config.spBaseUrl, database.host, secrets.existingSecret, and redis.host when config.sessionStore or config.tokenStore is redis (the default session store is redis). Rendering fails fast when one is missing.

Images. By default the chart pulls ghcr.io/melmarzgioui/helixiam/helix-iam-server with the tag v<appVersion> (the release tag format). Before the release after v1.1.0-rc.1 the defaults pointed at an image that did not exist, so a default install could not pull; installs that set image.repository and image.tag are unaffected. See Upgrading from 1.1.0-rc.1 to 1.1.0-rc.2.

The admin console as its own Deployment

The admin console (ghcr.io/melmarzgioui/helixiam/helix-dashboard, same tag as the server) is a separate image and, with console.enabled=true, a separate Deployment: its own console.replicaCount (default 1), console.resources, scheduling, console.autoscaling (HPA), console.podDisruptionBudget, console.service, console.ingress on its own host and console.networkPolicy. Its pods carry app.kubernetes.io/name: helixiam-console, so the server's selectors, PodDisruptionBudget and replicaCount never touch them. The section is off by default, so an existing install is unchanged. See Architecture.

helm upgrade --install helixiam oci://ghcr.io/melmarzgioui/helixiam/charts/helixiam --version 1.1.0-rc.14 --reset-then-reuse-values \
  --set console.enabled=true --set console.replicaCount=2 \
  --set console.ingress.enabled=true --set 'console.ingress.hosts[0].host=beheer.example.nl' \
  --set 'console.ingress.hosts[0].paths[0].path=/' --set 'console.ingress.hosts[0].paths[0].pathType=Prefix' \
  --set 'console.ingress.tls[0].secretName=helixiam-console-tls' --set 'console.ingress.tls[0].hosts[0]=beheer.example.nl' \
  --set 'console.networkPolicy.ingressNamespaceSelectors[0].matchLabels.kubernetes\.io/metadata\.name=ingress-nginx'
  • The console's URL. Its origin is config.spBaseUrl. Leave it empty and the chart sets it to https://<console.ingress.hosts[0].host>; if you set it, make it that same host. With the console enabled the chart passes it as HELIX_CONSOLE_BASE_URL, the origin of the built-in helix-console client (redirect URI <spBaseUrl>/console/callback).
  • Trusted proxies. Behind a TLS-terminating ingress, config.trustedProxies must also cover the console pods (usually the cluster pod CIDR): the console forwards the ingress's X-Forwarded-Proto/-Host/-Port, and the server honours them only from trusted proxies.
  • Keep it internal. Use an internal IngressClass, an IP allow-list annotation or a VPN-only load balancer. The console's ingress must overwrite client-sent X-Forwarded-* headers, and the console Service must never be exposed directly (no LoadBalancer or NodePort). The end-user account console stays on the server's own host.

Admin API only on the admin host

The server Ingress still routes every path of its host to the server. Set config.adminAllowedHosts (HELIX_ADMIN_ALLOWED_HOSTS) to the console host, plus any in-cluster name automation calls the admin API on:

  • /admin/**, /v3/api-docs/**, /swagger-ui/** and /swagger-ui.html then answer 404 on every other host;
  • so does the master realm (/realms/master/**: administrator sign-in, the provisioning service account's tokens) unless config.adminRestrictMasterRealm (HELIX_ADMIN_RESTRICT_MASTER_REALM) is false;
  • other realms are unaffected; the admin hosts are allowed hosts too;
  • the host checked is Host, or X-Forwarded-Host from a config.trustedProxies peer only.

Empty (the default) keeps the admin API on every allowed host. Once it is set, admin tools, Terraform, CI jobs and scripts must call the admin host: see the upgrade note.

# values.yaml
config:
  spBaseUrl: https://beheer.example.nl
  adminAllowedHosts:
    - beheer.example.nl
  adminRestrictMasterRealm: true

Behind an ingress: trusted proxies and host names

The server honours forwarded headers only from trusted proxies, and answers only for its own host names. Behind the chart's ingress, an external load balancer or a Gateway, set both:

helm upgrade helixiam oci://ghcr.io/melmarzgioui/helixiam/charts/helixiam --version 1.1.0-rc.14 --reset-then-reuse-values \
  --set 'config.trustedProxies={10.42.0.0/16}' \
  --set 'config.allowedHosts={helixiam.identity.svc}'
Value Env Set it to
config.trustedProxies HELIX_TRUSTED_PROXIES the addresses the ingress controller / load balancer connects to the pods from (e.g. the controller's pod CIDR)
config.allowedHosts HELIX_ALLOWED_HOSTS in-cluster names relying parties call, e.g. helixiam.<namespace>.svc (name = any port, name:port = that port)

Left empty, a TLS-terminating ingress yields http:// issuers and redirects, every request appears to come from the controller (one shared sign-in rate-limit bucket), and any Host other than those of config.idpBaseUrl / config.spBaseUrl gets 400. The install notes print a warning whenever config.trustedProxies is empty. Never list ranges that clients can reach the pods from directly. See Security hardening.

Internal mail relays, directories and HTTP gateways

Outbound calls to private or loopback addresses are refused unless the deployment allows them:

helm upgrade helixiam oci://ghcr.io/melmarzgioui/helixiam/charts/helixiam --version 1.1.0-rc.14 --reset-then-reuse-values \
  --set 'config.egressAllowedPrivateHosts={mailer.mail.svc.cluster.local:587}' \
  --set 'config.ldapAllowedHosts={ldap.corp,10.20.0.0/16}' \
  --set 'networkPolicy.extraEgressPorts[0].port=587' \
  --set 'networkPolicy.extraEgressPorts[1].port=636'
Value Env Purpose
config.egressAllowedPrivateHosts HELIX_EGRESS_ALLOWED_PRIVATE_HOSTS private hosts webhooks, SCIM, HTTP/SMTP email, SMS and push may reach; host or host:port
config.ldapAllowedHosts HELIX_FEDERATION_LDAP_ALLOWED_HOSTS internal LDAP directories: host names or CIDR ranges
networkPolicy.extraEgressPorts — the matching ports in the chart's NetworkPolicy

An SMTP relay on a port other than 25, 465, 587 or 2525 also needs HELIX_MESSAGING_SMTP_ALLOWED_PORTS, set through config.extraEnv.

Value Default Purpose
replicaCount 2 server replicas; sessions, tokens, flow state, rate limits, identity-provider changes and wallet transactions are shared through PostgreSQL / Redis (see High availability)
strategy {} = Kubernetes' RollingUpdate, 25% / 25% the Deployment's spec.strategy, always rendered in full; strategy.type=Recreate for an upgrade whose old and new pods must not overlap (the upgrade to rc.9)
enableServiceLinks false Kubernetes service-link variables are off, so a Service named redis cannot inject REDIS_PORT=tcp://…

Upgrading from 1.0.0-rc.8 or earlier to 1.0.0-rc.9

1.0.0-rc.9 moves to Spring Boot 4: old and new pods must not run side by side. Run the upgrade with --set strategy.type=Recreate (plus --server-side=false once with Helm 4) or scale to zero first — see Upgrading to rc.8 / rc.9, including the rollback.

Running without Redis

Value redis queue
config.sessionStore (HELIX_SESSION_STORE) — browser sessions Redis (default) PostgreSQL
config.tokenStore (HELIX_TOKEN_STORE) — codes, tokens, SSO sessions Redis (high-throughput tier) PostgreSQL (default)

With both on queue, leave redis.host empty and the server never contacts Redis:

helm install helixiam oci://ghcr.io/melmarzgioui/helixiam/charts/helixiam --version 1.1.0-rc.14 ... --set config.sessionStore=queue   # no redis.host

State that every replica must share follows these stores: the security rate limits (counted once for the cluster), the SAML assertion replay cache, the short-lived QR sign-in, push-approval, transaction-signing, device-enrolment and CIBA state, and EU Digital Identity Wallet transactions live in Redis when config.sessionStore is redis, and in PostgreSQL otherwise. See High availability.

Database TLS

database.sslMode (DB_SSLMODE: disable, allow, prefer, require, verify-ca or verify-full) and database.caSecret.name / database.caSecret.key (an existing Secret with the CA bundle that signed the PostgreSQL server certificate, mounted read-only and passed as DB_SSL_ROOT_CERT) turn on TLS to the database. Use verify-full in production: it checks the server certificate against the CA and its name against database.host. verify-ca or verify-full without a CA Secret is refused when the chart renders. Left empty, the JDBC driver default (prefer) applies: TLS when the server offers it, without verifying the certificate.

kubectl -n identity create secret generic postgres-ca --from-file=ca.crt=./postgres-ca.pem
helm upgrade --install helixiam oci://ghcr.io/melmarzgioui/helixiam/charts/helixiam --version 1.1.0-rc.14 ... \
  --set database.sslMode=verify-full --set database.caSecret.name=postgres-ca

Data retention

retention.enabled (default true), retention.interval and retention.auditLogMaxAge control the scheduled cleanup of expired rows; see Data retention.

The bootstrap admin password

Without the admin-password key, the first boot generates a strong random password. It is never logged: it is written (mode 0600) to /tmp/helixiam-admin-password in the pod that created the admin. /tmp is an emptyDir, so read it before that pod restarts, then sign in and change it:

kubectl exec deploy/helixiam -- cat /tmp/helixiam-admin-password

With several replicas, exec into the pod whose log says it generated the password.

Signing keys

On first boot the server seeds the master realm and generates its RSA signing keypair; no key mount is needed to start. Per-realm keys are managed and rotated from the console — see Realm keys.

Key custody (HSM and KMS)

Off by default: with custody.backends: [] every realm keeps software keys in the database. To keep realm keys in a PKCS#11 HSM, in Vault / OpenBao Transit or in a KMIP key manager (or, as your own choice, in a cloud key service through a plugin: AWS KMS, Azure Key Vault / Managed HSM or Google Cloud KMS, all Preview), declare the backends in the chart and then move realms as described in Key custody.

Value Meaning
custody.backends List of backends. Each has an id (a DNS-1123 label: lower-case letters, digits and -, at most 50 characters) and a type (pkcs11, vault, kmip, or the type of a plugin: aws-kms, azure-kv, gcp-kms).
PKCS#11 (type: pkcs11) library (path of the vendor module inside the container), tokenLabel, pinSecret: {name, key}, optional sessions and opTimeout. Optional keyTransport (see below).
Vault / OpenBao (type: vault) address, mount, optional namespace, auth: {kind, role, secretIdSecret: {name, key}, tokenSecret: {name, key}} (kind is approle, token or kubernetes), optional caSecret: {name, key} and opTimeout.
KMIP (type: kmip) address (host:port, port 5696 by default), serverName, tlsSecret: {name} (a kubernetes.io/tls Secret with the client certificate and its PKCS#8 key), caSecret: {name, key}, algorithms (required), kek (required, true or false), optional keyTransport, opTimeout, connectTimeout, connections and objectGroupPrefix. Mutual TLS and host-name verification cannot be turned off. See KMIP.
Plugin backend (type: aws-kms, azure-kv, gcp-kms, or your own) settings: {<name>: <string>} (a name that looks like a credential fails the render) and secretFiles: [{secret, key, file}]. The cloud plugins authenticate with the pod's workload identity, never with static keys. Preview. See Key custody plugins and Key custody.
custody.plugins.image / custody.plugins.jars The signed plugin image ({repository, digest}, pinned by digest: a tag alone fails the render) and the jars to load from it ([{file, sha256, type}], each pinned by its SHA-256). Empty by default: nothing is loaded.
custody.sovereignTypes Plugin backend types that you assert are sovereign key stores, for realms restricted to sovereign stores. The built-in pkcs11, vault and kmip always count. See Sovereign key stores only.
custody.backends[n].realms Optional, every backend: the realm ids it serves (one entry per tenant's HSM partition or Vault credential). Absent, the backend is shared by every realm. See Dedicated key stores.
custody.requireDedicatedBackendForHigh Default false. When true, a High-assurance realm needs a backend whose realms is exactly that realm.
extraVolumes / extraVolumeMounts Extra pod volumes and container mounts, rendered as given: for example a vendor PKCS#11 module from an init-container-populated emptyDir or a read-only hostPath.
dbEncryption.wrapped.secret {name, key} of the Secret holding the wrapped database key (DB_ENCRYPTION_WRAPPED).
dbEncryption.wrapped.kekRef The key-encryption-key reference in a custody backend (DB_ENCRYPTION_KEK).
dbEncryption.next.secret / dbEncryption.next.wrapped The next database encryption key of an online rotation (DB_ENCRYPTION_NEXT): a plain key in a Secret, or a wrapped one (secret and kekRef), not both. See Rotating the database encryption key.

The PKCS#11 keyTransport list names the RSA-OAEP key transports that eID decryption keys may use:

  • Each entry is written METHOD;DIGEST;MGF: RSA_OAEP_MGF1P;SHA1;SHA1 (xmlenc#rsa-oaep-mgf1p), or RSA_OAEP;<digest>;<mgf> (xmlenc11#rsa-oaep) with digest and MGF each one of SHA1, SHA256, SHA384 or SHA512.
  • Unset: every transport the token lists.
  • The server refuses an unknown value, one the token does not list, and (when a key is made) one the token cannot really decrypt.

The chart renders the backends into a ConfigMap (<release>-custody, key custody.yaml, mounted at /etc/helix/custody/ and loaded through SPRING_CONFIG_ADDITIONAL_LOCATION; not environment variables, which would lower-case the camelCase setting keys; a SPRING_CONFIG_ADDITIONAL_LOCATION of your own in config.extraEnv is merged into that one variable), mounts the PIN, secret id, token and CA bundle as files under /var/run/helix/custody/<id>/ (pin, secret-id, token, ca.crt), never as environment variables, and rolls the pods when a custody setting changes. A PKCS#11 module is not in the image: provide it with extraVolumes / extraVolumeMounts and point library at it (a module that reads its own configuration file gets its variable through config.extraEnv, for SoftHSM SOFTHSM2_CONF; see Configuring the module). Allow the backend's port in networkPolicy.extraEgressPorts (8200 for Vault, 5696 for KMIP, 443 for a cloud key service, your HSM's port for a network HSM).

# values.yaml (Helm): one HSM and one Vault backend
custody:
  backends:
    - id: hsm-1
      type: pkcs11
      library: /opt/hsm/lib/libvendorpkcs11.so
      tokenLabel: helixiam
      pinSecret: {name: hsm-pin, key: pin}
    - id: vault-1
      type: vault
      address: https://vault.example.com:8200
      mount: transit
      auth:
        kind: approle
        role: 0f6c1b9e-role-id
        secretIdSecret: {name: vault-approle, key: secret-id}
      caSecret: {name: vault-ca, key: ca.crt}
extraVolumes:
  - name: hsm-lib
    hostPath: {path: /opt/hsm/lib, type: Directory}
extraVolumeMounts:
  - name: hsm-lib
    mountPath: /opt/hsm/lib
    readOnly: true
networkPolicy:
  extraEgressPorts:
    - port: 587
      protocol: TCP
    - port: 8200
      protocol: TCP

dbEncryption.wrapped replaces the plain database key: it is mutually exclusive with secrets.dbEncryptionKey. Set that to "" when you use it, or the render fails. See DB_ENCRYPTION under a KEK.

After install

  • Sign in, change the admin password, and confirm the OIDC discovery document loads.
  • Set IDP_BASE_URL / SP_BASE_URL (Helm: config.idpBaseUrl / config.spBaseUrl) to your real https hostnames (WebAuthn requires https).
  • Upgrading an existing install? Coming from 1.0.0-rc.7 or earlier? Read Upgrading to rc.8 / rc.9 first; the other upgrade pages are under Upgrades & migrations.
  • Walk the Security hardening checklist before going live.

See also the full Configuration reference and the chart's own README.md and values.yaml (helm show readme / helm show values oci://ghcr.io/melmarzgioui/helixiam/charts/helixiam --version 1.1.0-rc.14).