Install
Two binaries: step (the CLI) and step-ca (the server).
# Debian / Ubuntu (deb repository)
curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg \
| sudo tee /etc/apt/keyrings/smallstep.asc >/dev/null
echo 'deb [signed-by=/etc/apt/keyrings/smallstep.asc] https://packages.smallstep.com/stable/debian debs main' \
| sudo tee /etc/apt/sources.list.d/smallstep.list
sudo apt update
sudo apt install step-cli step-ca
On other distros, single-binary releases on GitHub work the same.
Bootstrap the CA
One-shot wizard creates the root + intermediate keys, configures the server, and writes a starter ca.json:
sudo step ca init \
--name "Homelab CA" \
--dns ca.lab.example.com \
--address :443 \
--provisioner amir@example.com \
--password-file /etc/step-ca/password
This writes to /etc/step-ca/ (when run as root). The important files:
certs/root_ca.crt— the root certificate to distribute to clientscerts/intermediate_ca.crt— the online intermediate (this is what actually signs leaves)secrets/root_ca_key— the offline root key. Move this to cold storage after bootstrap. The server only needs the intermediate.secrets/intermediate_ca_key— the online signing keyconfig/ca.json— server configuration
Run as a systemd service:
sudo systemctl enable --now step-ca
sudo systemctl status step-ca
curl -k https://ca.lab.example.com/health
# Returns: {"status":"ok"}
The root key is the entire trust anchor. After bootstrap, move secrets/root_ca_key off the running server — a YubiKey, a printed paper backup in a safe, or simply an encrypted USB drive in a drawer. The intermediate key the server actually uses can be rotated; the root key cannot, easily.
Issue a leaf certificate manually
The first time a client uses the CA, it has to "bootstrap" against the CA's fingerprint:
# On a client machine
step ca bootstrap --ca-url https://ca.lab.example.com \
--fingerprint <fingerprint-from-init-output>
Now request a cert:
step ca certificate web1.lab.example.com web1.crt web1.key
By default it prompts for the provisioner password and produces a cert with a 24-hour lifetime. That's intentional: short-lived certs limit blast radius and make revocation almost unnecessary.
ACME provisioner: drop-in for Let's Encrypt
This is what makes Step CA practical at scale. Add an ACME provisioner to the CA config:
sudo step ca provisioner add acme --type ACME
sudo systemctl restart step-ca
Now any ACME client (certbot, Caddy, Traefik, nginx-acme, lego, acme.sh) can be pointed at https://ca.lab.example.com/acme/acme/directory in place of https://acme-v02.api.letsencrypt.org/directory, and it works exactly as if it were Let's Encrypt:
# Caddy
{
acme_ca https://ca.lab.example.com/acme/acme/directory
acme_ca_root /etc/ssl/certs/lab-root.crt
}
internal-app.lab.example.com {
reverse_proxy 127.0.0.1:8080
}
# certbot
certbot certonly \
--server https://ca.lab.example.com/acme/acme/directory \
-d internal-app.lab.example.com \
--standalone
For HTTP-01 to work on internal hostnames, the CA needs to be able to reach the requesting host (or vice-versa). For RFC1918 networks, use DNS-01 with an internal DNS provider that has an API.
Trust the root on clients
The reason "internal services have certificate warnings" is that the browser doesn't trust the CA. Fix it by adding the root cert to each client's trust store.
# Linux (system-wide)
sudo cp lab-root.crt /usr/local/share/ca-certificates/lab-root.crt
sudo update-ca-certificates
# macOS
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain lab-root.crt
# Windows (PowerShell as Admin)
Import-Certificate -FilePath lab-root.crt -CertStoreLocation Cert:\LocalMachine\Root
For fleets, push it via MDM (Intune, Jamf, Kandji) — same mechanism corporates use for corporate root CAs.
For mobile devices and browsers that don't use the system trust store (Firefox, some Android apps), the root has to be imported separately.
SSH certificate authority
SSH cert auth replaces the per-user, per-host authorized_keys dance with two facts: the server trusts a CA to sign user certs, and the user authenticates with a short-lived cert signed by that CA.
Add an SSH provisioner (any provisioner can issue SSH certs by enabling ssh: true in its options):
sudo step ca provisioner update amir@example.com --ssh
sudo systemctl restart step-ca
On each SSH server, add a line to /etc/ssh/sshd_config:
TrustedUserCAKeys /etc/ssh/lab-user-ca.pub
HostCertificate /etc/ssh/ssh_host_ecdsa_key-cert.pub
HostKey /etc/ssh/ssh_host_ecdsa_key
Where lab-user-ca.pub is the SSH public key from step ca config view --ssh-user.
A user requests their cert:
step ssh login --provisioner amir@example.com user@lab.example.com
The client receives a short-lived (default 16h) SSH certificate; subsequent ssh user@host.lab.example.com works without per-host key authorization — the host trusts any cert signed by the CA for the right principal.
Renewal automation
For non-ACME consumers, step can renew a cert near the end of its lifetime:
step ca renew --daemon --exec "systemctl reload nginx" \
/etc/ssl/web1.crt /etc/ssl/web1.key
Run that under a systemd service; it sleeps, wakes up before expiry, fetches a new cert, and runs the exec hook to reload the consuming service. For ACME consumers, the client's existing renew loop already handles it.
HA: secondary CA
For real production, the intermediate key shouldn't live on disk on one box. Step CA supports HSMs (YubiHSM, PKCS#11) and HashiCorp Vault as key storage backends; the running daemon then never sees the private key material directly. For a homelab, file-based is fine; for a team, look at the Vault or PKCS#11 paths before relying on it.
Worth knowing
- Short-lived is the point. Default leaf lifetimes are 24h; ACME's are configurable but the CA refuses to issue longer than
maxTLSCertDurationin config (default 24h). Revocation becomes almost irrelevant. - Step CA is not a public CA replacement. Public-facing services should still use Let's Encrypt — the world doesn't trust your root.
- OIDC integration lets human-issued SSH/TLS certs come from an SSO login (Google, Okta, Authentik). For team setups, that's the big quality-of-life upgrade.