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
stepuser. - 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.