Security architecture¶
This page describes the components of PKIFactor, the trust boundaries between them, the data flows, and where secrets and private keys are kept. It describes the single-node appliance; the high-availability layout adds database replication between nodes (High availability).
Components¶
flowchart TB
C["Browsers · API clients · ACME / EST / SCEP / CMP clients"]
N["nginx<br/>TLS termination · routing · rate limiting"]
F["Frontend<br/>(web interface)"]
B["Backend API<br/>(business logic, access control, encryption)"]
P["Protocol gateways<br/>ACME · EST · SCEP · CMP"]
W["Background worker<br/>renewals · deployment · verification · scans · notifications"]
D[("PostgreSQL 16<br/>single node or Patroni cluster")]
CA["Certification authorities<br/>Vault · AD CS · EJBCA · ZetaCA · DigiCert · Sectigo · ACME · CMP"]
DEV["Devices (deployment)<br/>F5 · FortiWeb · PAN-OS · Check Point · Kemp · IIS · SSH"]
PL["Platforms<br/>KMS / HSM · directories · SSO · ServiceNow · SIEM · SMTP"]
C --> N
N --> F
N --> B
N --> P
F --> B
P --> B
P --> D
B --> D
B --> CA
B --> PL
W --> D
W --> CA
W --> DEV
W --> PL
Overview. The backend issues directly when a request does not need approval; the worker runs everything that happens later (renewals, deployments, checks). The detailed view below shows the containers, ports and data stores.
Editable source: pkifactor-architecture.drawio (open it in draw.io / diagrams.net; two pages: architecture, and data and connectors).
| Component | Role | Runs as |
|---|---|---|
| nginx | The only container with published ports (80 redirects to 443). Terminates TLS for the interface, the API and the gateways; checks optional client certificates; rate limiting | Root in the appliance Compose file, to read the TLS keys written by the backend; the image itself runs as a non-root user (used on OpenShift) |
| Frontend | Renders the screens; holds no data; calls the backend API | Non-root user appuser |
| Backend API | Business logic, access control, encryption, connectors to authorities and devices | Starts as root to fix volume ownership, then drops to appuser |
| Worker | Same image as the backend, one process: renewals, deployments, revocation monitoring, scheduled backups, e-mail queue, SIEM replay | Same as the backend |
| ACME, EST, SCEP, CMP gateways | Separate applications, each enabled on its own; they speak the enrolment protocol and ask the backend to issue | Non-root user appuser |
| PostgreSQL 16 | One database; the ACME and EST gateways have their own schema | Not published on the host |
| KMS or HSM | Holds the key-encryption key (Vault Transit or a PKCS#11 HSM) | External to the appliance |
In single-node mode the CMP and SCEP containers also listen on 127.0.0.1 of the host (8003, 5683/udp, 8004), never on an external address; clients reach them through nginx.
Trust boundaries¶
| Boundary | What crosses it | Protection |
|---|---|---|
| Network to nginx | Every inbound request | TLS 1.2 or 1.3 with ECDHE and AES-GCM suites; optional client certificate checked against the configured CA chain; per-address request limits |
| nginx to the applications | Proxied requests, and the result of the client certificate check in X-SSL-Client-* headers |
Plain HTTP on the internal Docker network. nginx adds a shared secret (MTLS_PROXY_SECRET, EST_PROXY_SECRET for EST) that the interface, the backend and the EST gateway check before trusting the X-SSL-Client-* and X-Real-IP headers; the interface relays them to the backend only with that proof. Without the secret, the backend falls back to the trusted proxy networks (TRUSTED_PROXY_NETWORKS; nginx has a fixed address on that network) |
| Gateways to the backend | Issuance and revocation requests | Each gateway authenticates with its own API key, held by a protected service account; the backend applies the profile's allowed domains (when the filter is enabled) whatever the entry point |
| Applications to PostgreSQL | All data | Application role clm_app (see below); the database port is not published |
| Applications to the KMS or HSM | Wrap and unwrap of data keys only | Vault: HTTPS with the Vault CA, AppRole credentials; HSM: vendor PKCS#11 library and slot PIN |
| Backend to authorities, devices, directories | Certificate requests, deployments, directory reads | TLS with certificate verification by default; credentials encrypted at rest |
| Between HA nodes | Database replication, etcd, administrator actions forwarded to the other nodes | etcd with mutual TLS; forwarded administrator actions are refused unless HA_PEER_CA is set (Hardening) |
The database role¶
The PostgreSQL container creates an administrator account (DB_USER, clm_user on the appliance), used only inside the database container and by pkifactor-ctl for dumps and restores. The applications connect as clm_app, created with NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION: it owns the PKIFactor database and its schemas, nothing more. An SQL injection or a leaked connection string therefore does not give the rights of a PostgreSQL superuser (reading server files, running commands).
The backend builds its connection from DB_USER and DB_PASSWORD; the appliance Compose file sets both to the application role for the backend and the worker.
Data flows¶
| Flow | Path | Notes |
|---|---|---|
| Sign-in and screens | Browser, nginx, frontend, backend | The backend issues the session token; the frontend forwards it on each call |
| REST API | Client, nginx (/api/ and /api/v1/), backend |
Every route requires authentication, except the health probes (REST API) |
| Enrolment | Client, nginx, gateway, backend, authority | The gateway never talks to the authority itself |
| Deployment | Worker, device (F5, FortiWeb, Kemp, SSH...) | The private key is decrypted in memory only for the push |
| Audit | Backend, audit.log, SIEM |
See Logging and incident handling |
| Licence | None | Verification is local; PKIFactor sends no licence call and no telemetry |
The complete list of ports and outbound connections per connector is in Network flow matrix.
Where secrets and private keys live¶
| Item | Location | Protection |
|---|---|---|
| Platform secrets (data key, token keys, KMS credentials) | /opt/pkifactor/secrets/, mounted as files into the services that need them |
Directory mode 750, owner root:pkifactor |
| Data keys (DEKs) | data/keys/dek_store.json |
Stored wrapped by the KMS or HSM key |
| Key-encryption key (KEK) | Vault Transit or the HSM slot | Never leaves the KMS or HSM |
| Connector credentials, API keys, gateway secrets | Database | Encrypted with the data keys |
| Private keys generated by PKIFactor | Database, in the certificate record | Encrypted with the data keys; turned into a PKCS#12 only on download |
| Passwords of local accounts | Database | bcrypt-SHA256 hash |
| Database passwords | /opt/pkifactor/.env |
File mode 640 |
| TLS keys of the platform | nginx/ssl/ |
Key files mode 600 |
| Licence signing key | Not on the appliance | The appliance holds only the public key |
Details: Secrets and key management.
Isolation between organizations¶
All organizations share one database. Isolation is enforced by the backend on every request, not by separate schemas:
- every certificate belongs to one organization, taken from its profile;
- an operator or a user acts only on certificates of the organizations they belong to, through their teams; what an operator may do there is set per person by their operator permissions;
- a validator sees the queues of the organizations where they approve, and a substitute sees them only during an active delegation;
- administrators have a global view; auditors too, unless their audit scope is limited to their own organizations;
- a membership taken from a directory stops counting when that team has not been synchronized for too long: an access decision based on stale data is refused.
See Organizations and teams and Users and roles.