# Install

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

!!! info "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
    [contact@helixiam.com](mailto:contact@helixiam.com). 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:

```bash
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](quickstart.md).

!!! danger "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](quickstart.md) 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`.

```bash
# 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](../operations/upgrade-1.1.0-rc1-rc2.md#4-the-charts-default-image-can-be-pulled).

### 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](architecture.md#the-admin-console).

```bash
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](../operations/upgrade-1.1.0-rc1-rc2.md#3-admin-api-only-on-the-admin-host).

```yaml
# 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:

```bash
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](../operations/security.md#reverse-proxies-and-host-names).

### Internal mail relays, directories and HTTP gateways

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

```bash
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`.

### Update strategy and service links

| 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](../operations/high-availability.md)) |
| `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://…` |

!!! danger "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](../operations/upgrade-rc8-rc9.md#13-spring-boot-4-do-not-run-old-and-new-pods-side-by-side), 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:

```bash
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](../operations/high-availability.md).

### 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.

```bash
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](../operations/data-retention.md).

### 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:

```bash
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](../operations/realm-keys.md).

### 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](../operations/key-custody.md).

| 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](../operations/key-custody.md). |
| 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](../operations/custody-plugins.md) and [Key custody](../operations/key-custody.md). |
| `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](../operations/key-custody.md#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](../operations/key-custody.md#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](../operations/key-custody.md#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](../operations/key-custody.md#configuring-the-module-softhsm-and-other-hsms)). 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).

```yaml
# 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](../operations/key-custody.md#db_encryption-under-a-kek-envelope).

## 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](../operations/upgrade-rc8-rc9.md) first; the other upgrade pages are under [Upgrades & migrations](../operations/upgrades.md).
- Walk the [Security hardening](../operations/security.md) checklist before going live.

See also the full [Configuration](configuration.md) 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`).
