Skip to content

Latest commit

 

History

History
277 lines (179 loc) · 7.1 KB

File metadata and controls

277 lines (179 loc) · 7.1 KB

Homelab PKI with step-ca + ACME (your own internal Let’s Encrypt)

YouTube Video


Overview

Public TLS is easy because Let’s Encrypt exists.

Internal TLS is usually pain:

  • self-signed certs everywhere
  • browser warnings
  • manual renewals
  • random “works on my machine” trust stores

This guide shows how to run step-ca as your homelab Certificate Authority and expose it via ACME, so your services can get automatic, renewable TLS certs like they would with Let’s Encrypt.

Goal: “green lock” in the homelab, with copy-paste steps.


What you need

  • Docker + Docker Compose
  • A stable internal DNS name for your CA, example: ca.hse.local
  • A stable internal DNS pattern for services, example: app1.hse.local
  • One ACME client or reverse proxy that can talk ACME (Traefik, Caddy, certbot, etc.)

Design choices (what we are building)

1. Proper CA chain

  • Root CA: offline trust anchor (you install this cert in your devices)
  • Intermediate CA: online signer that issues your service certificates

This is the standard layout, and it is what step-ca bootstraps by default.

2. ACME for automation

We enable an ACME provisioner so anything ACME-capable can request and renew certs automatically.

3. Keep the CA internal

Do not publish your CA to the internet. It should be reachable only from your LAN or your VPN.


Step 1: Run step-ca in Docker Compose (persistent)

Create docker-compose.yml:

services:
  step-ca:
    image: smallstep/step-ca:latest
    container_name: step-ca
    ports:
      - "9000:9000"
    environment:
      - DOCKER_STEPCA_INIT_NAME=HomeLab CA
      - DOCKER_STEPCA_INIT_DNS_NAMES=ca.hse.local,localhost
    volumes:
      - step:/home/step
    restart: unless-stopped

volumes:
  step:

Start it:

docker compose up -d
  • DOCKER_STEPCA_INIT_NAME = Issuer name
  • DOCKER_STEPCA_INIT_DNS_NAMES = hostnames/IPs your CA will accept requests on

Get the generated CA password (save it).

docker compose exec step-ca cat secrets/password

This is just to get started. For “real” setups, treat that password like a secret.


Step 2: Distribute the Root CA certificate (trust)

This is the part that removes browser warnings.

You will:

  1. export the Root CA certificate from the step-ca volume
  2. copy it to every client
  3. install it into the OS trust store
  4. in some browsers you have to install the Root CA separately

You do not install the private key anywhere. Only the public Root CA cert (root_ca.crt).

  1. Export root_ca.crt from your step-ca host

    • On the host where step-ca runs:

      • mkdir -p ~/pki
        docker compose exec step-ca cat certs/root_ca.crt > ~/pki/root_ca.crt
    • Verify you got a certificate:

      • openssl x509 -in ~/pki/root_ca.crt -noout -subject -issuer -dates
  2. Copy the Root CA cert to a client

  3. Install Root CA cert on Client/Browser

    • e.g. Debian/Ubuntu

      • sudo install -D -m 0644 /tmp/root_ca.crt /usr/local/share/ca-certificates/homelab-root-ca.crt
        sudo update-ca-certificates
      • Quick test: openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /usr/local/share/ca-certificates/homelab-root-ca.crt

Test from the client (reach the CA over HTTPS):

curl -v https://ca.hse.local:9000/health

Replace the hostname if needed

You want:

  • no TLS trust error
  • an HTTP 200 from /health

If you get a TLS error, your trust store install did not take effect (or you installed the wrong file).


Step 3: Enable ACME in step-ca

Add an ACME provisioner named acme:

docker compose exec step-ca step ca provisioner add acme --type ACME

Then restart the step-ca container so it loads the updated config:

docker compose restart step-ca

Quick check (optional): list provisioners and confirm acme exists:

docker compose exec step-ca step ca provisioner list

Step 4: Your ACME Directory URL (this matters)

ACME clients need the “directory URL” in this format:

https://{ca-host-fqdn}:{port}/acme/{provisioner-name}/directory

If your CA is ca.hse.local:9000 and the provisioner is acme, your directory URL is:

https://ca.hse.local:9000/acme/acme/directory

Step 5: Request cerificate via ACME

Traefik

Traefik can use a custom ACME server via caServer, and it can trust your CA via caCertificates.

  1. Traefik static config (YAML example):

    • command:
        # ...
        # ACME with step-ca
        - "--certificatesresolvers.homelab.acme.caserver=https://ca.hse.local:9000/acme/acme/directory"
        - "--certificatesresolvers.homelab.acme.cacertificates=/etc/ssl/homelab/root_ca.crt"
        - "--certificatesresolvers.homelab.acme.storage=/acme/acme.json"
        - "--certificatesresolvers.homelab.acme.tlschallenge=true"
    • tlsChallenge (TLS-ALPN-01) is the easiest when Traefik is the thing terminating TLS.

  2. Docker Compose tip

    • Mount your root cert into Traefik:

      • services:
          traefik:
            image: traefik:v3.3
            volumes:
              # Root CA cert from Step 2
              - ~/pki/root_ca.crt:/etc/ssl/homelab/root_ca.crt:ro
              # Persistent ACME storage
              - ./acme:/acme
            # plus your normal Traefik config...
  3. In your app Compose:

  • labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`app1.hse.local`)"
      - "traefik.http.routers.whoami.entrypoints=websecure"
      - "traefik.http.routers.whoami.tls=true"
      - "traefik.http.routers.whoami.tls.certresolver=homelab"

Trust distribution

If you want no browser warnings on laptops, phones, and desktops:

  • export your root_ca.crt
  • install it into each device’s trust store and browser (or your MDM, or your homelab bootstrap script)

If you do not distribute trust, everything still “works”, but every client will complain forever.

Be intentional. Installing a root CA is a global trust action.


Security checklist (quick wins)

Keep the CA off the public internet

  • firewall it
  • expose it only on LAN or VPN

Back up the CA volume

Your step-ca Docker volume contains critical state (keys, config, DB). Back it up like you would a password manager.

Short lifetimes, automatic renewals

Short-lived certs reduce blast radius, but only if your renewals actually work.


Common “it doesn’t work” fixes

ACME client cannot connect to the CA (TLS error)

  • you did not trust the root CA for the HTTPS connection
  • fix by: providing a CA bundle to the client (Traefik caCertificates, Caddy acme_ca_root, certbot REQUESTS_CA_BUNDLE)

Wrong ACME directory URL

Remember the pattern:

https://{ca-host-fqdn}:{port}/acme/{provisioner-name}/directory

DNS names do not match

Your CA must be initialized with DNS names it will accept requests on (DOCKER_STEPCA_INIT_DNS_NAMES). If you forgot your real CA hostname, re-init in a clean volume and do it right.