MyOwnPKI documentation Source on GitHub

Security

A certificate authority is a high-value target, so the design is conservative. This page is a map: what the main protections are, where the details and the reasons are written down, and how to report a problem. It does not replace the security chapter of the project plan, which is the source for the threat model and the decisions behind them.

Report a vulnerability

Please report vulnerabilities privately, never as a public issue or pull request. The process is in SECURITY.md.

What protects what

Area What the product does
CA keys Online CA keys lie encrypted in the database. A key-encryption key is derived from the unseal passphrase with Argon2id and exists only in memory. The server starts sealed and signs only after an instance admin unseals it. Seal removes all keys from memory at once. An unseal refuses weak key-derivation parameters.
The root Can be offline. A root signs only intermediate CAs and CRLs; the engine refuses to issue end-entity certificates with it.
Backups Contain the CA keys, encrypted. A backup alone cannot sign; a backup plus the passphrase or the recovery key can.
Certificate content Extensions come from the preset and the policy, never from the CSR. CSRs, uploads and ACME payloads are treated as attacker input. Name constraints are enforced before signing, not only by clients.
People A second factor (an authenticator app with recovery codes) is mandatory. Passkeys work only through the REST API, not in the web UI. Sensitive actions ask for a fresh code (step-up): revoking, downloading keys, CA operations, user management, agent management, switching the agent enrollment mode. Wrong passwords lock the account for 15 minutes.
Roles viewer, operator and admin per tenant or per CA; instance admins unseal and manage users.
API tokens Never more than their owner has, one year at most, and they can never satisfy step-up.
Audit log Every action is logged in a hash chain with signed checkpoints. myownpki audit verify checks it; with the checkpoint key fingerprint from init it can prove that the log is the one you had.
Listeners The admin listener checks the allowed networks before TLS and serves only HTTPS. The public listener holds only public data. Cookies are HttpOnly; Secure; SameSite=Strict, with CSRF tokens and a strict content security policy without inline scripts.
ACME The validator resolves a name once and connects to exactly the checked address (no DNS rebinding), never to the ranges the address guard refuses (loopback, link-local and more; list), only to ports 80 and 443, and never tells the client what the target answered. New accounts can wait for approval.
Agents The agent takes no commands from the server, runs only hooks from its root-owned agent.yaml, writes only below its paths, and runs in a systemd sandbox. Its certificates last 7 days. Enrollment proves control of the host name. An enrollment pass admits a bounded number of hosts, never without that proof and never with an API token creating it.
Releases Built by a workflow from a tag, signed keyless with cosign (Sigstore), with an SBOM per archive. Builds are reproducible. Dependencies are pinned; workflows use pinned actions and a read-only token.
Logs Never contain secrets. Errors name hosts, never passwords or webhook paths.

The reasons for each are in the plan and in the operations guide; the rules the code is reviewed against are in the repository.

What it does not protect

Verify a download

Every release is signed keyless with cosign: the signature proves the files come from the release workflow at that tag. Check checksums.txt, then the files against it. The commands are in the README; install.sh does the same for the archive it downloads when cosign is installed. The image is signed too:

cosign verify ghcr.io/der-felix/myownpki:0.3.8 \
  --certificate-identity "https://github.com/Der-Felix/MyOwnPKI/.github/workflows/release.yml@refs/tags/v0.3.8" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

More