Certificate Strategy¶
SSL/TLS certificate management for homelab services.
Decision Summary¶
| Service Type | Method | Provider |
|---|---|---|
| Public (VPS) | Let's Encrypt (HTTP-01) | Caddy ACME | | Public (Static) | Cloudflare | Edge certificates | | Internal (Docker VM) | Let's Encrypt (DNS-01) | Caddy + Cloudflare DNS |
Decision:UseLet's Encrypt via Cloudflare DNS-01 for internal services. No ports open to internet required.
Current Status (as of 2026-02-22)¶
| Component | Status | Notes |
|---|---|---|
| VPS Caddy + Let's Encrypt (HTTP-01) | Deployed | cronova.dev, hs, status, notify | | Cloudflare Edge | Deployed | DNS proxied | | Docker VM Caddy + Let's Encrypt (DNS-01) | Deployed | home, media, frigate, sonarr, radarr, prowlarr | | NAS Traefik + Let's Encrypt (DNS-01) | Planned | tajy (Coolify) |
Previous limitation: tailscale cert doesn't work with self-hosted Headscale.
Solution: Custom Caddy build with caddy-dns/cloudflare plugin. Certificates obtained via DNS-01 challenge through Cloudflare API — no public ports needed.
Why DNS-01 via Cloudflare?¶
| Factor | DNS-01 (Cloudflare) | Internal CA | Tailscale HTTPS |
|---|---|---|---|
| Headscale compatible | Yes | Yes | No | | Setup complexity | Low | Medium | N/A | | Device trust setup | None | Install CA on each device | None | | Auto-renewal | Yes (Caddy) | Manual/scripted | Yes | | Browser warnings | None | None (after CA trust) | None | | Public ports required | No | No | No | | Guest device access | Works | Requires CA install | Works |
Winner: DNS-01 via Cloudflare — browser-trusted certs, no open ports, works with Headscale.
Architecture¶
┌─────────────────────────────────────────────────────────────────────┐
│ INTERNET │
└────────────────────────────────┬────────────────────────────────────┘
│
┌──────────────────┼──────────────────┬──────────────────┐
│ │ │ │
[Cloudflare] [VPS Caddy] [Docker VM Caddy] [NAS Traefik]
Edge Certs LE (HTTP-01) LE (DNS-01 via CF) LE (DNS-01 via CF)
│ │ │ │
┌─────────┴─────────┐ │ ┌──────┴──────┐ │
│ │ │ │ │ │
www.cronova.dev docs.cronova.dev jara.cronova.dev yrasema tajy.cronova.dev
(Cloudflare Pages) taguato.cronova.dev (Coolify apps)
sonarr/radarr/aoao
Public Services (Let's Encrypt)¶
VPS Caddy Configuration¶
Caddy automatically obtains Let's Encrypt certificates.
# Automatic HTTPS - Caddy handles everything
status.cronova.dev {
reverse_proxy localhost:3001
}
notify.cronova.dev {
reverse_proxy localhost:80
}
vault.cronova.dev {
reverse_proxy 100.68.63.168:8843
}
How it works¶
- Caddy detects HTTPS is needed
- Requests certificate from Let's Encrypt
- Completes HTTP-01 challenge automatically
- Renews 30 days before expiry
Requirements¶
- Port 80/443 open to internet
- DNS A record pointing to VPS IP
- Valid email in Caddy global config
Internal Services (DNS-01 via Cloudflare)¶
How It Works¶
Docker VM runs a custom Caddy build with the caddy-dns/cloudflare plugin. Caddy proves domain ownership by creating a DNS TXT record via the Cloudflare API, then Let's Encrypt issues the certificate. No public ports required.
Requirements¶
- Cloudflare API Token with Zone/DNS/Edit + Zone/Zone/Read for cronova.dev
- Pi-hole local DNS:
*.cronova.dev → 192.168.0.10(Docker VM LAN IP) - Custom Caddy image built from
docker/fixed/docker-vm/networking/caddy/Dockerfile
Docker VM Caddy Configuration¶
{
email [email protected]
}
(internal_tls) {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
}
}
jara.cronova.dev {
import internal_tls
reverse_proxy host.docker.internal:8123
}
yrasema.cronova.dev {
import internal_tls
reverse_proxy host.docker.internal:8096
}
taguato.cronova.dev {
import internal_tls
reverse_proxy host.docker.internal:5000
}
Certificate Renewal¶
Caddy handles renewal automatically — no cron jobs needed. Certificates renew 30 days before expiry.
Headscale Note¶
tailscale cert is NOT available with self-hosted Headscale. DNS-01 via Cloudflare is the chosen alternative.
Cloudflare (Static Sites)¶
Edge Certificates¶
Cloudflare provides free edge certificates for:
<www.cronova.dev>(Cloudflare Pages)docs.cronova.dev(Cloudflare Pages)
Settings¶
| Option | Value |
|---|---|
| SSL/TLS Mode | Full (strict) | | Always Use HTTPS | On | | Minimum TLS | 1.2 | | TLS 1.3 | On |
Origin Certificates (VPS)¶
For VPS behind Cloudflare proxy:
- Cloudflare Dashboard → SSL/TLS → Origin Server
- Create Certificate (15 years validity)
- Install on VPS
- Configure Caddy to use origin cert
# If using Cloudflare origin cert
www.verava.ai {
tls /etc/ssl/cloudflare/verava.ai.pem /etc/ssl/cloudflare/verava.ai.key
root * /var/www/verava
file_server
}
Certificate Inventory¶
| Domain | Type | Provider | Challenge | Auto-Renew |
|---|---|---|---|---|
| status.cronova.dev | Let's Encrypt | VPS Caddy | HTTP-01 | Yes |
| notify.cronova.dev | Let's Encrypt | VPS Caddy | HTTP-01 | Yes |
| vault.cronova.dev | Let's Encrypt | VPS Caddy | HTTP-01 | Yes |
|
Monitoring¶
Uptime Kuma SSL Checks¶
Add certificate expiry monitoring:
| Monitor | Type | Alert Threshold |
|---|---|---|
| status.cronova.dev | HTTPS | 14 days | | vault.cronova.dev | HTTPS | 14 days | | jara.cronova.dev | HTTPS | 14 days | | yrasema.cronova.dev | HTTPS | 14 days | | taguato.cronova.dev | HTTPS | 14 days |
Manual Check¶
# Check certificate expiry for any domain
echo | openssl s_client -connect jara.cronova.dev:443 -servername jara.cronova.dev 2>/dev/null | openssl x509 -noout -dates
Troubleshooting¶
Let's Encrypt Issues¶
# Check Caddy logs
journalctl -u caddy -f | grep -i acme
# Force renewal
caddy reload --config /etc/caddy/Caddyfile --force
# Test HTTP challenge
curl http://status.cronova.dev/.well-known/acme-challenge/test
DNS-01 Challenge Issues¶
# Check Caddy logs for certificate errors
docker logs caddy 2>&1 | grep -i "tls\|cert\|acme\|dns"
# Verify Cloudflare token works
curl -X GET "https://api.cloudflare.com/client/v4/zones" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json"
# Force certificate renewal
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
# Check certificate details
echo | openssl s_client -connect jara.cronova.dev:443 -servername jara.cronova.dev 2>/dev/null | openssl x509 -noout -dates -issuer
Cloudflare Issues¶
# Verify SSL mode
# Dashboard → SSL/TLS → Overview → Should show "Full (strict)"
# Check origin cert validity
openssl x509 -in /etc/ssl/cloudflare/verava.ai.pem -noout -dates
Implementation Checklist¶
VPS (Let's Encrypt)¶
- [x] Verify ports 80/443 open
- [x] Configure Caddy with email
- [x] Deploy Caddyfile
- [x] Verify auto-cert:
curl -I <https://status.cronova.dev>
Fixed Homelab (DNS-01 via Cloudflare)¶
- [x] Custom Caddy build with caddy-dns/cloudflare plugin
- [x] Cloudflare API Token (Zone/DNS/Edit + Zone/Zone/Read)
- [x] Pi-hole local DNS entries for *.cronova.dev → 192.168.0.10
- [x] Caddy Caddyfile with DNS-01 TLS snippets
- [x] HTTPS working for home, media, frigate, sonarr, radarr, prowlarr
Cloudflare¶
- [x] Set SSL mode to Full (strict)
- [x] Enable Always Use HTTPS
- [ ] (Optional) Create origin certificate for VPS