Skip to content

Realm keys

Every realm signs its tokens and its SAML assertions with its own key. You can let HelixIAM generate keys, bring your own key and certificate, or have HelixIAM create a key and a certificate signing request (CSR) for your CA — and roll over to a new key without downtime.

What it is

Each realm owns its own signing keys and publishes the public half in its JWKS and its SAML metadata. Relying parties verify the realm's tokens and assertions with those, so realms are isolated: one realm can never verify another's tokens.

  • An active RS256 key, always — plus an optional ES256 key. OpenID Connect requires RS256, so every realm keeps exactly one ACTIVE RSA key (RS256, with PS256 published alongside for FAPI clients). A realm may add exactly one ACTIVE EC P-256 key (ES256) beside it; an EC key can only be activated while an ACTIVE RSA key exists.
  • Who signs what. ID tokens use the client's ID-token algorithm (id_token_signed_response_alg, RS256 by default). Access tokens, logout tokens, delegation and workload-identity tokens and JARM responses use the realm's default signing algorithm. SAML signs with the ACTIVE RSA key unless the realm chooses ES256 for SAML. See Signing settings.
  • Staged rollover. A new key starts as PENDING: it is published (JWKS and SAML metadata) but does not sign. When relying parties have picked it up, you activate it; the previous key becomes retained (status ROTATED) and stays published until you retire it, so tokens it signed keep verifying.
  • Where keys live. In PostgreSQL (realm_key), the private key encrypted with DB_ENCRYPTION. Imported and CSR keys are refused when DB_ENCRYPTION is not set (outside the dev profile). The private key is never returned by the API, shown in the console, logged or written to the audit log.
Status Published in JWKS and SAML metadata Signs
PENDING yes no
ACTIVE (one RSA, at most one EC) yes (listed first) yes
ROTATED (console: Retained) yes no
RETIRED no no

Where relying parties get the keys

What Where
OIDC/OAuth2 token signing keys (JWKS) /realms/<realm>/oauth2/jwks, also advertised as jwks_uri in /realms/<realm>/.well-known/openid-configuration
Signing algorithms id_token_signing_alg_values_supported in the discovery document: exactly the algorithms of the ACTIVE keys (RS256, PS256, and ES256 while an EC key is active); PENDING and retained keys are not advertised
SAML signing certificates /realms/<realm>/saml/idp/metadata, one <KeyDescriptor use="signing"> per published key (ACTIVE first)

Every JWK carries kid, alg and use: sig. A key with a certificate chain also carries x5c (standard Base64 DER, leaf first, without a trailing self-signed root) and x5t#S256. The kid of an RSA key is derived from its modulus (as in earlier releases, so existing tokens keep verifying); the kid of an EC key is its RFC 7638 thumbprint. The console shows the kid of every key.

SAML assertion signing

The realm signs SAML with its ACTIVE RSA key — or with its ACTIVE EC key when the realm's SAML signing algorithm is ES256 (see Signing settings) — and publishes that key's certificate first in the IdP metadata:

  • With a certificate (imported, or CA-signed after a CSR): the metadata and the signatures' KeyInfo carry that leaf certificate.
  • Without a certificate (generated keys, or a CSR key before its certificate comes back): HelixIAM publishes a deterministic self-signed certificate around the key (subject CN=helix-idp-<realm>, valid 2000-01-01 to 2099-12-31), exactly as before. A PENDING key without a certificate is published with such a self-signed certificate too, so a metadata-reading SP can roll over to it either way.
  • PENDING and retained keys are published as additional <KeyDescriptor use="signing"> elements, so service providers that re-read the metadata trust the next key before it signs.
  • The <KeyDescriptor use="encryption"> is published only while SAML signs with an RSA certificate whose keyUsage allows keyEncipherment (or has no keyUsage extension).

Service providers should read live metadata — not pin the certificate

An SP that consumes the realm's metadata URL and re-reads it picks up a staged key before you activate it. An SP configured with a pinned copy of the certificate rejects assertions as soon as you activate a different key (or attach a renewed certificate to the active key): give it the new certificate first. See SAML clients.

EC keys and SAML

With the SAML signing algorithm set to ES256, SAML messages are signed with http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256, whatever RSA algorithm a SAML client's options name. Many service providers accept RSA signatures only: check every connected SP first. The default stays RSA. HelixIAM's own sandbox relying party (a test harness) verifies RSA SAML signatures only.

In the console

