Skip to content

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

values.yaml
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.yaml
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:

  1. 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
    
  2. 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'
    
  3. 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"