How to Run a Local Certificate Authority (Real Padlocks for Internal Names with mkcert and step-ca)

No more 'Your connection is not private' at home: your own certificate authority, trusted by every device, for internal names.

Let's Encrypt (tutorial 27) needs a public domain. Inside the house you've got nas.home.lan, pihole.lan, Cockpit on an IP — all with browser warnings, and some things (service workers, HomeKit, Vaultwarden's apps) simply refuse plain HTTP. A local certificate authority fixes it: you create one root certificate, install it once on each device, and every certificate it issues shows a real padlock on the LAN. mkcert does the quick version in five minutes; step-ca does the proper version with automatic renewal.

Quick reference

ToolBest forRenewal
mkcerta handful of internal names, set up oncemanual (certificates last ~2 years)
step-camany services, Caddy/Certbot/Traefik renewing by ACMEautomatic (24-hour certs)
Caddy tls internalthe lazy middle: Caddy runs its own CA (tutorial 57)automatic
Wildcard via DNS challengereal Let's Encrypt for internal names under a public domain (tutorial 147)automatic

Option 4 is often the best answer if you own a domain; this tutorial is for names with no public domain, and for understanding what the others do.

Part A: mkcert

1. Create the CA and a certificate

On the server:

sudo apt install libnss3-tools -y
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64 && sudo mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert
mkcert -install
mkcert -cert-file /etc/ssl/local/home.lan.pem -key-file /etc/ssl/local/home.lan-key.pem "*.home.lan" home.lan 192.168.1.50

(sudo mkdir -p /etc/ssl/local first.) -install creates the root in ~/.local/share/mkcert/ and trusts it on this machine. The second command issues a wildcard for *.home.lan plus the server's IP — one certificate for every internal service.

2. Use it in Nginx or Caddy

Nginx server block for an internal name:

listen 443 ssl;
server_name nas.home.lan;
ssl_certificate /etc/ssl/local/home.lan.pem;
ssl_certificate_key /etc/ssl/local/home.lan-key.pem;

Caddy: nas.home.lan { tls /etc/ssl/local/home.lan.pem /etc/ssl/local/home.lan-key.pem reverse_proxy ... }. Make sure Pi-hole (34) resolves *.home.lan to the server.

3. Trust the root on every device

Copy rootCA.pem from ~/.local/share/mkcert/ (never the -key file) to each device:

Open https://nas.home.lan: padlock. Repeat for every device once; new certificates from the same CA are trusted automatically.

Keep rootCA-key.pem safe (it's a chmod 600 file in your home folder) — anyone with it can impersonate any site to your devices. Back it up encrypted; don't put it on the server's shared folders.

Part B: step-ca (automatic renewal)

mkcert certificates last two years and you reissue by hand. step-ca is a real CA server that speaks ACME, so Caddy, Certbot and Traefik renew from it exactly as they do from Let's Encrypt.

mkdir -p ~/docker/step-ca && cd ~/docker/step-ca
nano compose.yaml

services:
step-ca:
image: smallstep/step-ca:latest
container_name: step-ca
ports:
- "9000:9000"
environment:
DOCKER_STEPCA_INIT_NAME: Home CA
DOCKER_STEPCA_INIT_DNS_NAMES: ca.home.lan,192.168.1.50
DOCKER_STEPCA_INIT_PROVISIONER_NAME: admin
DOCKER_STEPCA_INIT_PASSWORD: ${CA_PASSWORD}
volumes:
- ./data:/home/step
restart: unless-stopped

docker compose up -d
docker compose logs step-ca | grep -i fingerprint # note it
docker compose exec step-ca step ca provisioner add acme --type ACME
docker compose restart step-ca

The root certificate is ./data/certs/root_ca.crt — install it on devices exactly as in Part A, step 3.

Then point your ACME clients at it. Caddy, globally:

{
acme_ca https://ca.home.lan:9000/acme/acme/directory
acme_ca_root /etc/ssl/local/root_ca.crt
}
nas.home.lan { reverse_proxy 127.0.0.1:9443 }

Certbot: sudo certbot certonly --standalone -d nas.home.lan --server https://ca.home.lan:9000/acme/acme/directory, with REQUESTS_CA_BUNDLE=/etc/ssl/local/root_ca.crt in the environment. Certificates last 24 hours and renew themselves; nothing to remember.

Troubleshooting

Still "not private" on one device. The root isn't trusted there (iOS needs the second step under Certificate Trust Settings; Firefox has its own store).

Android app ignores the CA. Apps can opt out of user-installed CAs; nothing to do but use a public name with a real certificate (tutorial 147).

step-ca ACME fails: "unable to verify". The client doesn't trust step-ca's root — set acme_ca_root / REQUESTS_CA_BUNDLE.

Lost the CA key. Make a new CA and re-trust it everywhere; the old certificates keep working until expiry.

Wrapping up

mkcert for a wildcard on *.home.lan in five minutes and the root on each device once; step-ca when you want ACME renewal inside the house. The padlock stops lying on the LAN, the picky apps start working, and the only secret to guard is one root key file.

Call us