Open Manage → Realms → Realm keys. Changing keys needs the realm-admin permission (whoever controls the signing key can issue any token of the realm); admins with manage-realm only see the keys, read-only. Each key shows its status (Pending / Active / Retained / Retired), algorithm, certificate subject and issuer, expiry (with a warning chip when it expires within 30 days and a red chip when it has expired), and SHA-256 fingerprint; the kid and key id are in the muted line under its name. From here you can:

  • Import key and certificate — paste a PEM private key and its PEM certificate chain.
  • Create CSR — choose RS256 or ES256, enter the subject (and DNS names); copy or download the CSR.
  • Upload signed certificate — attach the chain your CA returned (pending or active keys).
  • Activate — a pending key starts signing; a confirmation explains the effect, including the SAML note above. It shows how long the key has been published; before the minimum staging time it asks you to confirm a forced activation.
  • Rotate (generate) — generate a new key and activate it at once (no staging).
  • Retire — drop a pending or retained key (or the active ES256 key) from the JWKS and metadata. The active RS256 key cannot be retired.

Back up the database and keep DB_ENCRYPTION

Realm keys live in PostgreSQL, encrypted with DB_ENCRYPTION. Losing the database, or the DB_ENCRYPTION value, loses the keys: every issued token and every cached JWKS stops validating, and SAML service providers must re-read metadata. Back up both. See the security checklist and backup guide.

Bring your own key and certificate

POST /admin/realms/{realm}/keys/import takes {privateKeyPem, certificateChainPem}.

  • Private key: exactly one unencrypted PEM key, with nothing before or after it — PKCS#8 (BEGIN PRIVATE KEY), PKCS#1 (BEGIN RSA PRIVATE KEY) or SEC1 (BEGIN EC PRIVATE KEY). RSA must be 2048 to 4096 bits with a public exponent of at least 65537 (odd, at most 32 bits); EC must be P-256. Encrypted PEM is refused (decrypt it first, for example openssl pkey -in key.pem -out plain.pem).
  • Certificate chain: PEM certificates, leaf first, each certificate issued by the next one. The leaf must be for this private key, must not be a CA certificate (CA:true), and must have digitalSignature if it has a keyUsage extension. Every following certificate must be a CA (CA:true, and keyCertSign if it has a keyUsage). Every certificate of the chain must be valid now (an expired or not-yet-valid certificate is refused). A single self-signed certificate is accepted.
  • A key that is already stored (in this realm or any other) is refused: every realm needs its own key.

Refusals are 400 {message, fieldErrors} on privateKeyPem or certificateChainPem. The examples below assume you have set $HELIX_URL / $REALM and logged in — see Authenticating to the API.

# Refresh CSRF after any GET, then POST (see the auth guide for the helper)
curl -s -b cookies.txt -c cookies.txt "$HELIX_URL/admin/realms/$REALM/keys" >/dev/null
CSRF=$(awk '($6=="__Host-XSRF-TOKEN"||$6=="XSRF-TOKEN")&&$7~/^r1\./{t=$7} END{print t}' cookies.txt)

jq -n --rawfile key key.pem --rawfile chain chain.pem \
  '{privateKeyPem: $key, certificateChainPem: $chain}' |
curl -s -b cookies.txt -X POST -H "X-XSRF-TOKEN: $CSRF" -H "Content-Type: application/json" \
  --data-binary @- "$HELIX_URL/admin/realms/$REALM/keys/import"
{
  "keyId": "3f2c9a10-6b1e-4d8f-9c55-1a7e0b2d4c61",
  "algorithm": "RS256",
  "status": "PENDING",
  "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwV1W…AB",
  "createdAt": 1792164099009,
  "rotatedAt": null,
  "kid": "tnydxwrdKX5Vlb6cLmgx1zTxt/GRcLtjNpJlR+jaqeU=",
  "source": "IMPORTED",
  "activatedAt": null,
  "hasCsr": false,
  "publicKeyFingerprint": "41:D1:C6:A4:…:9F:2F",
  "certificate": {
    "subject": "CN=idp.example.com,O=Example",
    "issuer": "CN=Example Issuing CA",
    "serialNumber": "1a1454db2d3",
    "notBefore": 1792077698000,
    "notAfter": 1823613698000,
    "sha256Fingerprint": "66:A8:B6:40:…:3B:B6",
    "selfSigned": false,
    "chainLength": 2,
    "expiry": "OK"
  }
}

certificate.expiry is OK, EXPIRING (within 30 days) or EXPIRED.

Have HelixIAM create the key and a CSR

The private key never leaves the server: HelixIAM generates it (RSA-2048 for RS256, P-256 for ES256), stores it as PENDING and returns a PKCS#10 CSR (signed with SHA256withRSA / SHA256withECDSA, with a keyUsage=digitalSignature request and the DNS names as subjectAltName). The subject may use the attributes CN, O, OU, C, L, ST, emailAddress, SERIALNUMBER, DC and UID (no raw OIDs, no #hex values); C must be a two-letter country code.

