Docker Compose on an existing host¶
The appliance is Docker Compose. If you already operate Docker hosts, use the same package on one of them: the provisioner keeps the Docker Engine already installed and only adds the PKIFactor files, images and services. Follow Appliance installation; everything on that page applies.
This page is for teams that manage the Compose files themselves (configuration management, their own directory layout). It describes what the installation produces, so that you can reproduce it.
Layout¶
/opt/pkifactor/
├── .env configuration (mode 640)
├── docker-compose.yml single-node service definitions (copy of docker-compose.dist.yml from the package)
├── docker-compose.ha-node.yml high-availability service definitions
├── secrets/ one file per secret (directory 750)
├── db/ database initialization scripts (mounted by Compose)
├── nginx/ssl/ TLS certificate of the platform (clm.crt, clm.key, chain)
└── data/{keys,license,backups}
Secrets¶
Secrets are files, mounted read-only into the containers that need them, never variables in .env. A value set in the environment would take precedence over the file.
| File | Content | Required |
|---|---|---|
db_encryption_key |
Fernet key protecting data at rest | yes; back it up |
jwt_secret_key |
key signing session tokens | yes |
session_secret_key |
key of the web interface session | yes |
kek_cache_key |
Fernet key of the encrypted data-key cache | yes |
kek_vault_role_id, kek_vault_secret_id, kek_vault_token |
Vault credentials, when the KMS is Vault Transit | empty file otherwise |
kek_pkcs11_pin |
HSM user PIN, when the KMS is an HSM | empty file otherwise |
jwt_secret_previous |
previous token key, during a rotation | empty file otherwise |
Every file must exist, even empty: Docker refuses to start a service whose secret file is missing. pkifactor-ctl creates missing files automatically before each operation.
cd /opt/pkifactor && sudo install -d -m 750 secrets
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode(), end='')" | sudo tee secrets/db_encryption_key >/dev/null
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode(), end='')" | sudo tee secrets/kek_cache_key >/dev/null
openssl rand -hex 32 | tr -d '\n' | sudo tee secrets/jwt_secret_key >/dev/null
openssl rand -hex 32 | tr -d '\n' | sudo tee secrets/session_secret_key >/dev/null
for f in kek_vault_role_id kek_vault_secret_id kek_vault_token kek_pkcs11_pin jwt_secret_previous; do sudo touch secrets/$f; done
sudo chmod 444 secrets/*
.env¶
| Variable | Example | Meaning |
|---|---|---|
VERSION |
v1.8.49 |
Tag of every image; required |
CLM_DOMAIN, FRONTEND_BASE_URL |
clm.example.com, https://clm.example.com |
Public name and address |
ACME_DOMAIN, EST_DOMAIN, CMP_DOMAIN, SCEP_DOMAIN |
acme.example.com... |
Protocol names |
ACME_ENABLED, EST_ENABLED, CMP_ENABLED, SCEP_ENABLED |
true |
Exposes each gateway through nginx (and selects its Compose profile when you use pkifactor-ctl) |
DB_USER, DB_PASSWORD, DB_NAME |
clm_user, random, clm_db |
Database administration account (migrations, backups) |
DB_APP_USER, DB_APP_PASSWORD |
clm_app, random |
Application role without superuser rights, used by all services. Without these two variables the services fall back to the administration account |
ACME_INTERNAL_TOKEN, EST_INTERNAL_TOKEN, CMP_BACKEND_API_KEY, SCEP_BACKEND_API_KEY |
random | API keys of the gateways' service accounts |
NGINX_HTTP_PORT, NGINX_HTTPS_PORT |
80, 443 |
Published ports |
SSL_CERTS_PATH |
/opt/pkifactor/nginx/ssl |
TLS certificate directory |
DEFAULT_ADMIN_EMAIL |
pki-admins@example.com |
First administrator (default admin@zetacert.com) |
KEK_PROVIDER |
empty, vault-transit or pkcs11-hsm |
Key-encryption provider (Encryption at rest) |
PKIFACTOR_SUBNET, PKIFACTOR_NGINX_IP, PKIFACTOR_FRONTEND_IP |
172.18.0.0/16, 172.18.0.10, 172.18.250.11 |
Docker network of the platform and the fixed addresses of nginx and of the web interface in it. Change them together, and only if this range is already used on the host |
TRUSTED_PROXY_NETWORKS |
empty | Networks allowed to relay the client address and the client-certificate headers. By default, the fixed addresses of nginx and of the web interface (/32); change it together with the addresses above |
MTLS_PROXY_SECRET |
random, 32 bytes or more | Proof that the client-certificate headers and the client address come from nginx. Pass it to the nginx service as well (backend and frontend read .env). Without it, these headers are trusted on the source address alone; set it in every installation, and on every node in high availability |
EST_PROXY_SECRET |
random, 32 bytes or more | Same proof for the EST gateway. Pass it to the nginx and EST services. Without it, EST refuses client-certificate authentication |
HTTPS_PROXY, NO_PROXY |
Outbound proxy | |
WEB_WORKERS |
2 |
Number of API processes of the backend; adjust to the number of vCPU. Keep WEB_WORKERS × DB_POOL_SIZE + WORKER_DB_POOL_SIZE (and their overflows) under the connection limit of PostgreSQL (100 by default) |
DB_POOL_SIZE, DB_POOL_MAX_OVERFLOW, WORKER_DB_POOL_SIZE, WORKER_DB_POOL_MAX_OVERFLOW |
8, 8, 10, 10 |
Database connection pools of the API and of the background worker |
ENROLL_RATE_LIMIT |
30/minute |
Enrollment requests allowed per client address on the EST, SCEP and CMP gateways |
CSP_STRICT |
empty | The strict content security policy of the interface is on by default; false turns it off, as a temporary workaround only |
CBOM_ENABLED |
empty (false) |
true shows the cryptographic inventory entries of the interface (Crypto Inventory, Crypto report, CBOM, Crypto catalogue) |
Start¶
cd /opt/pkifactor
sudo docker compose --profile acme --profile est --profile cmp --profile scep up -d
sudo docker compose logs backend | grep -A8 "INITIAL ADMIN PASSWORD"
The database initialization scripts create the application role and the ACME and EST schemas at the first start; the backend applies its migrations at every start. The initial administrator banner is printed only when DEFAULT_ADMIN_PASSWORD is empty (Quick start).