Skip to content

Upgrading to rc.8 / rc.9

What you must change, and what will behave differently, when you move a HelixIAM install from v1.0.0-rc.7 to v1.0.0-rc.8 or v1.0.0-rc.9.

Which release has what

Both are released: v1.0.0-rc.8, and v1.0.0-rc.9 (2026-10-13), which moves the server to Spring Boot 4.1 (Spring Framework 7, Spring Security 7.1) and adds the experimental EU Digital Identity Wallet sign-in and a complete outbound-request (SSRF / DNS-rebinding) hardening. Items marked rc.9 arrived with rc.9. If you upgrade from rc.7 or rc.8 to rc.9, apply every item on this page — and read item 13 first: the rc.9 rollout must not run old and new pods side by side. The sources are the BREAKING checklist at the top of CHANGES-1.0.md and docs/UPGRADING.md in the product repository.

Before you upgrade: checklist

Work through this list before you roll out. Each item links to its detail below.

# Do this Applies from
1 Behind any TLS-terminating proxy, ingress, load balancer or mesh, set HELIX_TRUSTED_PROXIES rc.8
2 List every other host name clients use in HELIX_ALLOWED_HOSTS rc.8
3 Check clients that reach the server on another port than the base URL's (port mappings) rc.9
4 Make sure your proxy appends to X-Forwarded-For: X-Real-IP is ignored rc.9
5 If you are adding DB_ENCRYPTION to an install that ran without it, plan the plaintext migration window rc.8 (enforced at runtime from rc.9)
6 Read the first start's log for the legacy re-encryption WARN rc.9
7 Read the generated admin password from the password file, not the log rc.9
8 Check that each eID connection's minimum level of assurance is one the server knows rc.8
9 Classic DigiD now requests the Logius context classes rc.9
10 Stop reading identity-provider secrets back from the admin API (sync scripts, Terraform) rc.9
11 Expect in-flight federated logins to fail once during the rollout rc.9
12 Give the server a database or Redis (no in-memory fallback) rc.9
13 Spring Boot 4: rename FACTOR_… roles, back up, upgrade with no old and new pods side by side (Recreate or scale to 0); know the rollback rc.9
14 If you set Spring properties directly, rename them rc.9
15 Allow an internal or loopback SMTP relay (realm or global) in HELIX_EGRESS_ALLOWED_PRIVATE_HOSTS, and odd ports in HELIX_MESSAGING_SMTP_ALLOWED_PORTS rc.9
16 Allow an internal LDAP directory in HELIX_FEDERATION_LDAP_ALLOWED_HOSTS rc.9
17 Rename helix.outbound.fetch.login.* if you tuned it rc.9
18 Give every private_key_jwt client an https JWKS URL rc.9
19 Use /agent/delegation/token, not actor_token on /oauth2/token rc.9
20 Expect wallet issuer certificates to be checked with HARD_FAIL by default rc.9

1. Trusted proxies are now required behind a proxy

What changed. The server honours forwarded headers only from the proxies you list. X-Forwarded-For (the client IP) and Forwarded / X-Forwarded-Proto / -Host / -Port / -Prefix (the scheme, host and OIDC issuer) are applied only when the TCP peer is in HELIX_TRUSTED_PROXIES; from any other peer they are removed. Before rc.8 they were honoured from any client, so a single request could steer the issuer in the discovery document or claim any client IP for the rate limiter and the audit log.

What you must do. Behind an ingress controller, an external load balancer, a Gateway API route, a service mesh or the console's nginx, set the addresses the proxy connects from:

Setting Value
Environment HELIX_TRUSTED_PROXIES (property helix.security.trusted-proxies)
Helm config.trustedProxies
Format comma-separated IP addresses or CIDR ranges, IPv4 or IPv6
Default empty: no proxy is trusted
# Example: the ingress controller's pod CIDR
HELIX_TRUSTED_PROXIES=10.42.0.0/16
# Helm
helm upgrade helixiam deploy/helm/helixiam --reuse-values \
  --set 'config.trustedProxies={10.42.0.0/16}'

If you skip it. Behind a TLS-terminating proxy the server sees plain HTTP: the request-derived issuer and redirects become http://, so relying parties reject the tokens' iss. Every request also appears to come from the proxy, so all clients share one sign-in rate-limit bucket and the audit log records the proxy's address. The server logs an ERROR, at most once a minute, when an https IDP_BASE_URL is reached over plain HTTP, and the Helm install notes print a warning whenever config.trustedProxies is empty.

