Skip to content

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

  1. Caddy detects HTTPS is needed
  2. Requests certificate from Let's Encrypt
  3. Completes HTTP-01 challenge automatically
  4. 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.

Caddy → Cloudflare API → _acme-challenge TXT record → Let's Encrypt validates → cert issued

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 augusto@cronova.dev
}

(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:

  1. Cloudflare Dashboard → SSL/TLS → Origin Server
  2. Create Certificate (15 years validity)
  3. Install on VPS
  4. Configure Caddy to use origin cert
# If using Cloudflare origin cert
<BUSINESS_DOMAIN> {
    tls /etc/ssl/cloudflare/<BUSINESS_DOMAIN>.pem /etc/ssl/cloudflare/<BUSINESS_DOMAIN>.key
    root * /var/www/<business>
    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
Edge Cloudflare N/A Yes
docs.cronova.dev Edge Cloudflare N/A Yes
jara.cronova.dev Let's Encrypt Docker VM Caddy DNS-01 (CF) Yes
yrasema.cronova.dev Let's Encrypt Docker VM Caddy DNS-01 (CF) Yes
taguato.cronova.dev Let's Encrypt Docker VM Caddy DNS-01 (CF) Yes
japysaka.cronova.dev Let's Encrypt Docker VM Caddy DNS-01 (CF) Yes
taanga.cronova.dev Let's Encrypt Docker VM Caddy DNS-01 (CF) Yes
aoao.cronova.dev Let's Encrypt Docker VM Caddy DNS-01 (CF) Yes
tajy.cronova.dev Let's Encrypt NAS Traefik (Coolify) DNS-01 (CF) 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/<BUSINESS_DOMAIN>.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

Reference