Encryption at rest, KMS and HSM¶
PKIFactor encrypts every sensitive value it stores: credentials of authorities, devices and directories, private keys it generated on the server side, API keys, secrets of protocol gateways. This page explains the key hierarchy and how to place its root in a key management service (Vault Transit) or a hardware security module (PKCS#11).
Key hierarchy¶
KEK (key-encryption key) never leaves the KMS or the HSM
| wraps / unwraps
DEKs (data-encryption keys) stored wrapped in data/keys/dek_store.json
| encrypt / decrypt (Fernet, AES-128-CBC + HMAC-SHA256)
data in the database
| Mode | Where the root lives | Use |
|---|---|---|
none |
The data key itself, in the db_encryption_key secret file |
Evaluation, laboratories |
vault-transit |
A Transit key in HashiCorp Vault (zcert-kek by default) |
Production |
pkcs11-hsm |
A non-extractable AES-256 key in an HSM slot (any PKCS#11 HSM; tested with Utimaco CryptoServer and SoftHSM) | Production with hardware protection |
AWS KMS, Azure Key Vault and Google Cloud KMS appear in the provider list as coming soon: they are not available in this version.
Several DEKs can coexist: after a rotation, the newest encrypts new data and the older ones still decrypt existing data.
Behaviour when the KMS is unavailable¶
- At start-up: PKIFactor retries three times (30 seconds apart). If the KMS still does not answer and an encrypted cache of the DEKs exists, it starts in degraded mode: everything can be read, but writes that need new secrets (issuance, revocation) pause until the KMS returns. Without a cache it refuses to start (fail-closed): it never falls back silently to a key stored next to the data.
- At run time: a KMS outage switches to degraded mode; PKIFactor leaves it by itself when the KMS answers again (or on Reload Keys).
- The cache is encrypted with its own key (
kek_cache_key), distinct from the KMS credentials.
/health/ready reports degraded while the KMS is unavailable.
Configure a provider¶
In the Administration workspace, open Infrastructure > Encryption (page Encryption Providers, reserved for administrators) and click Add Provider. Give the provider a unique Name and choose its Provider Type.
The page also shows:
- Encryption Status: Mode (the provider actually in use, or Local (env var)), Active Keys, Health, Key Store, Last Rotation, Last Reload;
- Encryption Keys: the DEKs, the newest marked Primary, the older ones Decrypt;
- the list of providers with their Status (Active, Connected, Error, Unchecked) and a summary of their configuration (key, mount, slot).
Vault Transit¶
On the Vault side:
vault secrets enable transit
vault write -f transit/keys/zcert-kek type=aes256-gcm96
vault policy write pkifactor-kms - <<'EOF'
path "transit/encrypt/zcert-kek" { capabilities = ["update"] }
path "transit/decrypt/zcert-kek" { capabilities = ["update"] }
path "transit/rewrap/zcert-kek" { capabilities = ["update"] }
path "transit/keys/zcert-kek" { capabilities = ["read"] }
path "sys/health" { capabilities = ["read"] }
EOF
vault auth enable approle
vault write auth/approle/role/pkifactor-kms token_policies=pkifactor-kms \
token_ttl=1h token_max_ttl=4h secret_id_bound_cidrs=<PKIFactor address>/32 token_bound_cidrs=<PKIFactor address>/32
vault read auth/approle/role/pkifactor-kms/role-id
vault write -f auth/approle/role/pkifactor-kms/secret-id
In PKIFactor, provider type HashiCorp Vault Transit:
| Field | Content |
|---|---|
| Vault Address | Full URL of the Vault server, with its port |
| Transit Mount Path | Mount of the Transit engine (transit) |
| Encryption Key Name | The Transit key used for wrapping (zcert-kek) |
| Key Store Path | Location of the wrapped DEKs |
| AppRole Role ID / AppRole Secret ID | The AppRole credentials (recommended) |
| Token (alternative) | A Vault token, instead of the AppRole |
| CA Certificate (PEM) | The CA that signed the Vault server certificate, when it is private. Empty: the system trust store (or VAULT_CA_CERT) is used |
| Skip TLS verification | Laboratories only: disables the check of the Vault server certificate |
When its Vault token expires or is rejected, PKIFactor logs in again with the AppRole by itself.
HSM (PKCS#11)¶
The vendor PKCS#11 library is not part of the images: it is mounted read-only into the backend and worker containers (on the appliance, /opt/pkifactor/hsm-libs/<vendor>; on Kubernetes, kms.pkcs11.librarySecret). On the appliance, put the library (and its configuration file, if the vendor needs one) under /opt/pkifactor/hsm-libs/<vendor>/, declare the mount in /opt/pkifactor/docker-compose.override.yml, and point KEK_PKCS11_LIB in .env at the path inside the container:
services:
backend:
volumes:
- ./hsm-libs/utimaco-r3:/opt/utimaco-r3:ro
backend_worker:
volumes:
- ./hsm-libs/utimaco-r3:/opt/utimaco-r3:ro
KEK_PKCS11_LIB=/opt/utimaco-r3/libcs_pkcs11_R3.so
The user PIN is never written in .env: it lives in /opt/pkifactor/secrets/kek_pkcs11_pin. This layout is the one used by the test appliance with a Utimaco HSM.
In PKIFactor, provider type PKCS#11 HSM. The Quick Setup buttons prefill the library path, configuration file and authentication key file for SoftHSM2, Utimaco R2, Utimaco R3 (PQC), Thales Luna and AWS CloudHSM; check the paths against your installation.
| Field | Content |
|---|---|
| PKCS#11 Library Path | Full path of the vendor library inside the container |
| HSM Slot | Slot number. Enter the library path, then click Detect Slots to list the slots of the HSM |
| HSM PIN | The slot's user PIN, stored encrypted |
| KEK Label | Label of the AES-256 wrapping key in the HSM |
| Provider Config File | Vendor configuration file, when the vendor needs one |
| Auth Key File | HSM authentication key file (Utimaco) |
When no key carries the label, PKIFactor creates the AES-256 key-encryption key in that slot, as non-extractable. DEKs are wrapped with AES Key Wrap (RFC 3394 or RFC 5649, whichever the HSM offers).
Use a dedicated slot
Give PKIFactor its own slot and user PIN. Never point it at a slot used by an authority or another product.
Test, activate, migrate¶
| Action | Effect |
|---|---|
| Test | Checks that the provider answers: Vault health endpoint, or a session opened on the HSM slot. The status becomes Connected or Error |
| Edit | Changes the name or the configuration. A secret shown masked and left unchanged keeps its stored value |
| Activate | Marks the provider as the one to use |
| Migrate | Re-wraps every DEK with this provider (from local mode, or from another provider), then activates it and reloads the keys. The provider must pass its health check first. Existing data stays readable throughout: only the wrapping of the DEKs changes |
| Rotate Key | Creates a new DEK, wrapped by the active provider, used for new data. Not available in local mode |
| Reload Keys | Re-reads the DEK store and the provider, for example after a KMS outage |
| Delete | Removes a provider that is not active |
Every action is recorded in the audit trail. The page warns when the provider marked active differs from the one actually used by the running backend (Configuration drift): the running backend follows the KEK_* variables described below, so update them to match.
Rotation does not re-encrypt existing data: older DEKs stay available to decrypt it. To re-wrap the DEKs after rotating the key in Vault or the HSM, use the rewrap command below.
Where the credentials of the KMS are kept¶
The secret that unlocks the KMS (Vault secret ID, HSM PIN) is a file, never an environment variable: /opt/pkifactor/secrets/kek_vault_secret_id or kek_pkcs11_pin on the appliance, a dedicated Kubernetes Secret mounted as files on OpenShift. It is mounted only into the services that need it (backend, worker, ACME and EST gateways).
| Variable | Meaning |
|---|---|
KEK_PROVIDER |
vault-transit or pkcs11-hsm |
KEK_KEY_NAME |
Transit key name or HSM key label (zcert-kek) |
KEK_VAULT_ADDR, KEK_VAULT_MOUNT, KEK_VAULT_SKIP_VERIFY |
Vault address, Transit mount, TLS verification (never skip in production) |
KEK_VAULT_ROLE_ID_FILE, KEK_VAULT_SECRET_ID_FILE, KEK_VAULT_TOKEN_FILE |
Files holding the Vault credentials |
KEK_PKCS11_LIB, KEK_PKCS11_PROVIDER_CONFIG, KEK_PKCS11_SLOT, KEK_PKCS11_PIN_FILE |
HSM library, vendor configuration, slot, PIN file |
KEK_DEK_STORE_PATH |
Location of the wrapped DEKs (/app/data/keys/dek_store.json) |
KEK_CACHE_KEY_FILE |
Key of the encrypted DEK cache |
KMS_RETRY_ATTEMPTS, KMS_RETRY_DELAY |
Start-up retries (3, 30 seconds) |
Via the API
# Routes without /api prefix: called under /api/v1/
curl -s -b jar https://clm.example.com/api/v1/admin/kms-providers
curl -s -b jar https://clm.example.com/api/v1/admin/kms-providers/status
curl -s -b jar -X POST https://clm.example.com/api/v1/admin/kms-providers/3/test
curl -s -b jar -X POST https://clm.example.com/api/v1/admin/kms-providers/3/migrate
curl -s -b jar https://clm.example.com/api/v1/admin/settings/kms/status
curl -s -b jar -X POST https://clm.example.com/api/v1/admin/settings/kms/rotate
curl -s -b jar -X POST https://clm.example.com/api/v1/admin/settings/kms/reload
Command line¶
From the backend container (appliance: docker exec -it pkifactor_backend ...):
python -m backend.utils.kms_admin status # provider, KEK, DEKs
python -m backend.utils.kms_admin init # first DEK, wrapped with the configured provider
python -m backend.utils.kms_admin migrate-from-env # wrap the local key with the configured provider
python -m backend.utils.kms_admin rotate # new DEK
python -m backend.utils.kms_admin rewrap # re-wrap the DEKs with the latest KEK version
Backups¶
A backup contains the wrapped DEKs, not the KEK. Restoring it on another host needs access to the same Vault Transit key or HSM key, and the credentials to reach it. See Backup and restore.