Skip to content
Private Certificates with Smallstep

Private Certificates with Smallstep

This guide sets up a private certificate authority (CA) using Smallstep step-ca on Ubuntu. Proxmox requests its own certificates through ACME, while Caddy manages HTTPS certificates for Sonarr, Radarr, and Prowlarr.

The architecture uses one online CA server. Its intermediate key signs service certificates; its root private key is backed up separately and removed from the online server.

All names and addresses below are examples. Replace them consistently with values for your network. Commands are intended for a fresh installation unless stated otherwise.

Architecture and example values

ComponentExample nameExample IP
Internal DNSdns.lab.internal192.168.50.53
Ubuntu CA serverca.lab.internal192.168.50.10
Proxmox nodepve01.lab.internal192.168.50.20
Ubuntu application serverapps.lab.internal192.168.50.30
Sonarrsonarr.lab.internal192.168.50.30
Radarrradarr.lab.internal192.168.50.30
Prowlarrprowlarr.lab.internal192.168.50.30

The CA endpoint is https://ca.lab.internal:9000. These private certificates become trusted only on devices where you install the root public certificate.

The Ubuntu hostname does not need to match ca.lab.internal. The CA’s --dns setting must match the hostname clients use to connect to its HTTPS endpoint.

Prerequisites

  • A dedicated Ubuntu CA server with a stable IP, reliable time synchronization, and backups.
  • An Ubuntu application server with Sonarr, Radarr, and Prowlarr already running.
  • A Proxmox VE node and administrative access to it.
  • Internal DNS that resolves the example names to their correct addresses.
  • A secure backup destination and a password manager.

Allow the following connections as appropriate for your network:

SourceDestinationPurpose
Proxmox and application serverCA TCP 9000Certificate issuance and renewal
CAProxmox TCP 80ACME HTTP-01 validation
CAApplication server TCP 80/443ACME validation
User devicesProxmox TCP 8006Proxmox HTTPS interface
User devicesApplication server TCP 443Application HTTPS interfaces
ServersInternal DNS UDP/TCP 53Name resolution

The CA must resolve the application names itself and reach their validation ports. Public internet exposure is not needed for private ACME validation. See Smallstep’s ACME guide.

1. Check DNS and routing

On each client server, confirm that the CA resolves correctly:

1
getent hosts ca.lab.internal

On the CA server, check the certificate names:

1
2
3
4
getent hosts pve01.lab.internal
getent hosts sonarr.lab.internal
getent hosts radarr.lab.internal
getent hosts prowlarr.lab.internal

Ubuntu DNS configuration

Ubuntu Server commonly uses Netplan and systemd-resolved. Inspect the current configuration before editing:

