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 withDB_ENCRYPTION. Imported and CSR keys are refused whenDB_ENCRYPTIONis 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'
KeyInfocarry 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 exampleopenssl 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 havedigitalSignatureif it has a keyUsage extension. Every following certificate must be a CA (CA:true, andkeyCertSignif 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:
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:
RS256andPS256always, plusES256while 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 with400onidTokenSignatureAlg. 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"
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=falseswitches it off). While a certificate expires within 30 days, or has expired, it writes one audit event per key per day —REALM_KEY_CERTIFICATE_EXPIRINGorREALM_KEY_CERTIFICATE_EXPIRED, actorsystem, with key id, algorithm, fingerprint,notAfterand days left — and a WARN log line. With several replicas only one of them warns. - The
realmSigningKeyshealth indicator (part of/actuator/health/readiness) reports the counts as details. It is alwaysUP: 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.