Never list addresses clients can connect from directly

A listed address is believed when it says who the client is. Only list your own proxies.

2. The server answers only for its own host names

What changed. Outside the dev profile, a request whose Host (after trusted forwarded-header processing) is not the host of IDP_BASE_URL, the host of SP_BASE_URL, or a host listed in HELIX_ALLOWED_HOSTS gets 400, before any issuer or URL is derived from it. Before rc.8, a server reached directly answered Host: evil.example with the issuer http://evil.example/realms/….

What you must do. List every other host name that clients use to reach the server, for example the in-cluster service name that relying parties call for tokens or JWKS:

Setting Value
Environment HELIX_ALLOWED_HOSTS (property helix.security.allowed-hosts)
Helm config.allowedHosts
Format comma-separated; name accepts any port, name:port (or [v6]:port) only that port
Default empty: only the hosts of IDP_BASE_URL and SP_BASE_URL
HELIX_ALLOWED_HOSTS=helixiam.identity.svc,helixiam.identity.svc.cluster.local
# Helm
helm upgrade helixiam deploy/helm/helixiam --reuse-values \
  --set 'config.allowedHosts={helixiam.identity.svc}'

Probes keep working: the connection's own address (kubelet probes send the pod IP), localhost from a loopback peer (Docker HEALTHCHECK) and /actuator/health/** stay reachable.

3. The Host port counts too

