Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsTraining
StudyProjectsNewsletterHire MeAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Training
Study
Projects
Newsletter
Hire Me
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

2112+ Articles
156+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Checklists
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. Projects
  3. Step-CA: Build a Private Certificate Authority for Your Homelab
Step-CA: Build a Private Certificate Authority for Your Homelab
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 integration, and browser trust.

Dylan H.

Projects

July 29, 2026
9 min read
3-5 hours

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.

#PKI#TLS#Homelab#Docker#Traefik#Security#Certificates

Related Articles

Building a Production-Ready Reverse Proxy with Traefik v3

Deploy Traefik v3 as a Docker-native reverse proxy with automatic Let's Encrypt TLS, label-based routing, and security middleware — no more port juggling...

10 min read

OpenCTI: Building a Cyber Threat Intelligence Platform in Your Homelab

Deploy OpenCTI to aggregate, correlate, and visualize threat intelligence from MISP, NVD, AlienVault OTX, and Shodan — all in a self-hosted Docker stack.

12 min read

Building a Wazuh XDR + SIEM Homelab

Deploy a full Wazuh stack in Docker to gain host-based intrusion detection, file integrity monitoring, vulnerability scanning, and active response across your…

14 min read
Back to all Projects