PROJECTIntermediate

Step-CA: Build a Private Certificate Authority for Your Homelab

Stop accepting self-signed certificate warnings. Deploy Smallstep's step-ca as a fully automated internal PKI — complete with ACME support, Traefik...

Dylan H.

Projects

July 29, 2026
9 min read
3-5 hours
Step-CA: Build a Private Certificate Authority for Your Homelab

Tools & Technologies

step-castep CLIDockerDocker ComposeTraefikOpenSSL

Overview

Every homelab eventually hits the same wall: self-signed certificate warnings in every browser, curl commands needing --insecure, and services that refuse to talk to each other without valid TLS. The usual workarounds — accepting warnings, disabling verification — are both annoying and genuinely bad security habits.

Smallstep's step-ca is an open-source certificate authority server that solves this properly. It implements a full PKI stack with a root CA, intermediate CA, and support for the ACME protocol (the same one Let's Encrypt uses). Your homelab services can request and auto-renew TLS certificates from your internal CA, browsers stop complaining once you install the root cert, and you get real certificate lifecycle management instead of one-off hacks.

This project walks through standing up step-ca in Docker, configuring an ACME provisioner, wiring it to Traefik so all your reverse-proxied services get valid certs automatically, and distributing the root certificate to clients. By the end your internal domains will show a proper padlock — no warnings, no exceptions.

Architecture

The PKI hierarchy follows standard two-tier CA design:

Root CA (offline-capable)
    └── Intermediate CA  ← step-ca serves this
            ├── ACME Provisioner  ← used by Traefik + any ACME client
            ├── JWK Provisioner   ← used by step CLI for manual issuance
            └── Issued Certs      ← *.homelab.local, service.internal, etc.

Components:

ComponentRole
step-ca containerCA server — issues and renews certificates
step CLIManagement tool — initialize CA, issue certs manually
TraefikReverse proxy — requests certs via ACME from step-ca
Client systemsTrust the root CA cert added to their trust store

Certificate flow for Traefik:

  1. Traefik starts and sees a domain needs a cert
  2. It contacts step-ca's ACME endpoint (https://step-ca:9000/acme/acme/directory)
  3. step-ca issues an HTTP-01 or TLS-ALPN-01 challenge
  4. Traefik completes the challenge and receives a signed cert
  5. Traefik stores the cert and renews ~2/3 through the validity period automatically

Ports:

  • 9000 — step-ca HTTPS API + ACME endpoint (internal network only)

Prerequisites

  • Docker + Docker Compose
  • Traefik already running (or follow the Traefik project in this series)
  • A local domain you control (e.g. homelab.local, home.arpa, or a real domain you own)
  • Access to add certificates to your client machines' trust stores

Build Instructions

Step 1: Install the step CLI

The step CLI is used to initialize the CA, inspect certificates, and issue certs manually. Install it on the Docker host:

# Linux (amd64)
wget https://dl.smallstep.com/gh-release/cli/gh-release-header/v0.27.4/step_linux_0.27.4_amd64.tar.gz
tar xzf step_linux_0.27.4_amd64.tar.gz
sudo mv step_0.27.4/bin/step /usr/local/bin/
step version

Or via Docker (if you prefer not to install locally):

docker run -it --rm -v /opt/step-ca:/home/step smallstep/step-ca step version

Step 2: Initialize the CA

Create a directory to hold CA data, then run step ca init to generate the root CA, intermediate CA, and initial config:

mkdir -p /opt/step-ca
docker run -it --rm \
  -v /opt/step-ca:/home/step \
  -e DOCKER_STEPCA_INIT_NAME="Homelab CA" \
  -e DOCKER_STEPCA_INIT_DNS_NAMES="step-ca,localhost" \
  -e DOCKER_STEPCA_INIT_REMOTE_MANAGEMENT="true" \
  -e DOCKER_STEPCA_INIT_ACME="true" \
  smallstep/step-ca

This generates:

  • /opt/step-ca/certs/root_ca.crt — root certificate (distribute to clients)
  • /opt/step-ca/certs/intermediate_ca.crt — intermediate (step-ca serves this)
  • /opt/step-ca/secrets/ — encrypted private keys
  • /opt/step-ca/config/ca.json — CA configuration

Save the fingerprint printed at the end — you need it to bootstrap clients:

Root fingerprint: abc123def456...

Step 3: Configure the CA

Edit /opt/step-ca/config/ca.json to tune certificate lifetimes and ensure the ACME provisioner is present. The init script adds ACME automatically when DOCKER_STEPCA_INIT_ACME=true, but verify:

