Kubernetes and OpenShift¶
PKIFactor ships a Helm chart that runs the same signed images as the appliance. It targets OpenShift first (restricted security context, arbitrary user ID, routes) and also works on Kubernetes.
Scope of the chart
The chart deploys one replica of each service. Database high availability is provided by your PostgreSQL (a managed service or an operator such as Crunchy or CloudNativePG): set postgresql.deployInCluster=false and point the chart at it. The in-cluster PostgreSQL of the chart is meant for evaluations.
What you receive¶
The OpenShift bundle pkifactor-openshift-vX.Y.Z.tar.gz, built for disconnected clusters:
| Item | Content |
|---|---|
images/ |
The product images, identical to the signed release (release-manifest.json lists the digests) |
chart/ |
The Helm chart |
load-images.sh |
Pushes the images to your internal registry (skopeo, or podman as a fallback) |
values-airgap.yaml.example |
Values to adapt: registry, database, KMS, routes |
cosign.pub |
Public key to verify the image signatures |
1. Load the images into your registry¶
skopeo login registry.example.com
./load-images.sh registry.example.com/pkifactor
Optionally verify a signature:
cosign verify --key cosign.pub --insecure-ignore-tlog=true registry.example.com/pkifactor/pkifactor-backend:v1.8.49
2. Prepare the namespace and the secrets¶
oc new-project pkifactor
# TLS certificate served by nginx (keys clm.crt and clm.key), covering the route host names
oc create secret generic pkifactor-tls --from-file=clm.crt=tls/clm.crt --from-file=clm.key=tls/clm.key
Generate the application secrets once and keep them in your vault:
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # dbEncryptionKey
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # kekCacheKey
openssl rand -hex 32 # jwtSecretKey
openssl rand -hex 32 # sessionSecretKey
openssl rand -hex 24 # estProxySecret
openssl rand -hex 24 # mtlsProxySecret
3. Write the values¶
image:
registry: registry.example.com/pkifactor
tag: v1.8.49
postgresql:
deployInCluster: false
host: pg.example.com
name: clm_db
appUser: clm_app # owner of clm_db, NOSUPERUSER
kms:
provider: vault-transit # or pkcs11-hsm
cacheEnabled: true
vault:
addr: https://vault.example.com:8200
keyName: zcert-kek
persistence:
keys:
type: pvc # the data-key store must survive pod restarts
route:
host: clm.example.com
acmeHost: acme.example.com
estHost: est.example.com
tls:
termination: passthrough
nginx:
tlsSecret: pkifactor-tls
components:
acme: true
est: true
cmp: false
scep: false
urls:
base: https://clm.example.com
publicAcme: https://acme.example.com
Pass the secrets in a separate file that you do not commit (or reference existing Secrets, see below):
secrets:
dbPassword: "<clm_app password>"
dbEncryptionKey: "<Fernet key>"
jwtSecretKey: "<hex>"
sessionSecretKey: "<hex>"
estProxySecret: "<hex>" # proof that nginx relays the client certificate to EST
mtlsProxySecret: "<hex>" # same proof for the interface and the API (certificate sign-in)
kekCacheKey: "<Fernet key>"
vaultRoleId: "<AppRole role_id>"
vaultSecretId: "<AppRole secret_id>"
4. Install¶
helm install pkifactor ./chart -n pkifactor -f values.yaml -f secrets.yaml
oc -n pkifactor rollout status deploy/pkifactor-pkifactor-core
oc -n pkifactor logs deploy/pkifactor-pkifactor-core -c backend | grep -E "KeyManager ready|INITIAL ADMIN"
The backend logs KeyManager ready | mode=vault-transit and, at the first start, the initial administrator banner (see Quick start).
How secrets reach the pods¶
| Secret | Kubernetes object | Delivered as |
|---|---|---|
Database password, data encryption key, token keys, gateway API keys, proxy secrets (EST_PROXY_SECRET, MTLS_PROXY_SECRET) |
<release>-secret |
environment (envFrom); nginx reads the two proxy secrets from the same Secret |
| KMS credentials (Vault AppRole, or HSM user PIN) | <release>-kms |
files under /run/pkifactor-secrets (*_FILE variables), never in the environment |
| Key of the encrypted data-key cache | <release>-kek-cache |
file, in its own Secret, separate from the KMS credentials |
If you leave estProxySecret and mtlsProxySecret empty, the chart generates them at the first install and reuses them at each upgrade. This reuse needs access to the cluster: with GitOps or helm template, provide both values, otherwise they change at every rendering. An existingSecret must contain both keys. Before version 1.8.50, the chart did not pass these secrets and nginx did not start.
To keep these values out of Helm entirely, create the Secrets yourself (for example with the Secrets Store CSI driver backed by Vault) and reference them: existingSecret, existingKmsSecret, existingKekCacheSecret.
Why not /run/secrets
On OpenShift, CRI-O mounts its own content (subscription data, service account) under /run/secrets, which hides a volume mounted there. The chart therefore uses /run/pkifactor-secrets.
Encryption at rest¶
The backend refuses to start without its key-encryption service (fail-closed). With kms.cacheEnabled=true, an encrypted cache of the data keys lets it keep reading data if Vault or the HSM is briefly unavailable; writes (issuance, revocation) pause until the KMS is back.
Moving an existing installation from none to Vault Transit¶
The data-key store records which provider wrapped the keys, so the migration is done in three steps without downtime of the data:
-
Upgrade with a persistent volume for the keys, still with
kms.provider=none:helm upgrade pkifactor ./chart -n pkifactor --reuse-values --set persistence.keys.type=pvc -
Wrap the existing data key with Vault, from the backend pod (credentials read from standard input):
printf '%s\n%s\n' "$ROLE_ID" "$SECRET_ID" | oc -n pkifactor exec -i deploy/pkifactor-pkifactor-core -c backend -- sh -c ' read RID; read SID export PYTHONPATH=/app:/app/backend KEK_PROVIDER=vault-transit KEK_VAULT_ADDR=https://vault.example.com:8200 \ KEK_KEY_NAME=zcert-kek KEK_VAULT_ROLE_ID=$RID KEK_VAULT_SECRET_ID=$SID cd /app && python -m backend.utils.kms_admin migrate-from-env && python -m backend.utils.kms_admin status' -
Switch the provider:
helm upgrade pkifactor ./chart -n pkifactor --reuse-values -f secrets.yaml \ --set kms.provider=vault-transit --set kms.vault.addr=https://vault.example.com:8200 \ --set kms.vault.keyName=zcert-kek --set kms.cacheEnabled=true
--reuse-values and new chart defaults
With --reuse-values, Helm keeps the values of the previous release and ignores the defaults of the new chart. Set explicitly any value that a newer chart introduced (for example kms.cacheEnabled).
The Vault side needs a Transit key and an AppRole whose policy allows only encrypt, decrypt and rewrap on that key (see Encryption at rest, KMS and HSM). Binding the AppRole's secret_id and tokens to the cluster's egress address is recommended.
Upgrades¶
./load-images.sh registry.example.com/pkifactor # new bundle
helm upgrade pkifactor ./chart -n pkifactor --reuse-values --set image.tag=v1.8.49
Database migrations run at the backend start. Check oc logs ... -c backend for Application startup complete and the KMS mode.
Values reference¶
| Value | Default | Meaning |
|---|---|---|
image.registry, image.tag, image.tags.<component> |
Where to pull the images; a per-component tag overrides the common one | |
components.acme, est, cmp, scep |
true, false, false, false |
Protocol gateways to deploy |
postgresql.deployInCluster |
true |
false for your own PostgreSQL (host, port, name, appUser) |
postgresql.storage.type |
emptyDir |
pvc to keep the in-cluster database across restarts |
kms.provider |
vault-transit |
vault-transit or pkcs11-hsm |
kms.cacheEnabled |
true |
Encrypted cache of the data keys (requires secrets.kekCacheKey or existingKekCacheSecret) |
kms.vault.addr, keyName, skipVerify |
Vault Transit address and key; skipVerify only for laboratories |
|
kms.pkcs11.* |
HSM library path, slot, key label; the vendor library is provided through librarySecret |
|
persistence.keys.type |
emptyDir |
Use pvc with a KMS |
route.enabled |
true |
false to create no OpenShift Route (expose nginx with your own Ingress) |
route.host, acmeHost, estHost, cmpHost, scepHost |
Host names of the virtual hosts. route.host empty lets OpenShift choose the name. estHost, cmpHost and scepHost each create a dedicated Route when the gateway is deployed; acmeHost names the ACME virtual host without creating a Route of its own |
|
route.tls.termination |
reencrypt |
Applies to every Route of the chart. passthrough keeps client certificates (mTLS, EST) end to end |
proxy.https, proxy.noProxy |
Outbound proxy for public authorities | |
extraConfig |
Additional non-secret environment, for example ENROLL_RATE_LIMIT: "60/minute" |