SSL Certificate Management for Your Home Lab
Nothing says "homelab in progress" like clicking through browser warnings every time you access your own services. SSL certificates fix that, but managing certificates across a homelab is genuinely tricky. You've got a mix of public-facing services, internal-only services, services on weird ports, and maybe some that are only accessible through a VPN. Each scenario calls for a different approach to certificates.
This guide covers the full landscape: Let's Encrypt for public services, internal CAs for private ones, wildcard certificates for convenience, and the tooling that ties it all together. By the end, you'll have a clear strategy for getting valid HTTPS on every service in your lab.
The Two Worlds: Public vs Internal Certificates
The first thing to understand is that certificate management splits into two distinct problems:
Public certificates are issued by a trusted Certificate Authority (like Let's Encrypt) and are automatically trusted by every browser and operating system. You need these for any service that's accessed from the public internet or where you don't control the client devices.
Internal certificates are issued by your own private CA and are only trusted by devices you've configured to trust that CA. These are better for services that never touch the internet — your NAS admin panel, internal dashboards, Proxmox UI, and so on.
You probably need both. Let's Encrypt for your publicly accessible services. An internal CA for everything else.
Let's Encrypt with DNS Challenges
Let's Encrypt is the standard for free, automated SSL certificates. The catch for homelabs: the default HTTP challenge requires your server to be reachable from the internet on port 80. Many homelabbers don't expose port 80, and even those who do have services behind reverse proxies that complicate things.
DNS challenges solve this. Instead of proving you own a domain by serving a file over HTTP, you prove it by creating a specific DNS TXT record. This works even if the server is completely offline — you just need API access to your DNS provider.
Certbot with DNS Challenge
Certbot is the original ACME client and still the most common:
# Install certbot and your DNS provider's plugin
sudo apt install certbot python3-certbot-dns-cloudflare
# Create credentials file for Cloudflare
mkdir -p ~/.secrets/certbot
cat > ~/.secrets/certbot/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = your-cloudflare-api-token-here
EOF
chmod 600 ~/.secrets/certbot/cloudflare.ini
# Request a certificate
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
-d "lab.example.com" \
-d "*.lab.example.com"
Certbot supports DNS plugins for most major providers: Cloudflare, Route53, DigitalOcean, Google Cloud DNS, Hetzner, and others. The python3-certbot-dns-<provider> package naming convention makes them easy to find.
Certificates land in /etc/letsencrypt/live/lab.example.com/:
fullchain.pem— The certificate plus intermediate chain (use this for your web server)privkey.pem— The private keycert.pem— Just the certificatechain.pem— Just the intermediate chain
acme.sh — The Flexible Alternative
acme.sh is a pure shell ACME client that supports over 150 DNS providers and doesn't require root:
# Install acme.sh
curl https://get.acme.sh | sh
# Set DNS provider credentials (Cloudflare example)
export CF_Token="your-cloudflare-api-token"
# Issue a wildcard certificate
~/.acme.sh/acme.sh --issue \
--dns dns_cf \
-d "lab.example.com" \
-d "*.lab.example.com"
# Install the certificate to a specific location
~/.acme.sh/acme.sh --install-cert \
-d "lab.example.com" \
--cert-file /opt/certs/cert.pem \
--key-file /opt/certs/key.pem \
--fullchain-file /opt/certs/fullchain.pem \
--reloadcmd "systemctl reload nginx"
acme.sh advantages over certbot:
- No root required
- Supports more DNS providers out of the box
- Pure shell — no Python dependencies
- The
--reloadcmdflag automatically restarts your web server after renewal - Can deploy certificates to routers, NAS devices, and other targets via deploy hooks
lego — The Go Alternative
lego is a single binary ACME client written in Go. No dependencies, no installation, just download and run:
# Download lego
wget https://github.com/go-acme/lego/releases/download/v4.20.4/lego_v4.20.4_linux_amd64.tar.gz
tar xzf lego_v4.20.4_linux_amd64.tar.gz
sudo mv lego /usr/local/bin/
# Issue a certificate (Cloudflare example)
CLOUDFLARE_DNS_API_TOKEN=your-token \
lego --email you@example.com \
--dns cloudflare \
--domains "lab.example.com" \
--domains "*.lab.example.com" \
run
lego is a great choice if you want a single binary with no dependencies. It supports the same wide range of DNS providers and produces the same certificate files.
Wildcard Certificates
A wildcard certificate covers *.lab.example.com — any subdomain, one level deep. This is extremely convenient for homelabs where you might have dozens of services on subdomains (grafana.lab.example.com, nas.lab.example.com, proxmox.lab.example.com, etc.).
Wildcard certificates require the DNS challenge. You cannot get a wildcard cert via the HTTP challenge.
# Certbot wildcard
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
-d "lab.example.com" \
-d "*.lab.example.com"
One certificate, one renewal process, covers everything under your lab subdomain. Put it in your reverse proxy (Traefik, Nginx Proxy Manager, Caddy) and every proxied service gets HTTPS automatically.
Limitation: Wildcards are single-level. *.lab.example.com covers grafana.lab.example.com but not api.grafana.lab.example.com. If you need nested subdomains, you'll need additional certificates or a wildcard at each level.
Internal CA with step-ca
For services that don't need publicly trusted certificates — your Proxmox web UI, internal admin panels, development tools — an internal CA is cleaner than Let's Encrypt. You create your own CA, distribute its root certificate to your devices, and then issue certificates for any internal hostname or IP address.
step-ca is the best tool for this in 2026. It's an open-source ACME server that runs on your own infrastructure, supports automated certificate issuance and renewal via the same ACME protocol that Let's Encrypt uses, and has a clean CLI.
Install step-ca
# Install the step CLI and step-ca
wget https://dl.smallstep.com/gh-release/cli/docs-cli-install/v0.27.5/step-cli_0.27.5_amd64.deb
wget https://dl.smallstep.com/gh-release/certificates/docs-ca-install/v0.27.5/step-ca_0.27.5_amd64.deb
sudo dpkg -i step-cli_0.27.5_amd64.deb step-ca_0.27.5_amd64.deb
Initialize the CA
# Initialize a new CA
step ca init \
--name "Homelab CA" \
--provisioner admin \
--dns ca.lab.local \
--address :8443 \
--deployment-type standalone
# This creates:
# - Root CA certificate: ~/.step/certs/root_ca.crt
# - Intermediate CA certificate: ~/.step/certs/intermediate_ca.crt
# - Private keys in ~/.step/secrets/
# - Configuration in ~/.step/config/ca.json
Enable ACME Provisioner
Add an ACME provisioner so clients can request certificates automatically:
step ca provisioner add acme --type ACME
Run step-ca as a Service
# /etc/systemd/system/step-ca.service
[Unit]
Description=Smallstep CA Server
After=network.target
[Service]
Type=simple
User=step
Environment=STEPPATH=/etc/step-ca
ExecStart=/usr/bin/step-ca /etc/step-ca/config/ca.json --password-file /etc/step-ca/password.txt
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now step-ca
Issue Certificates from Your Internal CA
# Issue a certificate for an internal service
step ca certificate "proxmox.lab.local" proxmox.crt proxmox.key \
--ca-url https://ca.lab.local:8443 \
--root ~/.step/certs/root_ca.crt
# Issue a certificate for an IP address
step ca certificate "192.168.1.10" server.crt server.key \
--ca-url https://ca.lab.local:8443 \
--root ~/.step/certs/root_ca.crt \
--san 192.168.1.10
Trust Your Internal CA
For your browsers and system to trust certificates from your internal CA, install the root certificate:
# Linux (Debian/Ubuntu)
sudo cp ~/.step/certs/root_ca.crt /usr/local/share/ca-certificates/homelab-ca.crt
sudo update-ca-certificates
# macOS
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain ~/.step/certs/root_ca.crt
# Windows (PowerShell as Admin)
Import-Certificate -FilePath root_ca.crt -CertStoreLocation Cert:\LocalMachine\Root
# Firefox (uses its own cert store — import manually)
# Settings > Privacy & Security > Certificates > View Certificates > Import
On phones and tablets, you can usually email the root CA certificate to yourself and open it to install.
mkcert for Quick Development Certificates
If you just need certificates for local development and don't want to run a full CA, mkcert is the simplest tool:
# Install mkcert
sudo apt install mkcert # or brew install mkcert on macOS
# Create and install a local CA
mkcert -install
# Generate certificates for local services
mkcert "*.lab.local" lab.local localhost 127.0.0.1 ::1
# Output:
# _wildcard.lab.local+3.pem (certificate)
# _wildcard.lab.local+3-key.pem (private key)
mkcert creates a CA, installs it in your system trust store, and generates certificates — all in two commands. The limitation: it doesn't automate renewal (certificates are valid for ~2 years by default) and the CA only lives on the machine where you installed mkcert.
Use mkcert when: You're the only person accessing these services and you just want the browser warnings to go away quickly.
Use step-ca when: Multiple people or devices need to trust internal services, or you want automated renewal.
cert-manager for Kubernetes
If your homelab runs Kubernetes, cert-manager is the standard way to manage certificates. It integrates with both Let's Encrypt and internal CAs, automatically provisions certificates for Ingress resources, and handles renewal.
Install cert-manager
# Install with Helm
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=true
Configure a Let's Encrypt Issuer
# letsencrypt-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: you@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
# Create the Cloudflare API token secret
kubectl create secret generic cloudflare-api-token \
--namespace cert-manager \
--from-literal=api-token=your-cloudflare-token
# Apply the issuer
kubectl apply -f letsencrypt-issuer.yaml
Configure a step-ca Issuer
# internal-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: internal-ca
spec:
acme:
server: https://ca.lab.local:8443/acme/acme/directory
privateKeySecretRef:
name: internal-ca-key
caBundle: <base64-encoded-root-ca-cert>
solvers:
- http01:
ingress:
class: nginx
Use in Ingress Resources
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
tls:
- hosts:
- grafana.lab.example.com
secretName: grafana-tls
rules:
- host: grafana.lab.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana
port:
number: 3000
cert-manager sees the annotation, requests a certificate from Let's Encrypt, stores it as a Kubernetes secret, and renews it automatically. You never touch a certificate file.
Automated Renewal
Let's Encrypt certificates expire after 90 days. Your renewal strategy depends on which ACME client you're using.
Certbot Auto-Renewal
Certbot installs a systemd timer automatically:
# Check the renewal timer
systemctl list-timers | grep certbot
# Test renewal without actually renewing
sudo certbot renew --dry-run
# Certbot checks twice daily and renews certificates
# within 30 days of expiration
Add a deploy hook to restart services after renewal:
# /etc/letsencrypt/renewal-hooks/deploy/restart-services.sh
#!/bin/bash
systemctl reload nginx
docker restart traefik
chmod +x /etc/letsencrypt/renewal-hooks/deploy/restart-services.sh
acme.sh Auto-Renewal
acme.sh installs a cron job automatically during setup:
# Check the cron job
crontab -l | grep acme
# You should see something like:
# 0 0 * * * "/home/user/.acme.sh/acme.sh" --cron --home "/home/user/.acme.sh"
The --reloadcmd you specified during --install-cert runs automatically after successful renewal.
step-ca Certificate Renewal
step-ca certificates are short-lived by default (24 hours). Use step ca renew with a daemon or cron:
# Auto-renew a certificate (runs as a daemon, renews before expiry)
step ca renew --daemon server.crt server.key
# Or via cron (renew every 12 hours)
0 */12 * * * step ca renew server.crt server.key --force
Putting It All Together: A Homelab Certificate Strategy
Here's a practical strategy that covers most homelab scenarios:
- Get a domain (e.g., example.com) and point a subdomain to your lab (lab.example.com)
- Use Let's Encrypt with DNS challenge for a wildcard certificate (
*.lab.example.com) - Put the wildcard cert in your reverse proxy (Traefik, Nginx Proxy Manager, Caddy) — every proxied service gets HTTPS automatically
- Run step-ca for internal services that aren't behind the reverse proxy (Proxmox UI, IPMI interfaces, internal APIs)
- Install your internal CA's root certificate on all your devices
- Set up monitoring for certificate expiration — Prometheus has a
probe_ssl_earliest_cert_expirymetric via the blackbox exporter
# Alert if any certificate expires within 14 days
(probe_ssl_earliest_cert_expiry - time()) / 86400 < 14
This way, publicly accessible services have publicly trusted certificates, internal services have properly signed certificates from your own CA, and everything renews automatically. No more clicking through browser warnings, no more expired certificate outages, and no manual certificate management.