MyOwnPKI documentation Source on GitHub

Installation

This page takes you from an empty machine to a running server, the first authority and the first certificate. There are two ways to install: a systemd service with install.sh (recommended on Linux) and a container. The complete reference is the operations guide.

What runs where

One program, several listeners. Which of them you open to whom is the main decision of an installation.

Part What it does Default Who needs to reach it
Admin listener Web UI and REST API, HTTPS only 127.0.0.1:9443 (0.0.0.0:9443 with install.sh) Administrators only. Never the internet.
Public listener CRLs, CA certificates and the trust page over plain HTTP, no login :8080 (:80 with --public-port-80) Every client that checks revocation or loads a CA certificate
ACME listener The ACME server Off until acme.base_url is set; 127.0.0.1:8081 behind a proxy, :443 with its own TLS ACME clients, or only your reverse proxy
Agent listener The agents of your hosts, mutual TLS Off until agent.base_url is set; :8444 The hosts that run myownpki-agent
Data directory Databases, audit log, keys (encrypted), bootstrap certificates /var/lib/myownpki The service user only (mode 0700)
Configuration One YAML file; every key also exists as a MYOWNPKI_* variable /etc/myownpki/config.yaml root, and read access for the service user

The public listener speaks plain HTTP on purpose. Every certificate contains the address of its CRL and of its issuer’s certificate, and clients fetch those before they trust anything. Do not redirect that port to HTTPS.

