Skip to content

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).