curl -s -b cookies.txt -X POST -H "X-XSRF-TOKEN: $CSRF" -H "Content-Type: application/json" \
  -d '{"algorithm":"RS256","subject":"CN=idp.example.com, O=Example, C=NL","dnsNames":["idp.example.com"]}' \
  "$HELIX_URL/admin/realms/$REALM/keys/csr" | tee csr-response.json | jq -r .csrPem > idp.csr
KEY_ID=$(jq -r .key.keyId csr-response.json)

Send idp.csr to your CA. You can download the CSR again later:

curl -s -b cookies.txt "$HELIX_URL/admin/realms/$REALM/keys/$KEY_ID/csr" | jq -r .csrPem

When the signed certificate comes back, attach it (leaf first, then the issuing CA certificates). The leaf must be for this key; the same chain rules as for an import apply:

jq -n --rawfile chain signed-chain.pem '{certificateChainPem: $chain}' |
curl -s -b cookies.txt -X PUT -H "X-XSRF-TOKEN: $CSRF" -H "Content-Type: application/json" \
  --data-binary @- "$HELIX_URL/admin/realms/$REALM/keys/$KEY_ID/certificate"

You can also attach a renewed certificate to the active key (same key, new certificate). Tokens do not change; the SAML metadata and signatures switch to the new certificate at once.

Activate a staged key

Wait until your relying parties have picked up the PENDING key (OIDC verifiers that read the live JWKS do so within their cache time; SAML SPs when they re-read metadata), then:

curl -s -b cookies.txt -X POST -H "X-XSRF-TOKEN: $CSRF" \
  "$HELIX_URL/admin/realms/$REALM/keys/$KEY_ID/activate"

The key becomes ACTIVE; the previous ACTIVE key of the same algorithm (RSA or EC) becomes ROTATED and stays published. Only a PENDING key can be activated, and an ES256 key only while an RS256 key is active (anything else is 409 with the reason). Activations, rotations and retirements of one realm are serialised, so two concurrent activations never leave two active keys. With several replicas, each replica switches within its 30-second key cache; because the new key was already published, verifiers accept tokens from either replica.

Minimum staging time. A key must have been published for helix.keys.min-staging (default 5 minutes, HELIX_KEYS_MIN_STAGING, an ISO-8601 duration such as PT10M) before it can be activated; earlier is 409 with "error": "staging". To activate anyway, add ?force=true; the audit event then records forced=true:

curl -s -b cookies.txt -X POST -H "X-XSRF-TOKEN: $CSRF" \
  "$HELIX_URL/admin/realms/$REALM/keys/$KEY_ID/activate?force=true"

Rotate (generate and activate at once)

curl -s -b cookies.txt -X POST -H "X-XSRF-TOKEN: $CSRF" -H "Content-Type: application/json" \
  -d '{"algorithm":"RS256"}' \
  "$HELIX_URL/admin/realms/$REALM/keys/rotate"

The body is optional (RS256 by default; ES256 generates an EC P-256 key, which needs an active RS256 key). Rotation replaces the active key of that algorithm and has no staging: the new key signs immediately, before any verifier has fetched it, so verifiers with a cached JWKS refetch it, and a SAML SP that pinned the certificate breaks at once. Prefer a CSR or an import plus activation. HELIX_ROTATE_REALM=<realm> still forces one RS256 rotation at startup; clear it afterwards.

Retire an old key

Once the longest-lived token signed by a retained key has expired, retire it:

# Returns 204 No Content; the ACTIVE RS256 key is refused with 409
curl -s -b cookies.txt -X DELETE -H "X-XSRF-TOKEN: $CSRF" \
  "$HELIX_URL/admin/realms/$REALM/keys/$KEY_ID"

A PENDING key can be retired too (to discard a staged key), and so can the ACTIVE ES256 key unless the realm's signing settings choose ES256 (then 409). While clients are registered for ES256 ID tokens, retiring the ACTIVE ES256 key is 409 with their number and names; add ?force=true to retire it anyway (those clients then get RS256 ID tokens; the audit event records forced=true). The ACTIVE RS256 key is never retired: OpenID Connect requires RS256. Key ids are scoped to the realm: a key id of another realm is 404 on every key route.

ES256 alongside RS256

