step-ca Internal Certificate Authority for mTLS and Service TLS
Security

step-ca Internal Certificate Authority for mTLS and Service TLS

  • Author :Liam K.
  • Date :July 03, 2026
  • Time :27 minutes

Every TLS connection depends on a Certificate Authority (CA) to vouch for identity. Public CAs like Let's Encrypt work for internet-facing services, but internal microservices, gRPC APIs, database connections, and admin tools need certificates signed by a private CA your infrastructure trusts. step-ca is a production-grade, open-source CA that issues certificates via CLI, ACME protocol, and automated provisioners.

This guide initializes a step-ca instance on Linux, issues certificates for internal services, configures mTLS between two applications, and distributes the root CA certificate to clients. The result is encrypted, authenticated communication across your private network without depending on public certificate authorities for internal traffic.

When You Need a Private CA

  • mTLS between microservices on a private network
  • TLS for internal APIs not exposed to the public internet
  • Short-lived certificates with automated rotation
  • Development and staging environments matching production TLS patterns
  • Service mesh sidecar certificate provisioning

Step 1: Install step-ca and step-cli

bash
curl -fsSL https://packages.smallstep.com/keys/apt/repo-signing-key.gpg \
  | sudo gpg --dearmor -o /etc/apt/keyrings/smallstep.asc
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 -y step-cli step-ca

Step 2: Initialize the Certificate Authority

bash
sudo mkdir -p /opt/step-ca
cd /opt/step-ca
sudo step ca init \
  --name "Internal CA" \
  --dns ca.example.com \
  --address :9000 \
  --provisioner admin@example.com
# This creates:
[...]
Command truncated. Copy to view full command.

Step 3: Create step-ca Configuration and Start Service

bash
sudo tee /etc/systemd/system/step-ca.service >/dev/null <<'EOF'
[Unit]
Description=step-ca Certificate Authority
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/step-ca
[...]
Command truncated. Copy to view full command.

Step 4: Distribute Root CA to Clients

Every machine that needs to trust certificates issued by your CA must have the root certificate installed in its trust store.

bash
# On Linux clients:
sudo cp /opt/step-ca/certs/root_ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates

# On macOS:
# sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain root_ca.crt

# Verify trust:
step certificate inspect /opt/step-ca/certs/root_ca.crt

Step 5: Issue a Certificate for a Service

bash
# Bootstrap your CLI to the CA:
step ca bootstrap --ca-url https://ca.example.com:9000 --fingerprint FINGERPRINT_FROM_CA

# Issue a certificate:
step ca certificate api.internal.example.com api.crt api.key \
  --provisioner admin@example.com \
  --not-after 720h

# Verify:
step certificate inspect api.crt

Step 6: Configure Nginx with Internal TLS

nginx
server {
    listen 443 ssl;
    server_name api.internal.example.com;
    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 optional;
  location / {
[...]
Command truncated. Copy to view full command.

Step 7: Enable ACME for Automated Renewal

step-ca supports the ACME protocol (RFC 8555). Services like Caddy and Certbot can request certificates from your internal CA the same way they request from Let's Encrypt — enabling fully automated short-lived certificate rotation.

bash
# Add ACME provisioner to ca.json:
# "authority": {
#   "provisioners": [
#     { "type": "ACME", "name": "acme" }
#   ]
# }
# Caddy with internal ACME:
# {
[...]
Command truncated. Copy to view full command.

Step 8: Automate Certificate Renewal

bash
# step-ca certificates expire — automate renewal:
sudo tee /etc/cron.daily/renew-internal-certs >/dev/null <<'EOF'
#!/bin/bash
step ca renew api.crt api.key --provisioner admin@example.com --force
systemctl reload nginx
EOF
sudo chmod +x /etc/cron.daily/renew-internal-certs

mTLS Between Two Services

Mutual TLS requires both client and server to present certificates. The server verifies the client certificate against the root CA, and the client verifies the server certificate — both sides authenticate each other.

bash
# Issue client certificate:
step ca certificate client-app.internal client.crt client.key \
  --provisioner admin@example.com --not-after 720h

# Server requires client cert (Nginx):
# ssl_verify_client on;

# Client presents cert (curl test):
curl --cert client.crt --key client.key \
  --cacert root_ca.crt https://api.internal.example.com/health

Production Checklist

  • Back up /opt/step-ca/secrets offline — losing the CA key invalidates all issued certs.
  • Use short certificate lifetimes (30–90 days) with automated renewal.
  • Restrict step-ca API access to internal network only.
  • Document certificate revocation procedure.
  • Monitor certificate expiry dates — alert 14 days before expiration.

"A private CA is only valuable when every client trusts the root, every cert auto-renews, and the CA key is backed up like the crown jewels."

Technical Author

Technical Author - Liam K.
Liam K.

System administrator and technical writer specializing in server infrastructure, security and deployment. Creating comprehensive guides to help you master server administration.