Install

# Debian / Ubuntu
curl -sSL https://github.com/smallstep/cli/releases/latest/download/step-cli_amd64.deb \
    -o /tmp/step-cli.deb
sudo dpkg -i /tmp/step-cli.deb

curl -sSL https://github.com/smallstep/certificates/releases/latest/download/step-ca_amd64.deb \
    -o /tmp/step-ca.deb
sudo dpkg -i /tmp/step-ca.deb

# macOS
brew install smallstep/smallstep/step
brew install smallstep/smallstep/step-ca

# Or via mise
mise use -g step step-ca

Initialize the CA

step ca init \
    --name "Lab Root CA" \
    --dns ca.lab.example.com \
    --address ":8443" \
    --provisioner amir@example.com \
    --acme

Generates:

  • Root CA private key + cert (in ~/.step/secrets/root_ca_key + ~/.step/certs/root_ca.crt)
  • Intermediate CA (used to actually sign certs)
  • An initial JWT provisioner (for human-driven cert issuance)
  • An ACME provisioner (for cert-manager / Caddy / certbot)
  • A passphrase for the provisioner key (write it down)

Run the CA

# Foreground (for testing)
step-ca ~/.step/config/ca.json --password-file ~/.step/password

# Systemd unit for persistent
sudo tee /etc/systemd/system/step-ca.service <<'EOF'
[Unit]
Description=step-ca
After=network-online.target

[Service]
User=step
Group=step
ExecStart=/usr/bin/step-ca /etc/step-ca/ca.json --password-file /etc/step-ca/password
Restart=always
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/etc/step-ca /var/lib/step-ca

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now step-ca

The CA listens on HTTPS port 8443 (configurable). Reverse-proxy with Caddy:

ca.lab.example.com {
    reverse_proxy 127.0.0.1:8443
}

Issue a cert via ACME

Once the ACME provisioner is enabled, any ACME client can request certs. With cert-manager on Kubernetes:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  acme:
    server: https://ca.lab.example.com/acme/acme/directory
    skipTLSVerify: false   # if you've added the CA root to the cluster's trust store
    caBundle: <base64-encoded root_ca.crt>
    privateKeySecretRef:
      name: internal-ca-account
    solvers:
      - http01:
          ingress:
            class: nginx

Now create Certificate resources as usual; cert-manager pulls certs from step-ca instead of Let's Encrypt. Inside the cluster, every Service can have a real TLS cert from your internal CA.

With Caddy (any service):

{
    acme_ca https://ca.lab.example.com/acme/acme/directory
    acme_ca_root /etc/caddy/root_ca.crt
}

api.lab.example.com {
    reverse_proxy backend:8080
}

Caddy fetches a cert from step-ca on first request, renews automatically.

Issue a cert manually

# Bootstrap a client (one-time per machine)
step ca bootstrap --ca-url https://ca.lab.example.com --fingerprint <ca-fingerprint>

# Issue a cert (24h default validity)
step ca certificate \
    "api.lab.example.com" \
    api.crt api.key \
    --provisioner amir@example.com

# Renew before expiry
step ca renew api.crt api.key

step-ca defaults to short-lived certs (24h). Combined with automatic renewal, it's vastly safer than year-long static certs — if a private key leaks, the worst-case exposure window is hours, not months.

mTLS: client certs for service authentication

The killer use case. Issue per-service / per-user certs from the same CA; configure servers to require + validate client certs:

# Issue a client cert
step ca certificate amir@example.com amir.crt amir.key

# Use with curl
curl --cacert root_ca.crt --cert amir.crt --key amir.key \
    https://internal-service.example.com/api

# Server-side (nginx)
server {
    listen 443 ssl;
    ssl_certificate /etc/nginx/api.crt;
    ssl_certificate_key /etc/nginx/api.key;

    ssl_client_certificate /etc/nginx/root_ca.crt;
    ssl_verify_client on;

    # Per-request: $ssl_client_s_dn has the client identity
    location / {
        proxy_set_header X-Client-CN $ssl_client_s_dn;
        proxy_pass http://upstream;
    }
}

Combined with the ESO bridge from that tutorial, you have an internal mTLS PKI with rotation, no shared secrets, audit trail for every cert issued.

SSH certificates

The other killer feature. Instead of distributing authorized_keys files everywhere, step-ca issues short-lived SSH user certificates and SSH host certificates:

# Configure SSH server (one-time)
sudo step ca ssh config \
    --host \
    --output /etc/ssh/sshd_config.d/01-step.conf

# Get a host certificate for this server
sudo step ssh certificate "<hostname>" /etc/ssh/ssh_host_ecdsa_key.pub \
    --host

# Get a user certificate (24h validity)
step ssh certificate amir@example.com ~/.ssh/id_ecdsa

# Now ssh works without authorized_keys / known_hosts pre-distribution
ssh root@host.lab.example.com

Users renew their cert daily (via cron / login hook); rotate compromised user access by revoking the cert, no per-server changes. SSH host certs eliminate the "trust this fingerprint?" UX entirely.

Distribute the root CA

For browsers / OS-level trust, install the root CA cert on each machine:

# Linux (Debian / Ubuntu)
sudo cp root_ca.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 root_ca.crt

# Windows
certutil -addstore -f "Root" root_ca.crt

# Firefox uses its own trust store; install via about:preferences#privacy → View Certificates

Distribute via Ansible (see that tutorial) or chezmoi (see that tutorial) for fleet-wide trust.

Backups

The root CA private key is the most critical secret in your infrastructure. Store it offline (encrypted USB stick in a safe). The intermediate CA + provisioner keys can be backed up via the standard backup tooling but should also be encrypted at rest.

step-ca vs Let's Encrypt

  • Let's Encrypt — for public-facing services with public DNS. Free, automatic, trusted by every browser.
  • step-ca — for internal services without public DNS, mTLS, device certs, SSH certs. Trusted only by hosts that have installed your root.

Use both: Let's Encrypt for public.example.com, step-ca for internal.example.com.

step-ca vs HashiCorp Vault PKI

  • Vault PKI (see that tutorial) — broader secrets-management platform; PKI is one engine. More integration with secret-store workflows.
  • step-ca — PKI-focused; simpler config; built-in ACME + SSH certs. The right pick if you don't already run Vault.

For teams already deep in Vault, use Vault's PKI engine. For "I just need a CA," step-ca is the smallest tool.