ES256 is offered alongside RS256, never instead of it:

  • Discovery lists exactly the algorithms of the ACTIVE keys: RS256 and PS256 always, plus ES256 while an EC key is active.
  • A client gets ES256 ID tokens only when it is registered for them (idTokenSignatureAlg: "ES256" on the client, the console's ID token signature algorithm). Choosing an algorithm the realm has no ACTIVE key for is refused with 400 on idTokenSignatureAlg. If the key goes away later, ID tokens fall back to RS256 and the server logs a WARN.
  • Access tokens and the other realm-signed JWTs follow the realm's default signing algorithm.

Signing settings

GET / PUT /admin/realms/{realm}/settings/signing (console: Realm settings → Tokens → Signing):

Setting Values Applies to
defaultAlg RS256 (default), PS256, ES256 access tokens, logout tokens, delegation and workload-identity tokens, JARM
samlAlg RS256 (default), ES256 SAML assertions, responses and logout messages

Changing them needs realm-admin (reading needs manage-realm). ES256 needs an ACTIVE EC key (400 on the field otherwise). The response also lists activeAlgorithms:

curl -s -b cookies.txt -X PUT -H "X-XSRF-TOKEN: $CSRF" -H "Content-Type: application/json" \
  -d '{"defaultAlg":"ES256","samlAlg":"RS256"}' \
  "$HELIX_URL/admin/realms/$REALM/settings/signing"
{ "defaultAlg": "ES256", "samlAlg": "RS256", "activeAlgorithms": ["RS256", "PS256", "ES256"] }

Realm exports do not include the signing settings yet; set them again on the target.

Certificate expiry

  • Import and attach refuse expired or not-yet-valid certificates.
  • HelixIAM checks ACTIVE and PENDING keys with a certificate one minute after start-up and every 6 hours (HELIX_KEYS_EXPIRY_CHECK_INTERVAL_HOURS; HELIX_KEYS_EXPIRY_CHECK_ENABLED=false switches it off). While a certificate expires within 30 days, or has expired, it writes one audit event per key per day — REALM_KEY_CERTIFICATE_EXPIRING or REALM_KEY_CERTIFICATE_EXPIRED, actor system, with key id, algorithm, fingerprint, notAfter and days left — and a WARN log line. With several replicas only one of them warns.
  • The realmSigningKeys health indicator (part of /actuator/health/readiness) reports the counts as details. It is always UP: certificate expiry never takes a pod out of rotation.
  • An expired ACTIVE certificate does not stop signing. Token signatures do not depend on certificate validity, and stopping would take every login down. The realm keeps signing, warns daily and shows a red banner in the console. SAML SPs that check certificate validity reject the realm's assertions until you attach a renewed certificate or activate a new key.

Audit

Every key write is audited with the key id, algorithm and SHA-256 fingerprint (of the certificate, or of the public key when there is none) — never key material: REALM_KEY_IMPORT, REALM_KEY_CSR, REALM_KEY_CERTIFICATE_ATTACH, REALM_KEY_ACTIVATE (with forced=true for a forced activation), REALM_KEY_ROTATE, REALM_KEY_RETIRE. Should a realm ever have two ACTIVE keys of one algorithm (for example after manual SQL), HelixIAM signs with the newest, publishes the other for verification only, logs an ERROR and writes REALM_KEY_DUPLICATE_ACTIVE — it never stops signing.

Realm export and import

Realm exports contain no signing keys and no certificates: private keys never leave the server, and a certificate without its key is useless on the target. An imported realm generates a fresh key on first use. To use your own key on the target, import the key and certificate there again (with its own key — the same key cannot be stored twice).

What you cannot do today

  • HSM and KMS are opt-in per realm. By default (the Standard profile) keys are software keys in the database, encrypted with DB_ENCRYPTION. To keep a realm's keys in a PKCS#11 HSM or in Vault / OpenBao Transit, see Key custody.
  • No scheduled rotation. Rotation and activation are manual (console or API).
  • No ACME or other automatic certificate renewal.
  • Algorithms: RS256 (with its PS256 twin) and ES256 only; RSA keys up to 4096 bits; no RSA-PSS-only keys, no P-384/P-521, no EdDSA.
  • No encrypted private-key PEM on import.
  • Rolling upgrade: activate an ES256 key only after every replica runs this release; an older replica cannot load an EC key.

Full endpoint set

Method & path (under /admin/realms/{realm}) Purpose
GET /keys List keys with status, algorithm, kid and certificate facts
GET /keys/access {"canManage": true|false}: whether the signed-in admin holds realm-admin
POST /keys/import Import a private key + certificate chain as PENDING
POST /keys/csr Generate a PENDING key and return a PKCS#10 CSR; optional purpose (all, access, id, saml) for a custody realm
GET /keys/{keyId}/csr Download the stored CSR again
PUT /keys/{keyId}/certificate Attach a certificate chain to a PENDING or ACTIVE key
POST /keys/{keyId}/activate Activate a PENDING key (the ACTIVE key of its algorithm becomes ROTATED); ?force=true skips the staging time
POST /keys/rotate Generate and activate a new key at once (optional {"algorithm"})
DELETE /keys/{keyId} Retire a PENDING or ROTATED key, or the ACTIVE ES256 key
GET /settings/signing · PUT /settings/signing The realm's signing algorithms

Every write under /keys, and PUT /settings/signing, needs realm-admin; reads need manage-realm.

See also