Key storage and HSMs
Every organization signs its tokens (OpenID Connect ID tokens, access tokens, SAML assertions, agent tokens, and signed authorization requests and responses) with its own key. An HSM (hardware security module) is a device that holds a private key and performs signatures without ever releasing the key. By default LumoAuth generates and stores that key itself, encrypted at rest with the platform encryption key. If your compliance regime says the private key must live in hardware you control, point the key at an external provider instead.
Configure under Organization portal → Security → Signing keys →
Generate new key, or at /orgs/{org}/portal/signing-keys. The Generate signing key form asks for the Algorithm (RSA, ECDSA or RSA-PSS and its variant) and, under Key storage, the provider that holds the private key; selecting an external provider reveals its fields. Activation and rotation holds the Sign new tokens with this key switch and the Rotate after period. Finish with Generate key.


Providers
| Provider | Where the private key lives | Signing happens | Notes |
|---|---|---|---|
| Local | LumoAuth database, encrypted with the platform encryption key | In LumoAuth | Default; no external dependency |
| Azure Key Vault | Azure Key Vault (software or HSM-backed) | In Azure | |
| AWS KMS | AWS KMS | In AWS | |
| Google Cloud KMS | Cloud KMS | In GCP | |
| HashiCorp Vault | Vault transit engine | In Vault | |
| Akeyless | Akeyless DFC | In Akeyless | |
| AWS CloudHSM | Your CloudHSM cluster, via a KMS custom key store | In the HSM | FIPS 140-3 L3, single-tenant |
| Azure Managed HSM | Managed HSM pool | In the HSM | FIPS 140-3 L3, single-tenant |
| Thales CipherTrust | CipherTrust Manager appliance | In the appliance | Keys created unexportable |
| Fortanix DSM | Fortanix DSM / SmartKey | In the HSM | SaaS (US/EU/UK) or self-hosted |
| Entrust KeyControl | KeyControl Vault | In the vault | Keys created non-extractable |
| Oracle OCI Vault | OCI Vault | In OCI | HSM or SOFTWARE protection mode |
| IBM Key Protect (also HPCS) | Wrapped by your root key; see the trade-off | In LumoAuth | Envelope encryption, not remote signing |
| OpenBao | OpenBao transit engine | In OpenBao | Vault-compatible API |
| PKCS#11 HSM (through a signing sidecar) | Any PKCS#11 HSM | In the HSM | Generic escape hatch; see below |
All nine algorithms are available (RS256/384/512, PS256/384/512,
ES256/384/512), except Oracle OCI Vault which supports exactly the same
set through its own algorithm names.
Rules that apply to every external provider
- Credentials are encrypted at rest and are never rendered back into the form.
- Endpoints must be public HTTPS hosts. Every call runs through the SSRF guard and pins the resolved IP, so a hostile DNS answer cannot redirect the request to an internal address between the check and the connection.
- Private keys are never imported. Keys are generated inside the provider
and marked non-extractable wherever the API allows it.
importKey()is deliberately refused by the HSM providers. - Only the public key is stored in LumoAuth, alongside the provider's key identifier. The JWKS (the list of public keys applications use to verify tokens) and verification therefore keep working even if the provider is briefly unreachable; signing does not.
- Losing access revokes signing. Deleting or disabling the key at the provider immediately stops LumoAuth from issuing new tokens for that key — this is the point of the feature ("crypto-shredding"), so keep a second, non-primary key if you want a fallback.
Per-provider setup
AWS CloudHSM (KMS custom key store)
Runs the standard KMS API (CreateKey, GetPublicKey, Sign, Verify,
ScheduleKeyDeletion) with CustomKeyStoreId and Origin: AWS_CLOUDHSM on
every create, so key material is generated inside your CloudHSM cluster and
never exists outside it. Requests are SigV4-signed.
| Field | Value |
|---|---|
| Region | e.g. us-east-1 |
| Access key ID / secret access key | IAM principal with kms:CreateKey, kms:CreateAlias, kms:GetPublicKey, kms:Sign |
| Session token | Only for temporary STS credentials |
| Custom key store ID | cks-…, must be CONNECTED |
Deletion honours KMS's 7–30 day pending window.
Azure Managed HSM
Wire-compatible with Azure Key Vault (REST API version 7.4) with three
differences: the vault URL is https://{pool}.managedhsm.azure.net, the Entra
token audience is https://managedhsm.azure.net/.default, and key types are the
HSM-protected RSA-HSM / EC-HSM variants.
| Field | Value |
|---|---|
| HSM pool name | e.g. contoso-hsm (or paste the full vault_url) |
| Organization ID / client ID / client secret | Service principal holding the Managed HSM Crypto User local RBAC role |
Thales CipherTrust Manager
| Field | Value |
|---|---|
| CipherTrust Manager URL | https://ciphertrust.example.com |
| Username + password, or API key | CipherTrust 2.10+ supports the api_key grant; older appliances use the password grant |
| Domain | Optional CipherTrust domain |
LumoAuth creates keys with usageMask sign+verify and unexportable: true, and
signs a pre-computed digest via /api/v1/crypto/sign. ECDSA signatures come back
ASN.1/DER and are converted to the raw R‖S form JWS requires.
Fortanix DSM
| Field | Value |
|---|---|
| DSM endpoint | https://apps.smartkey.io (EU: eu.smartkey.io, UK: uk.smartkey.io) or your own |
| App API key | The base64 app_id:secret shown in the DSM UI |
| Group ID | Optional; defaults to the app's group |
The app authenticates with Basic <api_key> to /sys/v1/session/auth and uses
the returned bearer for the key operations. RSA padding is selected with DSM's
mode object (PKCS1_V15 or PSS with MGF1).
Entrust KeyControl
| Field | Value |
|---|---|
| KeyControl Vault URL | https://keycontrol.example.com |
| Username / password | Vault user with key create + sign rights |
| Vault ID | Optional; scopes the login to one vault |
Uses the KeyControl Vault REST API (/vault/1.0/Login/, CreateKey, Sign,
Verify, DeleteKey) with non-extractable keys. If your deployment only exposes
the KMIP edition, use the PKCS#11 sidecar with Entrust's PKCS#11 library instead.
Oracle OCI Vault
Two endpoints from the OCI vault page are needed, because OCI splits management from crypto:
| Field | Value |
|---|---|
| Management endpoint | https://…-management.kms.<region>.oraclecloud.com |
| Crypto endpoint | https://…-crypto.kms.<region>.oraclecloud.com |
| Tenancy / user OCID, fingerprint, private key (PEM) | The API signing key |
| Compartment OCID | Where the key is created |
| Protection mode | HSM (default) or SOFTWARE |
Requests carry an OCI Signature version="1" header over
date (request-target) host content-length content-type x-content-sha256.
Signing sends the digest with messageType: DIGEST. Deleting schedules
deletion 7 days out (forced) or 30 days out.
OpenBao
OpenBao is the Linux Foundation fork of HashiCorp Vault and keeps the transit
engine byte-for-byte compatible, so the configuration is identical to the
HashiCorp Vault provider. It exists as a separate choice so that audits and
inventories can tell the two products apart.
| Field | Value |
|---|---|
| OpenBao address | https://openbao.example.com |
| Token or AppRole role ID + secret ID | |
| Mount path | Default transit |
IBM Key Protect / HPCS (wrap mode)
This provider uses envelope encryption, not remote signing. The signing key is generated locally and the private key is wrapped by your root key; at sign time it is unwrapped into LumoAuth's memory. It is the only external provider where the private key is briefly present in the application process.
Key Protect standard keys and the HPCS key-management REST API can wrap and unwrap data keys with a root key held in a FIPS 140-2 Level 4 HSM, but they do not expose an asymmetric Sign API over REST. HPCS signing is only reachable through GREP11 (Enterprise PKCS#11 over gRPC), which this PHP service cannot speak without a native gRPC extension. So:
- The key pair is generated locally with OpenSSL at creation time.
- The private key PEM is wrapped through
POST /api/v2/keys/{rootKeyId}/actions/wrap, and only the wrapped blob — additionally sealed with the platform encryption key — is stored. - At sign time the blob is unwrapped through
/actions/unwrap, held in memory for the lifetime of the PHP process only, and the signature is computed locally.
What you get compared with the Local provider: the key is protected by a customer-controlled root key with IBM's Activity Tracker audit trail, and disabling or deleting that root key immediately revokes LumoAuth's ability to sign. What you give up compared with a true HSM signing provider: the private key is briefly in LumoAuth memory.
If you need a pure-HSM signing path on IBM Cloud, use the PKCS#11 sidecar with the HPCS PKCS#11 library instead.
| Field | Value |
|---|---|
| IAM API key | Writer/Manager on the Key Protect instance |
| Instance ID | Key Protect / HPCS instance GUID (bluemix-instance header) |
| Region | e.g. us-south → https://us-south.kms.cloud.ibm.com |
| Endpoint | Optional override, e.g. HPCS https://api.<region>.hs-crypto.cloud.ibm.com:<port> |
| Root key ID | Optional; a root key is created when empty |
| Key ring ID | Optional |
Generic PKCS#11 via a signing sidecar
PHP cannot load a PKCS#11 module, so any HSM with a PKCS#11 library — Entrust
nShield, Thales Luna, Utimaco, YubiHSM 2, the AWS CloudHSM client, SoftHSM2 for
development — is reached through a small sidecar that owns the PKCS#11 session.
A reference implementation (Python, python-pkcs11, standard-library
http.server) ships in scripts/pkcs11-sidecar/ with its own README,
requirements.txt and a systemd unit.
LumoAuth ──HTTPS + Bearer (optionally mTLS)──► sidecar ──PKCS#11──► HSM
Sidecar HTTP contract
Every call carries Authorization: Bearer <token> and speaks JSON. alg is
always the JOSE algorithm (RS256…ES512).
| Method | Path | Request | Success |
|---|---|---|---|
GET | /v1/health | – | 200 {"status":"ok","token_label":"…","slot":0,"mechanisms":[…]} |
POST | /v1/keys | {"alg":"RS256","key_size":3072,"label":"lumoauth-ab12"} | 201 {"key_id":"…","public_key_pem":"-----BEGIN PUBLIC KEY-----…"} |
POST | /v1/keys/{id}/sign | {"alg":"RS256","data_b64":"<base64 signing input>"} | 200 {"signature_b64":"…"} |
POST | /v1/keys/{id}/verify | {"alg":"…","data_b64":"…","signature_b64":"…"} | 200 {"valid":true} |
DELETE | /v1/keys/{id} | – | 204 |
Errors are {"error":"…"} with 400, 401, 404, 500 or 501.
Three details are load-bearing:
data_b64is the unhashed signing input. The sidecar chooses the PKCS#11 mechanism (CKM_SHA256_RSA_PKCS,CKM_SHA256_RSA_PKCS_PSS,CKM_ECDSAover a locally computed digest), so the HSM decides how the digest is produced.- Signatures come back in JWS form — raw
R‖SforES*(which is what PKCS#11 returns natively) and MGF1/salt-length-equals-digest-length forPS*. LumoAuth tolerates a DER ECDSA signature and converts it, but a compliant sidecar should not send one. /v1/keys/{id}/verifyis optional. Answer501(or404) and LumoAuth verifies locally against the stored public key instead.
key_id is whatever the sidecar returns (the reference implementation uses the
hex-encoded CKA_ID); LumoAuth stores it as the key's external key id.
Configuring the provider
| Field | Value |
|---|---|
| Sidecar base URL | https://hsm-sidecar.internal:8443 — HTTPS only |
| Bearer token | The sidecar's shared secret |
| Client certificate / key path | Optional mTLS pair, absolute paths on the LumoAuth host |
| CA certificate path | Optional, pins the sidecar's issuer |
A sidecar normally sits on a private network, which the default SSRF guard
rejects. Set PKCS11_SIDECAR_ALLOW_PRIVATE=true on the LumoAuth host to allow
it; HTTPS is still enforced, the URL must have a host and may not carry
credentials. mTLS paths must be absolute and free of .., and the certificate
and key must be supplied together.
Rotation, health and troubleshooting
- Rotate with Rotate key on the signing keys page. A rotation creates a new key at the provider; the old key stays usable for verification during the grace period, so tokens already issued keep validating.
- Provider validation runs when you generate a key: the form checks the
provider connection before anything is created. Reachability problems
surface as
connection_failed, bad credentials asauthentication_failed, and a deleted or disabled key askey_not_found. - JWKS keeps serving the public key even while the provider is unreachable; only new signatures fail, so relying parties are not immediately affected.
- If a provider's REST schema differs from what LumoAuth expects (a KMIP-only KeyControl edition, an older CipherTrust appliance), fall back to the PKCS#11 sidecar with the vendor's own PKCS#11 library.
Standards used on this page
| Plain name | Standard |
|---|---|
| JSON Web Key (JWK) and key sets (JWKS) | RFC 7517 |
See the standards reference for what each one covers and how LumoAuth uses it.