cat /opt/step-ca/config/ca.json

Look for the ACME provisioner block:

{
  "type": "ACME",
  "name": "acme",
  "forceCN": false,
  "claims": {
    "maxTLSCertDuration": "2160h",
    "defaultTLSCertDuration": "2160h"
  }
}

Adjust defaultTLSCertDuration to control cert lifetime. 2160h = 90 days (matching Let's Encrypt). For tighter security on internal certs, consider 720h (30 days) — Traefik auto-renews anyway.

Also add your internal domains to the DNS names in the CA config if needed. The default config allows any domain in ACME challenges.

Step 4: Create the Password File

step-ca encrypts its private keys and needs the password at startup:

# You were prompted for a password during init — store it
echo "your-ca-password-here" > /opt/step-ca/secrets/password.txt
chmod 600 /opt/step-ca/secrets/password.txt

In production, use Docker secrets or an environment-variable manager instead of a plain text file.

Step 5: Deploy step-ca with Docker Compose

Create /opt/step-ca/docker-compose.yml:

version: "3.8"
 
services:
  step-ca:
    image: smallstep/step-ca:latest
    container_name: step-ca
    restart: unless-stopped
    ports:
      - "9000:9000"
    volumes:
      - /opt/step-ca:/home/step
    environment:
      STEPCA_INIT_PASSWORD_FILE: /home/step/secrets/password.txt
    networks:
      - proxy
    labels:
      - "traefik.enable=false"   # Don't proxy the CA itself through Traefik
 
networks:
  proxy:
    external: true

Start it:

cd /opt/step-ca
docker compose up -d
docker compose logs -f step-ca

You should see:

2026/07/29 00:00:00 Serving HTTPS on :9000 ...

Step 6: Verify the CA is Running

# Check health endpoint
curl --insecure https://localhost:9000/health
 
# Get the CA root certificate
step ca root root_ca.crt --ca-url https://localhost:9000 \
  --fingerprint <your-fingerprint-here>
 
# Inspect it
step certificate inspect root_ca.crt

Step 7: Configure Traefik to Use step-ca for ACME

In Traefik's static config (traefik.yml or equivalent), add a new certificate resolver pointing at your step-ca ACME endpoint:

# traefik.yml (static config)
certificatesResolvers:
  internal:
    acme:
      email: admin@homelab.local
      storage: /certs/internal-acme.json
      caServer: https://step-ca:9000/acme/acme/directory
      # Use TLS-ALPN-01 challenge (no port 80 needed)
      tlsChallenge: {}

Because step-ca uses your internal root CA (not a public trusted root), Traefik needs to trust it. Add your root certificate to Traefik's container trust store:

# docker-compose.yml for Traefik
services:
  traefik:
    image: traefik:v3.1
    environment:
      # Tell Go's crypto to include your root CA
      LEGO_CA_CERTIFICATES: /certs/root_ca.crt
    volumes:
      - /opt/step-ca/certs/root_ca.crt:/certs/root_ca.crt:ro
      - /etc/traefik/certs:/certs

Then point any Docker service at the internal resolver:

# Labels on any service container
labels:
  - "traefik.enable=true"
  - "traefik.http.routers.myapp.rule=Host(`myapp.homelab.local`)"
  - "traefik.http.routers.myapp.tls=true"
  - "traefik.http.routers.myapp.tls.certresolver=internal"

Restart Traefik and watch the logs — it will contact step-ca and obtain a certificate within seconds.

Step 8: Distribute the Root Certificate to Clients

Browsers and operating systems only trust certificates signed by a CA in their trust store. Add your root CA to all client machines.

Linux (Debian/Ubuntu):

sudo cp root_ca.crt /usr/local/share/ca-certificates/homelab-root.crt
sudo update-ca-certificates

Linux (RHEL/Fedora/Arch):

sudo cp root_ca.crt /etc/pki/ca-trust/source/anchors/homelab-root.crt
sudo update-ca-trust extract

macOS:

sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain root_ca.crt

Windows (PowerShell as Administrator):

Import-Certificate -FilePath "root_ca.crt" `
  -CertStoreLocation "Cert:\LocalMachine\Root"

Firefox (uses its own trust store — add manually): Go to Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import.

Docker containers / CI pipelines:

COPY root_ca.crt /usr/local/share/ca-certificates/homelab-root.crt
RUN update-ca-certificates

Step 9: Issue Certificates Manually (Optional)

For services that don't support ACME, issue certificates directly with the step CLI:

# Bootstrap the step CLI to trust your CA
step ca bootstrap \
  --ca-url https://localhost:9000 \
  --fingerprint <your-fingerprint>
 
# Issue a cert (prompts for JWK provisioner password)
step ca certificate myservice.homelab.local myservice.crt myservice.key
 
# Issue a wildcard cert
step ca certificate "*.homelab.local" wildcard.crt wildcard.key \
  --san "*.homelab.local" --san "homelab.local"
 
# Inspect the issued cert
step certificate inspect myservice.crt
 
# Renew a cert
step ca renew myservice.crt myservice.key

Step 10: Enable SSH Certificate Authority (Bonus)

step-ca can also act as an SSH certificate authority — a major upgrade over authorized_keys files:

# During init, use --ssh flag (or add an SSH provisioner manually)
step ca provisioner add sshpop --type SSHPOP
 
# Issue an SSH user certificate
step ssh certificate your-username id_ed25519.pub
 
# The issued cert is valid for a limited time and inherits SSH principals
# On each server, configure sshd to trust the CA:
step ssh config --host --set IdentityFile=/etc/ssh/ssh_host_ecdsa_key >> /etc/ssh/sshd_config

Testing

Verify Traefik Certs

# Confirm step-ca issued the cert (not a self-signed fallback)
echo | openssl s_client -connect myapp.homelab.local:443 2>/dev/null \
  | openssl x509 -noout -issuer -dates
 
# Should show:
# issuer=O=Homelab CA, CN=Homelab CA Intermediate CA
# notBefore=...
# notAfter=...  (90 days out)

Verify Browser Trust

Navigate to https://myapp.homelab.local — the padlock should be green with no warnings. Click the padlock and inspect the certificate chain:

myapp.homelab.local
  └── Homelab CA Intermediate CA
        └── Homelab CA Root CA  ← your root, trusted by OS

Check Certificate Renewal

Traefik renews certificates automatically when they pass 2/3 of their lifetime. To force a test:

# Temporarily issue a very short cert (5 minute lifetime)
step ca certificate test.homelab.local test.crt test.key \
  --not-after 5m
 
# Watch Traefik logs for renewal attempt
docker logs traefik -f | grep -i acme

Check CA Health

# Built-in health endpoint
curl https://localhost:9000/health
 
# List active certificates (requires admin credentials)
step ca admin list --ca-url https://localhost:9000

Deployment Notes

Backup your CA data. Everything in /opt/step-ca is irreplaceable — if you lose the private keys, you must reissue every certificate and redistribute a new root to every client.

# Minimal backup — run before any CA changes
tar czf step-ca-backup-$(date +%Y%m%d).tar.gz /opt/step-ca

Firewall the CA port. Port 9000 should be accessible only from your internal network — never exposed to the internet. Your internal CA should not be reachable from outside your homelab.

# UFW example — allow only LAN
sudo ufw allow from 192.168.1.0/24 to any port 9000

Monitor certificate expiry. Even with auto-renewal, set up an external expiry check:

# Add to crontab or a monitoring script
openssl s_client -connect myapp.homelab.local:443 \
  -servername myapp.homelab.local 2>/dev/null \
  | openssl x509 -noout -enddate

Extensions and Next Steps

mTLS between services: Issue client certificates to individual services and configure them to present certs to each other. Any service that can't present a valid cert from your CA is rejected — strong zero-trust segmentation inside your homelab.

ACME clients beyond Traefik: Any ACME-compatible tool (certbot, acme.sh, cert-manager in Kubernetes) can use your internal CA by pointing at https://step-ca:9000/acme/acme/directory.

Kubernetes cert-manager integration: Install cert-manager in your k3s/k8s cluster and configure an Issuer pointing at step-ca. Every pod can then request its own certificate automatically.

Step-CA RA mode: Run step-ca in Registration Authority mode backed by a Hardware Security Module (HSM) like a YubiKey — the private key never lives on disk.

OIDC provisioner: Connect step-ca to an identity provider (Authentik, Keycloak) so users authenticate via SSO before receiving a certificate. Pairs perfectly with the Keycloak SSO project in this series.

Automated client provisioning: Write an Ansible playbook that bootstraps new VMs into your PKI on first boot — install the root cert, configure the step CLI, and request service certificates automatically.

Running your own CA is a significant upgrade to homelab security maturity. Once it's in place, every TLS handshake inside your network is verified against a CA you control — not a browser warning you've trained yourself to click through.