rc.9. In rc.8 the host allow-list ignored the port. From rc.9, outside dev:

  • a host of IDP_BASE_URL or SP_BASE_URL is accepted only on that URL's port (its explicit port, or 443/80 for https/http);
  • a Host header without a port is accepted;
  • a HELIX_ALLOWED_HOSTS entry accepts any port unless it is written host:port (or [v6]:port);
  • a port outside 1–65535 gets 400;
  • the probe exemptions (the connection's own address, localhost from a loopback peer) apply only on the port the connection arrived on, or the scheme's default port; /actuator/health/** still answers on any host and port.

What you must do. A client that reaches the server by its base URL's host on another port, for example through a Docker port mapping, needs that host:port in HELIX_ALLOWED_HOSTS. Otherwise it gets 400.

# IDP_BASE_URL=http://localhost:8080, but a tool calls http://localhost:18080
HELIX_ALLOWED_HOSTS=localhost:18080

4. X-Real-IP is no longer read

rc.9. The client IP comes only from X-Forwarded-For, and only from a trusted proxy (read from the right; the first address that is not a trusted proxy is the client). X-Real-IP is never read, not even from a trusted proxy: a trusted hop that neither set nor stripped it (an L4 load balancer, an ingress with forwarded headers off) let a client choose its own address for rate limits and the audit log.

What you must do. Make sure every trusted proxy appends to X-Forwarded-For (nginx $proxy_add_x_forwarded_for; ingress-nginx and cloud load balancers do). A proxy that sets only X-Real-IP now makes every client appear as the proxy.

5. Plaintext secret rows: a time-boxed migration window

What changed (rc.8). A secret column (TOTP and HOTP secrets, realm signing keys, client and webhook secrets, SCIM tokens, messaging-provider secrets, SAML relying-party certificates) whose stored value cannot be decrypted used to be returned as stored. It now fails the read. A deployment that ran without DB_ENCRYPTION and then sets it may hold plaintext rows; reading them is an explicit, loud and time-boxed migration mode:

Variable Default Meaning
HELIX_DB_ENCRYPTION_MIGRATE_PLAINTEXT false true reads undecryptable secret columns as stored (a WARN per read, naming entity and column), encrypts them on their next write, and logs an ERROR banner at startup and every hour
HELIX_DB_ENCRYPTION_MIGRATE_UNTIL empty the last day of the window, yyyy-MM-dd (UTC, inclusive); outside dev the server refuses to start in migration mode unless this is a date at most 30 days ahead

rc.9: the end date is also enforced while the server runs. After that day, a plaintext read fails closed and logs an ERROR naming the date (rc.8 only checked it at startup).

DB_ENCRYPTION=<your 32-hex-character key>
HELIX_DB_ENCRYPTION_MIGRATE_PLAINTEXT=true
HELIX_DB_ENCRYPTION_MIGRATE_UNTIL=2026-11-10

Unset both once the rows are rewritten. HELIX_ALLOW_PLAINTEXT_SECRETS again only means "accept running without a key", and an empty DB_ENCRYPTION now means "no key" (it used to fail startup with "Empty key").

The mode also hides a changed key

While it is on, a value written under a different DB_ENCRYPTION key is read as stored too. That is why it is loud and time-boxed. Never use it to paper over a key change.

6. Legacy-encrypted secrets are re-encrypted at startup

rc.9. At every startup, values of every encrypted column still in the legacy AES/ECB format (realm keys, TOTP and HOTP secrets, client secrets, SCIM tokens, webhook secrets, identity-provider and messaging secrets, SAML relying-party certificates, verifier keys, wallet peppers) are re-encrypted to AES-GCM. The job is idempotent and logs table, column and row id only. Rows it cannot read are skipped and named in a production-readiness WARN.

After that run the legacy format is no longer read: a remaining legacy value fails closed, unless you set:

Variable Default Meaning
HELIX_DB_ENCRYPTION_ALLOW_LEGACY false true keeps reading the legacy format after the startup run

Each column is migrated in its own transaction. A column that fails with a database error is logged at ERROR by table and column, reported as a readiness WARNING, and counted in the gauge helix_db_encryption_legacy_failed_columns; the legacy fallback then stays on until a later start migrates it.

What you must do. Usually nothing: the first start rewrites the legacy rows. Check that start's log for the readiness WARN. Only if the WARN names rows, or you attach a database restored from an older backup to a running server, set HELIX_DB_ENCRYPTION_ALLOW_LEGACY=true (or restart, which re-runs the job) while you investigate.

7. The generated admin password is no longer logged

rc.9. When HELIX_ADMIN_PASSWORD is unset, the first boot still generates a strong random password for the master-realm admin, but it is never logged. It is written with mode 0600 to HELIX_ADMIN_PASSWORD_FILE, and only the path is logged.

Variable Default
HELIX_ADMIN_PASSWORD_FILE $TMPDIR/helixiam-admin-password (/tmp/helixiam-admin-password in the container image)

Read it from where the server ran, then sign in and change it:

# Docker Compose (after removing HELIX_ADMIN_PASSWORD from the compose file)
docker compose exec server cat /tmp/helixiam-admin-password

# Helm: /tmp is an emptyDir, so read it before that pod restarts.
# With several replicas, exec into the pod whose log says it generated the password.
kubectl exec deploy/helixiam -- cat /tmp/helixiam-admin-password

HELIX_ADMIN_USERNAME, HELIX_ADMIN_PASSWORD and HELIX_ADMIN_PASSWORD_FILE are declared in application.properties, so they also work as command-line arguments (--HELIX_ADMIN_PASSWORD=…). For any non-local install, set HELIX_ADMIN_PASSWORD explicitly (Helm: the admin-password key of secrets.existingSecret).

8. Unknown eID minimum levels are refused

What changed (rc.8). An eID connection (DigiD, eHerkenning, eIDAS) whose minimumLoa was not a level the server knows accepted every asserted level. An unrecognised non-blank minimum is now never met (fail closed); a blank minimum still means "no minimum". The keywords the console stores are now mapped to the scheme's ladder, and eHerkenning EH2+ (loa2plus) ranks above EH2 and below EH3.

An unknown minimum is refused:

  • when the connection is saved through the admin API: 400 with fieldErrors.minimumLoa;
  • when a realm is imported: the import reports it as a failed import with the field error (rc.9; rc.8 saved it and skipped it with a WARN at load);
  • when a stored connection is built: it is skipped with a warning (a connection in helix.federation.eid fails startup).

What you must do. Check every eID connection's minimum against the accepted values. A connection with a value outside that list stops working after the upgrade.

9. Classic DigiD requests the Logius context classes

rc.9. The classic DigiD AuthnRequest used to ask for the urn:nl-eid-gdi:1.0:LoA:* URN, which a DigiD endpoint does not know. Following Logius' Koppelvlakspecificatie DigiD SAML Authenticatie v3.5, a DigiD keyword is now requested as the SAML 2.0 context class of that level, with RequestedAuthnContext Comparison="minimum":

DigiD level Keywords Requested class (urn:oasis:names:tc:SAML:2.0:ac:classes:…)
Basis loa1, basis PasswordProtectedTransport
Midden loa2, midden MobileTwoFactorContract
Substantieel loa3, substantieel Smartcard
Hoog loa4, hoog SmartcardPKI

The asserted class is ranked on the same ladder (rc.8 already ranked these classes when comparing). A minimum stored as an urn:nl-eid-gdi:1.0:LoA:* URN is still compared as stored but requested as the context class of the same level, and brokers that assert those URNs keep working. No eID scheme sends an nl-eid-gdi URN in the request any more.

What you must do. Nothing for the console keywords. If you worked around the old behaviour by storing the oasis class by hand as an "Other (custom)" minimum, that is no longer needed. A broker that requires the nl-eid-gdi URN in the AuthnRequest is not supported as a DigiD connection. See DigiD.

10. Identity-provider secrets are write-only in the admin API

rc.9 — API change. GET /admin/realms/{realm}/identity-providers (list and by alias) no longer returns secret values: the OIDC clientSecret, the eID SP private keys (spDecryptionPrivateKey, spSigningPrivateKey), the LDAP bind password, and the wallet's keys. Each one comes back as a flag instead:

{ "clientSecretSet": true }

On a write (POST or PUT):

You send Effect
the key absent, null or blank the stored secret is kept
a value the stored secret is replaced
"clear<Key>": "true" (e.g. "clearClientSecret": "true") and no value the stored secret is removed
a value and its clear flag 400

Secrets are now encrypted at rest (Flyway V75 / V75.1), and realm exports mask them.

What you must do. Any script or tool that reads a secret back must stop doing so. A tool that compares the returned secret with its desired value (a sync script, a Terraform provider) will always see a difference. The bundled Terraform provider is not affected. At rc.9 it had no identity-provider resource; its helix_wallet_identity_provider resource, added after rc.10, never reads a secret back: access_certificate_private_key keeps the value in state, and removing it from the configuration clears it on the server. A third-party or home-grown provider that manages identity providers is affected.

11. In-flight federated and wallet logins fail once during the rollout

rc.9. The broker's per-login session state (state, nonce, SAML request id, browser binding, wallet transaction) is now keyed by realm and alias, so logins in two realms that use the same alias no longer clobber each other. The session keys were renamed as a result.

What to expect. A federated (OIDC, SAML, eID) or wallet login, or a wallet link in the account console, that starts on the old version and finishes on the new one ends with error=federation (or a refused link). Starting it again works. This is transient and affects no data. Roll out outside peak hours if that matters to you.

12. The wallet transaction store needs the database or Redis

rc.9. The EU Digital Identity Wallet keeps each login's in-flight state in a transaction store shared by all replicas: PostgreSQL (wallet_transaction, Flyway V79) or Redis, chosen from your session and token stores. The in-memory store is used only when you ask for it with HELIX_WALLET_TRANSACTION_STORE=memory (development and tests, one replica). A server with neither Redis nor a database that was not asked for memory refuses to start instead of falling back silently.

What you must do. Nothing for a normal install, which always has PostgreSQL. See Transaction store.

13. Spring Boot 4: do not run old and new pods side by side

rc.9. The server moved from Spring Boot 3.5 (rc.8) to Spring Boot 4.1 (Spring Security 7.1). It reads everything the earlier release stored — sessions, authorization codes, refresh tokens, device codes, consents — in PostgreSQL and in Redis. The reverse does not hold: the earlier release cannot read what rc.9 writes or rewrites. Every new sign-in carries Spring Security 7's sign-in factor (FactorGrantedAuthority), a class the earlier release does not have.

Written by rc.9 Read by an earlier pod, PostgreSQL store (queue) Read by an earlier pod, Redis store
a browser session (any sign-in) dropped (HTTP session dropped WARN): the user is signed out HTTP 500 on every request with that cookie, until the session expires or is deleted
an OAuth2 authorization rc.9 created or rewrote (sign-in, code exchange, refresh, revocation, consent, device-flow step) treated as absent: refresh fails (invalid_grant), introspection says active: false HTTP 500 for that token until it expires or is deleted

Access tokens are JWTs and stay valid for their lifetime on either version. The stores: PostgreSQL tables helix_authorization, helix_authorization_token and helix_http_session (HELIX_SESSION_STORE=queue); Redis keys spring:session:* (sessions, the default store) and helix:authz:* (tokens with HELIX_TOKEN_STORE=redis).

1. Rename roles named FACTOR_…. The prefix (any case) is now reserved for Spring Security's sign-in factors (FACTOR_PASSWORD marks every password login). A role with such a name is ignored — it grants nothing in admin RBAC and never appears in a token — and the admin API and realm import refuse to create one (400). List them; an empty result means nothing to do:

Run this query with psql "$DATABASE_URL":

SELECT 'realm role' AS kind, tenant_id AS realm, name AS role, NULL AS holder
  FROM user_roles WHERE name ILIKE 'factor\_%'
UNION ALL
SELECT 'client role', realm_id, name, client_id
  FROM client_role WHERE name ILIKE 'factor\_%'
UNION ALL
SELECT 'service-account role', realm_id, role_name, client_id
  FROM client_service_account_role WHERE role_name ILIKE 'factor\_%'
UNION ALL
SELECT 'agent role', realm_id, roles, name
  FROM agent_identity WHERE roles ~* '(^|[,[:space:]/])factor_'
ORDER BY 1, 2, 3;

It covers realm, client, service-account and agent roles. Rename any it lists (create the new role, move its users, grants and mappings, delete the old one) before upgrading.

2. Back up PostgreSQL, the token store included (a pg_dump of the whole database covers it), and Redis if you use it (redis-cli BGSAVE, then copy dump.rdb).

3. Upgrade without mixing versions. A rolling update, or scaling to one replica, still runs old and new side by side. Pick one:

  • Helm, Recreate — every old pod stops before a new one starts (the server is unavailable for one start-up):
helm upgrade helixiam deploy/helm/helixiam --reuse-values --server-side=false \
  --set image.tag=<new version> --set strategy.type=Recreate

--server-side=false is needed once, with Helm 4: the rc.8 chart never rendered spec.strategy, so under Helm 4's default server-side apply the rollingUpdate block Kubernetes defaulted would survive and the API server would reject the Deployment. With Helm 3 leave the flag out. From rc.9 the chart renders the strategy in full (the new value strategy), so later changes need no flag. - Or scale to zero first, then upgrade (the upgrade restores replicaCount):

kubectl -n <namespace> scale deploy/helixiam --replicas=0
kubectl -n <namespace> wait --for=delete pod -l app.kubernetes.io/name=helixiam --timeout=180s
helm upgrade helixiam deploy/helm/helixiam --reuse-values --set image.tag=<new version>
- Docker Compose / plain containers: stop every old container, then start the new ones.

4. Check readiness (/actuator/health/readiness), one sign-in, and one refresh of a token issued before the upgrade. Later releases can go back to rolling updates: --set strategy.type=RollingUpdate.

Rolling back to rc.8 signs users out and drops every authorization rc.9 wrote or rewrote. The rc.8 chart has no strategy value, so helm rollback alone would mix the versions. Instead:

  1. Scale to zero and wait until every rc.9 pod is gone (the two kubectl commands above).
  2. Delete what rc.8 cannot read from Redis (PostgreSQL stores: nothing to delete): sessions spring:session:*, and with HELIX_TOKEN_STORE=redis tokens helix:authz:* (every client then needs a new sign-in or client_credentials token). Otherwise rc.8 answers HTTP 500 for them until they expire. The script uses SCAN and UNLINK:

# REDIS_HOST, REDIS_PORT; REDIS_PASSWORD (requirepass) or REDIS_USER + REDIS_PASSWORD (ACL);
# REDIS_TLS=1 + REDIS_CACERT (+ REDIS_CERT, REDIS_KEY); REDIS_CLUSTER=1 for a cluster.
REDIS_HOST=redis.example.internal REDIS_PASSWORD=... deploy/scripts/redis-delete-keys.sh 'spring:session:*'
REDIS_HOST=redis.example.internal REDIS_PASSWORD=... deploy/scripts/redis-delete-keys.sh 'helix:authz:*'

# or inside the Redis pod (no local redis-cli needed):
kubectl -n <namespace> exec -i deploy/redis -- sh -s 'spring:session:*' < deploy/scripts/redis-delete-keys.sh
3. helm rollback helixiam <revision before the upgrade> (helm history helixiam lists it). 4. Check the pods are back; if the replica count stayed at 0, kubectl -n <namespace> scale deploy/helixiam --replicas=<n>.

Restoring the backup instead undoes every other change made since the upgrade (users, clients, settings, audit log): use it only if nothing else changed.

14. Renamed Spring properties

rc.9. Only if you set Spring properties directly (environment variables SPRING_* / MANAGEMENT_*, or a mounted application.properties). The HELIX_* settings and the Helm chart are unchanged.

Spring Boot 3.5 (rc.8) Spring Boot 4 (rc.9)
server.error.* (e.g. SERVER_ERROR_INCLUDE_MESSAGE) spring.web.error.* (SPRING_WEB_ERROR_INCLUDE_MESSAGE); the old keys are ignored
spring.session.redis.* spring.session.data.redis.*
spring.jackson.* (the application's JSON mapper) spring.jackson2.* (spring.jackson.* now configures Boot's Jackson 3 mapper only)
spring.dao.exceptiontranslation.enabled spring.persistence.exceptiontranslation.enabled

This row is reversed again as of rc.11. rc.9/rc.10 (above) ran the application's own mapper on Jackson 2 under spring.jackson2.*, with spring.jackson.* configuring only Boot's JsonMapper. rc.11 moves the application itself onto Jackson 3: spring-boot-jackson2 is removed, and spring.jackson.* once again configures the application's mapper (as well as Boot's). There is no spring.jackson2.* property any more. See Upgrading from rc.10 to rc.11 for the full detail, including the new 500-level nesting limit and the unchanged 20 MB string limit.

15. SMTP relays pass the egress guard

rc.9. Until rc.8 the SMTP driver connected to any host and port a realm admin typed, loopback and cluster-internal addresses included. Now every relay — a realm's SMTP provider and the operator's global relay (helix.notification.smtp.*, used when a realm has no email provider) — passes the outbound guard: the name is resolved once, the connection goes to the checked address, and TLS (SMTPS or STARTTLS) still verifies the configured host name.

Setting Helm Default Meaning
HELIX_EGRESS_ALLOWED_PRIVATE_HOSTS (helix.egress.allowed-private-hosts) config.egressAllowedPrivateHosts empty private or loopback hosts outbound calls may reach; exact host names or IP literals, optionally host:port
HELIX_MESSAGING_SMTP_ALLOWED_PORTS (helix.messaging.smtp.allowed-ports) — (config.extraEnv) empty SMTP ports allowed besides 25, 465, 587 and 2525; ports or ranges, e.g. 1025,8025-8030

What you must do. List an in-cluster or localhost relay, preferably as host:port so only that port opens:

HELIX_EGRESS_ALLOWED_PRIVATE_HOSTS=mailer.mail.svc.cluster.local:587
# a relay on a non-standard port also needs:
HELIX_MESSAGING_SMTP_ALLOWED_PORTS=1025

Until then sends fail with result CONFIGURATION ("not allowed by the egress policy"). Both are deployment settings: a realm admin cannot widen them. In Helm, also allow the port in networkPolicy.extraEgressPorts.

16. LDAP directories pass the egress guard

rc.9. An LDAP federation provider used to let JNDI connect to any URL a realm admin typed. Now only one ldap:// or ldaps:// URL is accepted, the host is resolved once and both binds connect to the checked address, LDAPS verifies the certificate against the configured name, and referrals are ignored. A private or loopback directory — the usual Active Directory setup — must be listed:

Setting Helm Default Format
HELIX_FEDERATION_LDAP_ALLOWED_HOSTS (helix.federation.ldap.allowed-hosts) config.ldapAllowedHosts empty (public directories only) host names (ldap.corp, ldap.corp:636) or CIDR ranges (10.20.0.0/16)
helm upgrade helixiam deploy/helm/helixiam --reuse-values --set 'config.ldapAllowedHosts={ldap.corp,10.20.0.0/16}'

Until then LDAP logins for that provider fail (logged as refused by the egress policy). Metadata and link-local addresses stay refused even inside a listed range. Also allow 389/636 in networkPolicy.extraEgressPorts.

17. Outbound fetch limits

rc.9. Server-initiated calls run in feature groups, each with a total limit and a per-realm share, so one realm's slow endpoints cannot delay another realm or any login:

Group Used by helix.outbound.fetch.<group>.max-concurrent .per-realm-max
login-admin OIDC broker, eID, workload identity 32 8
login-client client-assertion JWKS, FAPI request-object JWKS 32 8
otp OTP delivery 32 8
outbound webhooks, SCIM, logout 32 8
ldap LDAP binds (one 15 s deadline per bind sequence) 32 8
wallet wallet status lists and issuer revocation 16 = max-concurrent

What you must do. Only if you tuned it: helix.outbound.fetch.login.* is split into login-admin and login-client. The old login names are still read as login-admin; helix.wallet.fetch.max-concurrent is still read as helix.outbound.fetch.wallet.max-concurrent. Webhook and SCIM queues also hold at most 1,000 pending deliveries per realm and helix.outbound.deliveries.max-pending (20,000) in total.

18. Client JWKS URLs must be https

rc.9. A private_key_jwt client's JWKS URL is chosen by whoever registers the client, so the token endpoint now fetches it through the outbound guard, without redirects.

  • Admin API, console and realm-import client writes need an https jwksUrl (http://localhost / http://127.0.0.1 only in dev). A realm import reports such a client as a failed clients entry (jwksUrl: The JWKS URL must be an https URL.).
  • Flyway V80 rewrites a DCR-stored lowercase private_key_jwt auth method to PRIVATE_KEY_JWT. The auth method is matched case-insensitively; the admin API and realm import refuse unknown values. Dynamic Client Registration accepts only client_secret_basic, private_key_jwt and none, and requires an https jwks_uri the guard allows.
  • A client stored earlier with an http JWKS URL can no longer authenticate outside dev: the token endpoint refuses to fetch it (invalid_client). At startup the server logs a WARN (ClientAuthMethodAudit) for every such client, and for every private_key_jwt client without a JWKS URL. Change their JWKS URL to https before upgrading.

19. actor_token is refused on /oauth2/token

rc.9. A token exchange on /oauth2/token with actor_token or actor_token_type is refused with invalid_request. Plain token exchange (subject_token only) still works. HelixIAM's supported on-behalf-of delegation is /agent/delegation/token — see Agent authorization.

20. Wallet issuer revocation defaults to HARD_FAIL

rc.9. An EU Digital Identity Wallet connection without an explicit revocationChecking now checks issuer certificates with HARD_FAIL (was SOFT_FAIL): an issuer certificate whose CRL / OCSP status cannot be determined — including one that names no CRL or OCSP endpoint — is refused. For an issuer that publishes neither, set revocationChecking to SOFT_FAIL (console: Credentials → Issuer certificate revocation).


These need no action but change behaviour you may see:

  • Federated logins get a new session id at completion and again after a post-login step-up (OIDC, SAML, eID, wallet).
  • MFA after a federated sign-in. The post-broker second factor is now enforced for users with MFA enabled, and in realms that require MFA, a user who has not enrolled is sent to /mfa/enable after a federated, password, flow-engine or /flow sign-in instead of getting a full session.
  • Console-created identity providers load in every realm (HELIX_FEDERATION_STORE_ENABLED, default true). With several replicas, an identity-provider change reaches every replica within helix.federation.store.poll-interval-seconds (default 10).
  • Broker start errors send the browser to /login?error=federation instead of an error page.
  • eID artifact binding. The back-channel ArtifactResponse of classic DigiD must carry a valid signature by the IdP's metadata signing key; an unsigned response, or any response when the connection has no IdP signing certificate, is refused.
  • ID tokens carry acr for brokered eID logins whose provider asserted a recognised level, and for wallet logins.
  • APNs push goes only to api.push.apple.com / api.sandbox.push.apple.com (HTTP/2, no redirects); a provider host naming anything else is a configuration error.
  • Outbound calls are pinned (rc.9): webhooks, SCIM, HTTP email/SMS/push, logout, OIDC broker and workload-identity JWKS, eID artifact resolution, SAML metadata import, wallet status lists and CRL/OCSP connect to the address the guard checked (no DNS rebinding), follow no redirects, and IPv6 forms that embed an IPv4 address are judged by that address.
  • Helm (rc.9): new values strategy (rendered in full; default Kubernetes' RollingUpdate 25% / 25%) and enableServiceLinks (default false, so a Service named redis no longer injects REDIS_PORT=tcp://…).

See also

  • Upgrades & migrations — the general rolling-upgrade procedure
  • Security hardening — the production checklist, including proxies and host names
  • Configuration — every variable on this page, with defaults
  • Install — Helm values config.trustedProxies, config.allowedHosts, config.ldapAllowedHosts, strategy