Internal services such as dashboards, databases, message queues, admin panels and service-to-service APIs deserve TLS too. Self-signing each one gets messy, and public CAs can’t issue for private hostnames like db.internal. A private certificate authority solves this: clients trust your root once, and every certificate it issues is accepted. step-ca (by Smallstep) makes running one practical, including ACME, so Certbot, Caddy and Traefik can renew certificates automatically, just like with Let’s Encrypt.

When you need a private CA

  • Many internal services, with TLS that clients verify properly (no -k).
  • Mutual TLS (mTLS): services authenticate each other with client certificates.
  • Short-lived certificates, issued automatically.
  • Private hostnames or IPs that public CAs won’t certify.

For public-facing sites, keep using Let’s Encrypt. See SSL/TLS explained.

Install

On a small dedicated VPS, or a protected internal host:

# Debian/Ubuntu: download the step-cli and step-ca .deb packages from Smallstep's releases
sudo apt install ./step-cli_*_amd64.deb ./step-ca_*_amd64.deb
step version

Initialise the CA

step ca init --name "Example Internal CA" --dns ca.internal.example.com \
  --address :9000 --provisioner [email protected] --acme

This creates:

  • a root CA certificate and key (keep the root key offline after setup if possible);
  • an intermediate CA, which signs day-to-day certificates;
  • a JWK provisioner (password-protected) and an ACME provisioner;
  • config in ~/.step/ (move it to a service account for production).

Note the root fingerprint it prints. Clients use it to bootstrap trust securely.

Run as a service

Create a step user, move the config to /etc/step-ca, store the password file with chmod 600, and run it under systemd:

# /etc/systemd/system/step-ca.service
[Unit]
Description=step-ca
After=network-online.target

[Service]
User=step
Environment=STEPPATH=/etc/step-ca
ExecStart=/usr/bin/step-ca /etc/step-ca/config/ca.json --password-file /etc/step-ca/password.txt
Restart=on-failure

[Install]
WantedBy=multi-user.target

Expose port 9000 only on the private network, or through a VPN. See UFW and WireGuard.

Trust the root on clients

step ca bootstrap --ca-url https://ca.internal.example.com:9000 --fingerprint <root-fingerprint> --install

--install adds the root to the system trust store. Alternatively, copy root_ca.crt to each machine (/usr/local/share/ca-certificates/ + update-ca-certificates). See self-signed certificates for per-OS trust steps.

Issue certificates

Manually with the CLI:

step ca certificate grafana.internal.example.com grafana.crt grafana.key
step certificate inspect grafana.crt --short

The default lifetime is 24 hours. Short-lived certificates are the point: renew them automatically.

step ca renew --daemon grafana.crt grafana.key      # renews before expiry

Via ACME (the same protocol as Let’s Encrypt):

sudo certbot certonly --standalone \
  --server https://ca.internal.example.com:9000/acme/acme/directory \
  -d grafana.internal.example.com

Certbot needs to trust your root (REQUESTS_CA_BUNDLE=/path/root_ca.crt, or the system store). Caddy and Traefik accept a custom ACME directory too, so internal services get automatic HTTPS. See Caddy.

For ACME’s HTTP-01 challenge, the CA must reach the service on port 80, over the internal network. DNS-01 works with an internal DNS server that supports updates.

Set longer lifetimes for ACME-issued certificates in the provisioner’s claims if needed (e.g. 30 days).

Mutual TLS between services

Issue client certificates for services, and require them on the server side.

Nginx example: only clients with certificates from your CA may connect:

server {
    listen 443 ssl;
    ssl_certificate     /etc/ssl/internal/api.crt;
    ssl_certificate_key /etc/ssl/internal/api.key;
    ssl_client_certificate /etc/ssl/internal/root_ca.crt;
    ssl_verify_client on;
}

The client presents its certificate: curl --cert client.crt --key client.key https://api.internal.example.com. Databases (PostgreSQL clientcert=verify-full), message brokers and service meshes support the same model. See zero trust networking.

SSH certificates too

step-ca can also act as an SSH CA, issuing short-lived SSH user and host certificates, often tied to single sign-on. See SSH certificates.

Operating the CA safely

  • Protect the keys: the root key ideally offline (or in an HSM/KMS). The intermediate key is on the CA server, readable only by the step user.
  • Back up /etc/step-ca (config, certificates, encrypted keys, database) securely off-server. See backup strategy.
  • Monitor the service, and alert if renewals fail. Short-lived certificates make CA downtime visible quickly.
  • Restrict provisioners: limit which names each provisioner may issue (policy settings), so a compromised service can’t request certificates for other hosts.

Quick reference

Task Command
Initialise CA step ca init --acme
Trust root (client) step ca bootstrap --ca-url URL --fingerprint FP --install
Issue cert step ca certificate NAME crt key
Renew automatically step ca renew --daemon crt key
ACME directory https://ca:9000/acme/acme/directory
Inspect step certificate inspect crt

Related: openssl commands and SSL certificate chain errors.

  • SSL
  • Security
  • Self-Hosting