1
2
3
4
5
resolvectl status
ip -br address
ip route
sudo ls /etc/netplan
sudo cat /etc/netplan/*.yaml

Under the existing LAN interface in Netplan, configure the internal resolver:

1
2
3
nameservers:
  addresses:
    - 192.168.50.53

This is a fragment to merge into the existing interface configuration, not a complete Netplan file. Preserve addresses, routes, and other settings. If that interface uses DHCP and you need to reject DHCP-supplied DNS, merge this too:

1
2
dhcp4-overrides:
  use-dns: false

On a server with multiple interfaces, attach the internal DNS settings to the LAN-facing interface. A connected route to 192.168.50.0/24 already provides access to hosts on that subnet; do not add a duplicate route. Preserve any deliberate VPN/default-route arrangement.

Validate and apply with rollback protection:

1
2
3
4
5
sudo netplan generate
sudo netplan try
sudo resolvectl flush-caches
resolvectl query ca.lab.internal
ip route get 192.168.50.53

Confirm the change when prompted if connectivity remains working. Do not edit the generated /etc/resolv.conf stub directly. If cloud-init or another management tool owns the configuration, make the persistent change through that tool.

2. Install Smallstep on the CA server

On the CA server, enter a root shell for the installation and service configuration:

1
sudo -i

Root is convenient for these commands, but the CA service will run as a dedicated unprivileged account.

Install from Smallstep’s official Ubuntu repository:

1
2
3
4
5
6
7
apt-get update
apt-get install -y --no-install-recommends curl gpg ca-certificates
install -d -m 0755 /etc/apt/keyrings

curl -fsSL \
  https://packages.smallstep.com/keys/apt/repo-signing-key.gpg \
  -o /etc/apt/keyrings/smallstep.asc

Create the repository file:

1
2
3
4
5
6
7
cat > /etc/apt/sources.list.d/smallstep.sources <<'EOF'
Types: deb
URIs: https://packages.smallstep.com/stable/debian
Suites: debs
Components: main
Signed-By: /etc/apt/keyrings/smallstep.asc
EOF

The final EOF must begin at the start of the line, with no leading or trailing spaces. Press Enter after it. Otherwise, the shell continues waiting for input.

1
2
3
4
apt-get update
apt-get install -y step-cli step-ca
step version
step-ca version

3. Initialize the CA

Create the service account:

1
2
3
4
5
6
useradd --system --user-group \
  --home-dir /var/lib/step-ca \
  --create-home \
  --shell /usr/sbin/nologin step-ca

chmod 700 /var/lib/step-ca

Create protected password files:

1
2
3
4
5
6
7
8
install -o step-ca -g step-ca -m 0600 \
  /dev/null /var/lib/step-ca/password.txt

install -o step-ca -g step-ca -m 0600 \
  /dev/null /var/lib/step-ca/provisioner-password.txt

nano /var/lib/step-ca/password.txt
nano /var/lib/step-ca/provisioner-password.txt

Put a different strong password on one line in each file and save both in your password manager. The first encrypts the CA keys. The second protects the administrative provisioner key. Do not place passwords directly in shell commands.

Initialize once:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
sudo -u step-ca env STEPPATH=/var/lib/step-ca \
  step ca init \
  --deployment-type standalone \
  --name "Example Lab PKI" \
  --dns ca.lab.internal \
  --address :9000 \
  --with-ca-url https://ca.lab.internal:9000 \
  --provisioner bootstrap-admin \
  --password-file /var/lib/step-ca/password.txt \
  --provisioner-password-file /var/lib/step-ca/provisioner-password.txt \
  --acme

bootstrap-admin is a chosen provisioner name, not a special role or automatically temporary account. Proxmox and Caddy will use separate ACME provisioners. See the initialization reference.

4. Back up and remove the online root key

Initialization creates both root and intermediate keys on this server. The root is needed for future intermediate signing operations, but not for everyday service certificate issuance.

Back up these files:

1
2
/var/lib/step-ca/certs/root_ca.crt
/var/lib/step-ca/secrets/root_ca_key

The root key is encrypted using the password saved from password.txt. Store that password separately from the backup.

Copy using an SSH user

The CA directory is private to the service account. To retrieve the files through an ordinary SSH account, stage protected copies. Replace adminuser and its group/home path with your actual account:

1
2
3
4
5
6
install -d -o adminuser -g adminuser -m 0700 /home/adminuser/ca-backup

install -o adminuser -g adminuser -m 0600 \
  /var/lib/step-ca/certs/root_ca.crt \
  /var/lib/step-ca/secrets/root_ca_key \
  /home/adminuser/ca-backup/

On your workstation:

1
2
3
4
5
6
7
8
mkdir -p ~/ca-backup
chmod 700 ~/ca-backup

scp adminuser@ca.lab.internal:/home/adminuser/ca-backup/root_ca.crt \
    adminuser@ca.lab.internal:/home/adminuser/ca-backup/root_ca_key \
    ~/ca-backup/

chmod 600 ~/ca-backup/root_ca_key

SCP prompts for the remote SSH user’s password if password authentication is enabled. It does not need the root key’s encryption password to copy the file.

Compare file hashes. On the CA server:

1
2
sha256sum /var/lib/step-ca/certs/root_ca.crt \
          /var/lib/step-ca/secrets/root_ca_key

On the workstation:

1
sha256sum ~/ca-backup/root_ca.crt ~/ca-backup/root_ca_key

After both hashes match and the password is saved, move the backup to secure, preferably offline storage. Then, on the CA server:

1
2
3
4
5
rm /var/lib/step-ca/secrets/root_ca_key
rm /home/adminuser/ca-backup/root_ca_key
rm /home/adminuser/ca-backup/root_ca.crt
rmdir /home/adminuser/ca-backup
rm /var/lib/step-ca/provisioner-password.txt

Keep the root public certificate, intermediate certificate/key, and password.txt on the CA server. Removing files does not erase copies already captured in snapshots or backups. An always-connected backup machine is not offline storage.

This is a practical single-server bootstrap. For higher assurance, generate the root on an offline machine from the outset. See Smallstep’s production guidance.

5. Configure issuance policy and lifetimes

Edit the generated configuration:

1
nano /var/lib/step-ca/config/ca.json

Inside the existing authority object, add this policy, avoiding duplicate keys:

1
2
3
4
5
6
7
8
"policy": {
  "x509": {
    "allow": {
      "dns": ["*.lab.internal"]
    },
    "allowWildcardNames": false
  }
},

This permits names such as sonarr.lab.internal. It does not permit literal wildcard certificates, IP SANs, the bare lab.internal, or deeper names such as host.services.lab.internal. Add explicit rules if those are needed. This is an issuance policy, not a cryptographic constraint on a stolen signing key. See Smallstep policies.

In the existing authority.provisioners array, replace the generated ACME entry with these two objects. Preserve the administrative JWK provisioner:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "type": "ACME",
  "name": "acme",
  "claims": {
    "defaultTLSCertDuration": "168h",
    "maxTLSCertDuration": "168h"
  }
},
{
  "type": "ACME",
  "name": "proxmox",
  "claims": {
    "defaultTLSCertDuration": "2160h",
    "maxTLSCertDuration": "2160h"
  }
}

These are fragments to merge into the generated JSON. Adjacent objects require commas; the last object has no trailing comma.

This guide uses 7-day certificates for automated application services and 90-day certificates for Proxmox, giving its scheduled renewal process ample margin. Provisioner claims control these lifetimes; the provisioner names themselves do not restrict which client can use them. See Smallstep configuration.

6. Test and run under systemd

First test in the foreground:

1
2
3
sudo -u step-ca env STEPPATH=/var/lib/step-ca \
  step-ca /var/lib/step-ca/config/ca.json \
  --password-file /var/lib/step-ca/password.txt

Successful startup reports that HTTPS is serving on port 9000. The command occupies the terminal because step-ca runs in the foreground. Press Ctrl+C to stop it cleanly before starting the systemd service.

Create /etc/systemd/system/step-ca.service:

1
nano /etc/systemd/system/step-ca.service
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[Unit]
Description=Private Certificate Authority
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=step-ca
Group=step-ca
Environment=STEPPATH=/var/lib/step-ca
WorkingDirectory=/var/lib/step-ca
ExecStart=/usr/bin/step-ca /var/lib/step-ca/config/ca.json \
  --password-file /var/lib/step-ca/password.txt
Restart=on-failure
RestartSec=5
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/step-ca

[Install]
WantedBy=multi-user.target

The backslash in ExecStart explicitly continues the command. Do not split a file path across physical lines.

1
2
3
4
systemd-analyze verify /etc/systemd/system/step-ca.service
systemctl daemon-reload
systemctl enable --now step-ca
systemctl status step-ca --no-pager -l

The CA now runs in the background and starts after reboot. Verify HTTPS:

1
2
curl --cacert /var/lib/step-ca/certs/root_ca.crt \
  https://ca.lab.internal:9000/health

Expected:

1
{"status":"ok"}

Record the public root fingerprint through a trusted channel:

1
step certificate fingerprint /var/lib/step-ca/certs/root_ca.crt

Inspect logs with:

1
journalctl -u step-ca -n 50 --no-pager

7. Install root trust on Ubuntu and Proxmox

Distribute only root_ca.crt to clients. Never distribute the root private key for certificate trust.

From the workstation, copy the verified certificate to each target server:

1
scp ~/ca-backup/root_ca.crt adminuser@SERVER_IP:~/lab-root-ca.crt

Use the target’s actual SSH user. On the target, inspect its fingerprint and compare it with the value recorded from the CA; ignore colon separators and letter case:

1
openssl x509 -in ~/lab-root-ca.crt -noout -fingerprint -sha256

Install it on Ubuntu/Debian, including Proxmox:

1
2
3
4
5
sudo install -m 0644 ~/lab-root-ca.crt \
  /usr/local/share/ca-certificates/lab-root-ca.crt

sudo update-ca-certificates
curl https://ca.lab.internal:9000/health

If logged in as root on Proxmox, omit sudo. A successful health response without --cacert confirms system trust and connectivity.

8. Issue the Proxmox certificate

On the Proxmox node as root, register an ACME account. Replace the example email with a real contact address:

1
2
pvenode acme account register lab admin@example.com \
  --directory https://ca.lab.internal:9000/acme/proxmox/directory

In the Proxmox web interface:

  1. Select the node, then System → Certificates.
  2. Select the lab account in the ACME section.
  3. Add a domain using the HTTP challenge.
  4. Enter pve01.lab.internal.
  5. Select Order Certificates Now.

The CA must resolve this name to the node and reach TCP 80. The management interface continues using TCP 8006:

1
https://pve01.lab.internal:8006

Repeat for additional nodes with their own names and certificates. Proxmox provides scheduled ACME renewal; check renewal tasks on your installed version and verify a successful renewal after deployment.

Use the supported certificate/ACME interface. Do not overwrite Proxmox’s pve-ssl.pem, pve-ssl.key, or cluster CA files. Check integrations that pin a server certificate fingerprint when certificates change. See Proxmox Certificate Management.

9. Trust the root on Omarchy / Arch Linux

On the workstation, verify the public certificate fingerprint before installing it:

1
2
3
4
openssl x509 -in ~/ca-backup/root_ca.crt -noout -fingerprint -sha256
sudo trust anchor --store ~/ca-backup/root_ca.crt
sudo update-ca-trust
curl https://ca.lab.internal:9000/health

Fully close and reopen the browser before testing Proxmox. If command-line trust works but the browser still warns, inspect that browser’s certificate settings; some applications maintain separate trust stores.

Use DNS names in URLs. A certificate for pve01.lab.internal does not validate access by IP address.

10. Configure HTTPS for Sonarr, Radarr, and Prowlarr

The following assumes native application services on the same Ubuntu host as Caddy, default ports, and empty URL Base settings. If you already use subpath URL bases, account for those before switching to hostname-based access.

Check the application server

1
2
3
systemctl status sonarr radarr prowlarr --no-pager
sudo ss -ltnp | grep -E ':(80|443|8989|7878|9696)\b'
curl https://ca.lab.internal:9000/health

Sonarr should listen on 8989, Radarr on 7878, and Prowlarr on 9696. Resolve any existing listener on 80/443 before installing Caddy.

Install Caddy

Use the official Caddy Ubuntu repository:

1
2
sudo apt update
sudo apt install -y curl gnupg debian-keyring debian-archive-keyring apt-transport-https
1
2
curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key \
  | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
1
2
curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt \
  | sudo tee /etc/apt/sources.list.d/caddy-stable.list
1
2
3
4
sudo chmod 644 /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod 644 /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy

Configure the reverse proxy

Copy the already verified root certificate:

1
2
3
4
5
6
sudo install -m 0644 \
  /usr/local/share/ca-certificates/lab-root-ca.crt \
  /etc/caddy/lab-root-ca.crt

sudo cp -a /etc/caddy/Caddyfile /etc/caddy/Caddyfile.original
sudo nano /etc/caddy/Caddyfile

For this fresh Caddy installation, replace the default configuration with:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(lab_tls) {
    tls {
        issuer acme {
            dir https://ca.lab.internal:9000/acme/acme/directory
            trusted_roots /etc/caddy/lab-root-ca.crt
        }
    }
}

sonarr.lab.internal {
    import lab_tls
    reverse_proxy 127.0.0.1:8989
}

radarr.lab.internal {
    import lab_tls
    reverse_proxy 127.0.0.1:7878
}

prowlarr.lab.internal {
    import lab_tls
    reverse_proxy 127.0.0.1:9696
}

The shared snippet explicitly selects Smallstep as the ACME issuer. Caddy obtains and renews certificates and serves HTTPS, forwarding requests to the applications over local HTTP. See Caddy TLS configuration.

Validate and load:

1
2
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile

If validation succeeds:

1
2
3
sudo systemctl enable --now caddy
sudo systemctl reload caddy
sudo journalctl -u caddy -n 50 --no-pager

Verify all sites:

1
2
3
curl -I https://sonarr.lab.internal
curl -I https://radarr.lab.internal
curl -I https://prowlarr.lab.internal

An HTTP 401 Unauthorized response without a TLS error is expected when an application requires authentication. A Via: ... Caddy header confirms proxying. Open each site in a browser and sign in to verify full application access.

Keep application authentication enabled. Once you have checked integrations, restrict direct network access to application ports 8989, 7878, and 9696. Local integrations can often use loopback addresses, but review existing Prowlarr/application URLs before changing bindings or firewall rules.

11. Add other servers

For applications with native ACME support, configure:

1
https://ca.lab.internal:9000/acme/acme/directory

Install the root trust, create the DNS record, and allow the chosen challenge method from the CA. Generate separate private keys for separate services.

For another local HTTP application behind Caddy, add a site using the existing snippet:

1
2
3
4
dashboard.lab.internal {
    import lab_tls
    reverse_proxy 127.0.0.1:8080
}

Replace the name and port, then validate and reload Caddy. If the backend is on another machine, consider TLS for that network connection too.

For services requiring certificate files, Smallstep offers step ca certificate and automated step ca renew. Configure renewal under a service manager and reload the consuming application after renewal. Avoid storing administrative provisioner passwords on every host. See Smallstep renewal.

Troubleshooting

SymptomCheck or fix
Shell waits after a pasted heredocEnter the terminating EOF without indentation, or cancel and paste again.
env: '--name': No such file or directoryRestore the missing step ca init command before its options.
curl: (7) downloading packagesCheck DNS filtering, firewall, routing, and endpoint reachability. It occurs before certificate verification.
getent hosts returns nothingUse resolvectl query, verify the DNS record, and check resolver/interface routing.
curl: (60) against the CAInstall the verified root certificate in the client’s trust store. Do not disable TLS verification.
SCP cannot access the CA directoryStage protected copies in the SSH user’s home, verify the transfer, and remove staged copies.
step-ca occupies the terminalForeground execution is normal; use systemd for continuous operation.
systemd reports Missing '=' or is a directoryCheck for broken lines or split paths in ExecStart.
ACME validation failsCheck DNS from the CA, challenge ports, firewall rules, and CA/Caddy task logs.
Browser warns but curl succeedsRestart the browser and check its trust store and the exact URL hostname.
HTTPS returns 401TLS succeeded; authenticate to the application.

If DNS filtering blocks a vendor package hostname, inspect the matching rule and allow the necessary hostname rather than leaving filtering disabled indefinitely.

Operations and recovery

  • Back up CA configuration, intermediate key/certificate, unlock password, and a consistent database copy. For a simple embedded-database backup, stop the CA while copying its state and restart afterward.
  • Keep the root private key backup separately protected, preferably offline. Test recovery and record where the password is stored.
  • Back up Caddy configuration and persistent state, including its ACME account and certificates. Treat that state as sensitive.
  • Monitor the certificates actually presented by HTTPS endpoints, as well as CA/Caddy health and renewal failures.
  • Verify at least one successful automatic renewal on Proxmox and Caddy. Initial issuance alone does not prove renewal works.
  • Track root and intermediate expiry and plan rotation before either expires.
  • Keep time synchronized. Preserve console/SSH access for recovery if the CA VM runs on the Proxmox infrastructure it serves.
  • Protect DNS administration and enrollment access. ACME name validation is not a substitute for controlling who can alter DNS or reach enrollment services.
  • After key compromise, replace the key and revoke affected certificates, then remove the attacker’s enrollment access. Do not assume all clients immediately enforce revocation.

The setup is complete when the CA survives a reboot, clients validate its chain, Proxmox and all application endpoints present the expected certificates, and renewal monitoring is in place.