Note: The address on which the public listener is reached, the base URL (for example http://pki.homelab.lan), is written into every certificate that a CA issues and can never be changed for that CA. Pick a stable DNS name that you control, not an IP address and not a name of the machine.

Before you start

Option A: Linux with systemd

install.sh installs MyOwnPKI as a hardened systemd service with one command. Do not pipe it from the internet into a shell: download it with the checksum file, check it, then run it.

v=0.3.8
curl -fsSLO "https://github.com/Der-Felix/MyOwnPKI/releases/download/v$v/install.sh"
curl -fsSLO "https://github.com/Der-Felix/MyOwnPKI/releases/download/v$v/checksums.txt"
curl -fsSLO "https://github.com/Der-Felix/MyOwnPKI/releases/download/v$v/checksums.txt.sigstore.json"
sha256sum --check --ignore-missing checksums.txt

Then run it as root. --tls-names lists the names you will type into the browser:

sudo bash install.sh --tls-names "pki.homelab.lan, 192.0.2.5"

The script downloads the release archive of its own version, checks its SHA-256 against checksums.txt (and the signature of checksums.txt when cosign is installed), and stops before it installs anything if one check fails. It then:

Run it again and it changes nothing that is already in place (“Changes: none”). A newer release makes it an update, which is described on the Upgrade page.

Options

Option Meaning
--version V Install another release than the one the script belongs to
--archive FILE Install from a local myownpki_<version>_linux_<arch>.tar.gz, with checksums.txt next to it (and checksums.txt.sigstore.json if you have it). For machines without internet.
--tls-names LIST Names and addresses in the admin certificate. Default: hostname -f and the machine’s addresses.
--admin-listen ADDR Admin listener. Default 0.0.0.0:9443.
--allowed-networks LIST Who may connect to the admin listener. Default: 127.0.0.1 and the /24 of each own IPv4 address.
--public-port-80 Public listener on :80 without a reverse proxy (installs the low-ports.conf drop-in). Default :8080.
--acme URL ACME with its own TLS, for example https://pki.homelab.lan/acme. Sets acme.base_url, the listener port (443 if the URL has none) and acme.tls_issuer.
--acme-ca NAME The CA that issues the ACME listener’s certificate. Default issuing. Give the name of your own intermediate.
--backup-keep N How many daily backups the timer keeps. Default 14.
--no-backup-timer Switch the daily backup timer off.
-h, --help The same list on the terminal

The options about the configuration (--tls-names, --admin-listen, --allowed-networks, --public-port-80, --acme) apply only when install.sh creates config.yaml. With an existing file it tells you which values to set by hand. The drop-ins are installed either way.

Note: install.sh must run as root and needs systemctl, tar, sha256sum, install, getent, useradd, runuser, awk, cmp, stat and curl or wget. If it says the new binary does not run, /tmp may be mounted noexec: run it with TMPDIR set to another directory.

Behind a proxy

curl takes the proxy from https_proxy, but sudo drops most of the environment. Keep the variables explicitly, for the downloads and for the script:

export https_proxy=http://proxy.homelab.lan:3128
sudo --preserve-env=https_proxy,http_proxy,no_proxy bash install.sh --tls-names "pki.homelab.lan, 192.0.2.5"

Without --preserve-env the script fails with a download error although curl works in your shell. The server itself does not use these variables, except for webhooks (see Monitoring).

Ports and firewall

Direction Port Open to
In 9443/tcp (admin) Your admin networks only
In 8080/tcp, or 80/tcp with --public-port-80 or a reverse proxy (public) Every client
In 443/tcp with --acme (ACME with its own TLS), or the port of your reverse proxy Your ACME clients
In 8444/tcp (agents) The hosts with an agent
Out 80/tcp to your hosts ACME http-01 and agent enrollment: the server connects to the host to fetch a challenge file
Out 25, 465 or 587/tcp, and HTTPS Mail server and webhook receiver, if you use them

Ports below 1024 (:80, :443) without a reverse proxy need the drop-in myownpki.service.d/low-ports.conf. It gives the service exactly one capability, binding such a port. install.sh installs it when you use --public-port-80 or --acme. If a host firewall (nftables, ufw, firewalld) is active, open these ports there too; the symptoms of a closed port are in Troubleshooting.

Where everything is

Path What
/usr/local/bin/myownpki The binary. After an update the old one is myownpki.previous.
/etc/systemd/system/myownpki.service and myownpki.service.d/ The unit with a strict sandbox, and its drop-ins
/etc/myownpki/config.yaml The configuration, owner root, group myownpki, mode 0640
/var/lib/myownpki The data directory, owned by myownpki, mode 0700
/var/lib/myownpki/agent-dist The agent packages of this release
/var/backups/myownpki Backups of the daily timer and of updates, mode 0700
myownpki-backup.service, myownpki-backup.timer The daily backup

The service runs as the user myownpki. Run every myownpki command that reads the data directory as that user, with runuser -u myownpki -- /usr/local/bin/myownpki ….

Own TLS certificate or ACME without a proxy

By default the admin listener uses a self-signed bootstrap certificate whose fingerprint you compare once. Better: give it a certificate from your own CA so that browsers that trust your root show no warning. With admin.tls_issuer: issuing in the configuration the server issues and renews this certificate itself while it is unsealed; see operations guide §2.4. For ACME without a reverse proxy see --acme above and §10.10.

Option B: container

The image is ghcr.io/der-felix/myownpki for linux/amd64 and linux/arm64. It is distroless and runs as the non-root user UID 65532. Each stable release is tagged with its version (:0.3.8) and with :latest. Pin the version you tested: :latest moves with every stable release, and updating a CA should be a decision, not a pull. Pre-releases get only their version tag.

You need two files from the deploy/ directory of the release, docker-compose.yml and docker/config.yaml. Put them next to each other as docker-compose.yml and docker/config.yaml.

services:
  myownpki:
    image: ghcr.io/der-felix/myownpki:0.3.8
    restart: unless-stopped
    volumes:
      - myownpki-data:/var/lib/myownpki
      - ./docker/config.yaml:/etc/myownpki/config.yaml:ro
    ports:
      - "127.0.0.1:9443:9443"   # admin UI and API: this host only
      - "8080:8080"             # CRLs and CA certificates: every client
    read_only: true
    tmpfs: [/tmp]
    cap_drop: [ALL]
    security_opt:
      - no-new-privileges:true
    stop_grace_period: 30s

volumes:
  myownpki-data:

How the container differs from the service:

The passphrase and the recovery key

init, the next step, creates the secrets on which everything rests. Know what each one is before you create it.

Warning: The unseal passphrase (or the recovery key) decrypts every CA key. Without one of the two, the CA keys are gone for good: there is no support, no back door and no recovery from a backup. And whoever has a backup plus the passphrase or the recovery key can sign as your CAs.

Secret What it protects Where to keep it
Unseal passphrase (at least 16 characters) All CA keys. You type it after every start of the server. A password manager. Not on the server, not in a script, not in an environment variable, not next to the backups.
Recovery key (9 groups of 6 characters, shown once) The same as the passphrase: the second way in, if the passphrase is lost. It sets a new passphrase (passphrase reset); the unseal screen takes only the passphrase On paper, in a safe or another place that is not your password manager and not the backup disk
Admin password and authenticator (TOTP) Sign-in to the UI and API The admin’s password manager and phone
Checkpoint key fingerprint (shown at the end of init) Lets you prove later that the audit log is the one you had Write it down; you need it for backup verify and restore
Backups Everything, but encrypted: they cannot sign without the passphrase A third place: a backup disk or another machine

Keep the passphrase, the recovery key and the backups in three different places. Once a year, unseal with the passphrase and check that the paper with the recovery key is readable. The commands to change a passphrase or use the recovery key are in operations guide §7.

Initialize and start

Run init once, at a terminal, as the service user:

runuser -u myownpki -- /usr/local/bin/myownpki init --config /etc/myownpki/config.yaml

In a container:

docker compose run --rm -it myownpki init

It asks for the unseal passphrase (twice), the name and password of the first admin, shows the recovery key and asks you to type two groups of it again, shows the setup for the authenticator app and wants one code, and ends with recovery codes and the checkpoint key fingerprint. Write the recovery key and the fingerprint down now.

Then start the server:

systemctl start myownpki
docker compose up -d

The server starts sealed. It serves the UI, CRLs and CA certificates, but it can sign nothing until an instance admin unseals it. The log names the SHA-256 fingerprint of the admin certificate; compare it with what your browser shows the first time:

journalctl -u myownpki -o cat | grep fingerprint_sha256
docker compose logs myownpki | grep fingerprint_sha256

Open https://pki.homelab.lan:9443/, sign in with your password and authenticator code, and use Unseal in the banner. A restart seals the server again, so you unseal after every restart and every update.

For automation without a terminal, init --stdin is described in the operations guide §3.

The first CA and the first certificate

A root and an intermediate. The root CA is the anchor that every client trusts; it signs only other CAs and CRLs. The intermediate signs the certificates you issue. If the intermediate is ever compromised you replace it; replacing a root means touching every client.

Give your clients the root: the trust page at http://pki.homelab.lan/ (the public listener) shows every root with its SHA-256 fingerprint, downloads and a guide for Windows, macOS, iOS, Android, Linux and Firefox. Always compare the fingerprint with the one in the admin UI, because the page comes over plain HTTP.

The first certificate. On the host that will use it, make a key and a request. The key never leaves that host:

openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout www.homelab.lan.key -out www.homelab.lan.csr -subj "/CN=www.homelab.lan" -addext "subjectAltName=DNS:www.homelab.lan"

In the UI choose Issue certificate, the preset server, paste the request and issue. Download the fullchain and save it next to the key. The server alternatively makes the key itself, if the tenant allows that. For many certificates you do not want to do this by hand: let ACME clients or the agent ask (Adding hosts).

Check the result against your root:

openssl verify -CAfile root.pem -untrusted www.homelab.lan.fullchain.pem www.homelab.lan.fullchain.pem

Verify the installation

Go through this once after the installation. Each line should be true.

  1. The service runs:

    systemctl is-active myownpki
    
  2. The health check passes (exit code 0):

    runuser -u myownpki -- /usr/local/bin/myownpki healthcheck --config /etc/myownpki/config.yaml
    
  3. The configuration is valid and shows what is in effect:

    runuser -u myownpki -- /usr/local/bin/myownpki config check --config /etc/myownpki/config.yaml
    
  4. The public listener answers from a client machine (200 is the trust page; 404 means you switched it off, which is fine). Use the port you chose: 80 behind a proxy or with --public-port-80, otherwise :8080:

    curl -sS -o /dev/null -w '%{http_code}\n' http://pki.homelab.lan/
    
  5. The admin UI opens over HTTPS, the fingerprint matched, you could sign in with the second factor, and the server is unsealed (no banner).
  6. Your authorities are listed:

    runuser -u myownpki -- /usr/local/bin/myownpki ca list --config /etc/myownpki/config.yaml
    
  7. A certificate you issued carries working revocation and issuer addresses. Print them, then fetch each URL from a client:

    openssl x509 -in www.homelab.lan.fullchain.pem -noout -ext crlDistributionPoints,authorityInfoAccess
    
  8. The backup works: the timer is listed, one run succeeds and passes its own check:

    systemctl list-timers myownpki-backup.timer
    
    systemctl start myownpki-backup.service
    
    journalctl -u myownpki-backup.service -o cat
    
  9. You have written down the passphrase location, the recovery key and the checkpoint fingerprint, and you know who may unseal the server.
  10. A notification channel is configured and a test message arrived (Monitoring).

Next steps