Caddy phase 2: TLS via Porkbun DNS-01 wildcard certs
- Caddyfile: drop http:// site prefixes, global acme_dns porkbun block, HTTPS redirects on the bare-IP and legacy vhosts - new dankclaw/caddy/Containerfile: xcaddy build with caddy-dns/porkbun - compose: caddy uses franksdank/caddy-porkbun:latest + Porkbun env vars - deploy.sh: build the image on first deploy if missing - .env.example: rename PORKBUN_API_SECRET -> PORKBUN_API_SECRET_KEY (the name the caddy-dns/porkbun module expects) - tests: 7 new caddyfile/compose seams for the TLS config
This commit is contained in:
1 parent
e9277fa8af
commit
7f4ce06927
6 files changed
+115
-32
No files matched your search
@@ -35,7 +35,9 @@ SIGNAL_ACCOUNT=+15551234567
|
|||||||
# Comma-separated E.164 numbers allowed to message the agent.
|
# Comma-separated E.164 numbers allowed to message the agent.
|
||||||
SIGNAL_ALLOWED_USERS=+15551234567
|
SIGNAL_ALLOWED_USERS=+15551234567
|
||||||
|
|
||||||
# Porkbun API (franksdank.cloud zone). Caddy uses these for DNS-01
|
# Porkbun API (franksdank.cloud + franksdank.casa zones). Caddy uses these
|
||||||
# Let's Encrypt wildcard cert issuance. Scope the token to this zone only.
|
# for DNS-01 Let's Encrypt wildcard cert issuance. Porkbun API keys are
|
||||||
|
# account-wide; rotate them here rather than in the dashboard.
|
||||||
|
# PORKBUN_API_SECRET_KEY is the name the caddy-dns/porkbun module expects.
|
||||||
PORKBUN_API_KEY=changeme
|
PORKBUN_API_KEY=changeme
|
||||||
PORKBUN_API_SECRET=changeme
|
PORKBUN_API_SECRET_KEY=changeme
|
||||||
+40
-28
@@ -15,21 +15,33 @@
|
|||||||
# hash (CADDY_BASICAUTH_HASH from the host .env, git-ignored) into
|
# hash (CADDY_BASICAUTH_HASH from the host .env, git-ignored) into
|
||||||
# /home/dank/Containers/caddy/Caddyfile, which the container mounts.
|
# /home/dank/Containers/caddy/Caddyfile, which the container mounts.
|
||||||
#
|
#
|
||||||
# Phase 1 uses explicit http:// site addresses (no TLS yet). Phase 2 drops
|
# TLS: real Let's Encrypt certs. Site addresses carry no http:// prefix,
|
||||||
# the prefix and adds a wildcard Let's Encrypt cert via DNS-01 (Porkbun).
|
# so Caddy serves HTTPS (and auto-redirects HTTP). Certs are wildcard
|
||||||
|
# (dns-01) via the global acme_dns block below, using the Porkbun module
|
||||||
|
# built into the franksdank/caddy-porkbun image (caddy/Containerfile).
|
||||||
|
# That needs no inbound ports and works from the LAN, where dnsmasq
|
||||||
|
# resolves these names to 192.168.0.143.
|
||||||
|
|
||||||
{
|
{
|
||||||
admin off
|
admin off
|
||||||
email admin@franksdank.cloud
|
email admin@franksdank.cloud
|
||||||
|
|
||||||
|
# Solve ACME challenges for every site via Porkbun DNS (no port 80/443
|
||||||
|
# reachability needed). Credentials come from the container environment
|
||||||
|
# (compose.yaml, from the host .env — never committed).
|
||||||
|
acme_dns porkbun {
|
||||||
|
api_key {env.PORKBUN_API_KEY}
|
||||||
|
api_secret_key {env.PORKBUN_API_SECRET_KEY}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Bare-IP access (health checks, convenience) ---
|
# --- Bare-IP access (health checks, convenience) ---
|
||||||
:80 {
|
:80 {
|
||||||
redir http://dankclaw.franksdank.cloud/ 302
|
redir https://dankclaw.franksdank.cloud/ 302
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Homepage (password protected) ---
|
# --- Homepage (password protected) ---
|
||||||
http://dankclaw.franksdank.cloud {
|
dankclaw.franksdank.cloud {
|
||||||
basicauth {
|
basicauth {
|
||||||
dank $2a$14$REPLACE_WITH_HASH
|
dank $2a$14$REPLACE_WITH_HASH
|
||||||
}
|
}
|
||||||
@@ -38,8 +50,8 @@ http://dankclaw.franksdank.cloud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Legacy name: the media host has no Caddy of its own anymore ---
|
# --- Legacy name: the media host has no Caddy of its own anymore ---
|
||||||
http://dankserver.franksdank.cloud {
|
dankserver.franksdank.cloud {
|
||||||
redir http://dankclaw.franksdank.cloud/ 302
|
redir https://dankclaw.franksdank.cloud/ 302
|
||||||
}
|
}
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
@@ -47,7 +59,7 @@ http://dankserver.franksdank.cloud {
|
|||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
# --- SearXNG (quadlet; behind the Gluetun tunnel, published 8081) ---
|
# --- SearXNG (quadlet; behind the Gluetun tunnel, published 8081) ---
|
||||||
http://search.franksdank.cloud {
|
search.franksdank.cloud {
|
||||||
basicauth {
|
basicauth {
|
||||||
dank $2a$14$REPLACE_WITH_HASH
|
dank $2a$14$REPLACE_WITH_HASH
|
||||||
}
|
}
|
||||||
@@ -55,12 +67,12 @@ http://search.franksdank.cloud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- ComfyUI ---
|
# --- ComfyUI ---
|
||||||
http://comfyui.franksdank.cloud {
|
comfyui.franksdank.cloud {
|
||||||
reverse_proxy comfyui:8188
|
reverse_proxy comfyui:8188
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- OpenWebUI (quadlet on slirp4netns; published 3005) ---
|
# --- OpenWebUI (quadlet on slirp4netns; published 3005) ---
|
||||||
http://chat.franksdank.cloud {
|
chat.franksdank.cloud {
|
||||||
basicauth {
|
basicauth {
|
||||||
dank $2a$14$REPLACE_WITH_HASH
|
dank $2a$14$REPLACE_WITH_HASH
|
||||||
}
|
}
|
||||||
@@ -68,7 +80,7 @@ http://chat.franksdank.cloud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Hermes ---
|
# --- Hermes ---
|
||||||
http://hermes.franksdank.cloud {
|
hermes.franksdank.cloud {
|
||||||
basicauth {
|
basicauth {
|
||||||
dank $2a$14$REPLACE_WITH_HASH
|
dank $2a$14$REPLACE_WITH_HASH
|
||||||
}
|
}
|
||||||
@@ -76,12 +88,12 @@ http://hermes.franksdank.cloud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Gitea (quadlet; published 3000) ---
|
# --- Gitea (quadlet; published 3000) ---
|
||||||
http://gitea.franksdank.cloud {
|
gitea.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.143:3000
|
reverse_proxy 192.168.0.143:3000
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Grafana ---
|
# --- Grafana ---
|
||||||
http://grafana.franksdank.cloud {
|
grafana.franksdank.cloud {
|
||||||
basicauth {
|
basicauth {
|
||||||
dank $2a$14$REPLACE_WITH_HASH
|
dank $2a$14$REPLACE_WITH_HASH
|
||||||
}
|
}
|
||||||
@@ -89,7 +101,7 @@ http://grafana.franksdank.cloud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Prometheus ---
|
# --- Prometheus ---
|
||||||
http://prometheus.franksdank.cloud {
|
prometheus.franksdank.cloud {
|
||||||
basicauth {
|
basicauth {
|
||||||
dank $2a$14$REPLACE_WITH_HASH
|
dank $2a$14$REPLACE_WITH_HASH
|
||||||
}
|
}
|
||||||
@@ -97,7 +109,7 @@ http://prometheus.franksdank.cloud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Loki ---
|
# --- Loki ---
|
||||||
http://loki.franksdank.cloud {
|
loki.franksdank.cloud {
|
||||||
reverse_proxy loki:3100
|
reverse_proxy loki:3100
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,21 +118,21 @@ http://loki.franksdank.cloud {
|
|||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
# --- Plex (existing, kept) ---
|
# --- Plex (existing, kept) ---
|
||||||
http://plex.franksdank.casa {
|
plex.franksdank.casa {
|
||||||
reverse_proxy 192.168.0.235:32400
|
reverse_proxy 192.168.0.235:32400
|
||||||
}
|
}
|
||||||
|
|
||||||
http://plex.franksdank.cloud {
|
plex.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.235:32400
|
reverse_proxy 192.168.0.235:32400
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Calibre-Web ---
|
# --- Calibre-Web ---
|
||||||
http://books.franksdank.cloud {
|
books.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.235:8083
|
reverse_proxy 192.168.0.235:8083
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- RomM ---
|
# --- RomM ---
|
||||||
http://romm.franksdank.cloud {
|
romm.franksdank.cloud {
|
||||||
basicauth {
|
basicauth {
|
||||||
dank $2a$14$REPLACE_WITH_HASH
|
dank $2a$14$REPLACE_WITH_HASH
|
||||||
}
|
}
|
||||||
@@ -128,7 +140,7 @@ http://romm.franksdank.cloud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Copyparty ---
|
# --- Copyparty ---
|
||||||
http://files.franksdank.cloud {
|
files.franksdank.cloud {
|
||||||
basicauth {
|
basicauth {
|
||||||
dank $2a$14$REPLACE_WITH_HASH
|
dank $2a$14$REPLACE_WITH_HASH
|
||||||
}
|
}
|
||||||
@@ -136,7 +148,7 @@ http://files.franksdank.cloud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Jellyfin ---
|
# --- Jellyfin ---
|
||||||
http://jellyfin.franksdank.cloud {
|
jellyfin.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.235:8096 {
|
reverse_proxy 192.168.0.235:8096 {
|
||||||
transport http {
|
transport http {
|
||||||
versions h2c 1.1
|
versions h2c 1.1
|
||||||
@@ -145,36 +157,36 @@ http://jellyfin.franksdank.cloud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- *arr Stack ---
|
# --- *arr Stack ---
|
||||||
http://radarr.franksdank.cloud {
|
radarr.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.235:7878
|
reverse_proxy 192.168.0.235:7878
|
||||||
}
|
}
|
||||||
|
|
||||||
http://sonarr.franksdank.cloud {
|
sonarr.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.235:8989
|
reverse_proxy 192.168.0.235:8989
|
||||||
}
|
}
|
||||||
|
|
||||||
http://lidarr.franksdank.cloud {
|
lidarr.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.235:8686
|
reverse_proxy 192.168.0.235:8686
|
||||||
}
|
}
|
||||||
|
|
||||||
http://readarr.franksdank.cloud {
|
readarr.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.235:8787
|
reverse_proxy 192.168.0.235:8787
|
||||||
}
|
}
|
||||||
|
|
||||||
http://prowlarr.franksdank.cloud {
|
prowlarr.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.235:9696
|
reverse_proxy 192.168.0.235:9696
|
||||||
}
|
}
|
||||||
|
|
||||||
http://qbittorrent.franksdank.cloud {
|
qbittorrent.franksdank.cloud {
|
||||||
reverse_proxy 192.168.0.235:8080
|
reverse_proxy 192.168.0.235:8080
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Optional: Jackett ---
|
# --- Optional: Jackett ---
|
||||||
# http://jackett.franksdank.cloud {
|
# jackett.franksdank.cloud {
|
||||||
# reverse_proxy 192.168.0.235:9117
|
# reverse_proxy 192.168.0.235:9117
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# --- Optional: Whisparr ---
|
# --- Optional: Whisparr ---
|
||||||
# http://whisparr.franksdank.cloud {
|
# whisparr.franksdank.cloud {
|
||||||
# reverse_proxy 192.168.0.235:6969
|
# reverse_proxy 192.168.0.235:6969
|
||||||
# }
|
# }
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Caddy with the Porkbun DNS provider (github.com/caddy-dns/porkbun).
|
||||||
|
#
|
||||||
|
# The porkbun module lets Caddy solve Let's Encrypt DNS-01 challenges
|
||||||
|
# directly through the Porkbun API, so the wildcard cert for
|
||||||
|
# *.franksdank.cloud / *.franksdank.casa can be issued and renewed without
|
||||||
|
# any port 80/443 reachability from the internet (see the Caddyfile in
|
||||||
|
# ../Caddyfile and docs/Infrastructure/LAN-DNS.md).
|
||||||
|
#
|
||||||
|
# Build on dankclaw (podman rootless, same as every other container here):
|
||||||
|
# podman build -t franksdank/caddy-porkbun:latest -f dankclaw/caddy/Containerfile .
|
||||||
|
#
|
||||||
|
# The first build runs an xcaddy Go compilation and takes a few minutes.
|
||||||
|
# Rebuild only when Caddy or the porkbun module must be updated.
|
||||||
|
|
||||||
|
FROM docker.io/library/caddy:2-builder AS builder
|
||||||
|
|
||||||
|
RUN xcaddy build \
|
||||||
|
--with github.com/caddy-dns/porkbun \
|
||||||
|
--output /usr/bin/caddy-porkbun
|
||||||
|
|
||||||
|
FROM docker.io/library/caddy:2
|
||||||
|
COPY --from=builder /usr/bin/caddy-porkbun /usr/bin/caddy
|
||||||
@@ -199,12 +199,18 @@ services:
|
|||||||
# REPLACE_WITH_HASH placeholder. Certs/config live under
|
# REPLACE_WITH_HASH placeholder. Certs/config live under
|
||||||
# /home/dank/Containers/caddy (rootless-writable).
|
# /home/dank/Containers/caddy (rootless-writable).
|
||||||
caddy:
|
caddy:
|
||||||
image: docker.io/library/caddy:latest
|
# Locally built: stock Caddy + the Porkbun DNS provider
|
||||||
|
# (dankclaw/caddy/Containerfile). See that file for build instructions.
|
||||||
|
image: franksdank/caddy-porkbun:latest
|
||||||
container_name: caddy
|
container_name: caddy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
environment:
|
||||||
|
# Consumed by the Caddyfile's acme_dns block (DNS-01 wildcard certs).
|
||||||
|
PORKBUN_API_KEY: ${PORKBUN_API_KEY}
|
||||||
|
PORKBUN_API_SECRET_KEY: ${PORKBUN_API_SECRET_KEY}
|
||||||
volumes:
|
volumes:
|
||||||
- /home/dank/Containers/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
- /home/dank/Containers/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
- /home/dank/Containers/caddy/data:/data
|
- /home/dank/Containers/caddy/data:/data
|
||||||
|
|||||||
@@ -137,6 +137,10 @@ fi
|
|||||||
|
|
||||||
if [ '${COMPOSE_UP[$TARGET]}' = '1' ]; then
|
if [ '${COMPOSE_UP[$TARGET]}' = '1' ]; then
|
||||||
echo '[4/4] Restarting stack...'
|
echo '[4/4] Restarting stack...'
|
||||||
|
if ! podman image exists franksdank/caddy-porkbun:latest; then
|
||||||
|
echo ' franksdank/caddy-porkbun image missing; building it (a few minutes)...'
|
||||||
|
podman build -t franksdank/caddy-porkbun:latest -f dankclaw/caddy/Containerfile .
|
||||||
|
fi
|
||||||
podman-compose -f '${COMPOSE[$TARGET]}' up -d --pull=missing
|
podman-compose -f '${COMPOSE[$TARGET]}' up -d --pull=missing
|
||||||
else
|
else
|
||||||
echo '[4/4] Skipping compose restart (stack runs as bare containers on this host)'
|
echo '[4/4] Skipping compose restart (stack runs as bare containers on this host)'
|
||||||
|
|||||||
@@ -58,3 +58,40 @@ CADDYFILE="$REPO_ROOT/dankclaw/Caddyfile"
|
|||||||
@test "the caddy service healthcheck probes the bare-IP catch-all" {
|
@test "the caddy service healthcheck probes the bare-IP catch-all" {
|
||||||
grep -q 'curl.*-sf.*http://localhost:80/' "$REPO_ROOT/dankclaw/compose.yaml"
|
grep -q 'curl.*-sf.*http://localhost:80/' "$REPO_ROOT/dankclaw/compose.yaml"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Phase 2: real TLS via DNS-01 (Porkbun wildcard cert) ---
|
||||||
|
|
||||||
|
@test "all vhosts use TLS (no http:// site prefix remains)" {
|
||||||
|
! grep -qE '^http://' "$CADDYFILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "the Caddyfile configures the Porkbun DNS-01 provider globally" {
|
||||||
|
grep -q 'acme_dns porkbun' "$CADDYFILE"
|
||||||
|
grep -q '{env.PORKBUN_API_KEY}' "$CADDYFILE"
|
||||||
|
grep -q '{env.PORKBUN_API_SECRET_KEY}' "$CADDYFILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "the bare-IP catch-all redirects to the HTTPS homepage" {
|
||||||
|
grep -q 'redir https://dankclaw.franksdank.cloud/' "$CADDYFILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "the caddy service uses the locally built porkbun image" {
|
||||||
|
grep -q 'franksdank/caddy-porkbun' "$REPO_ROOT/dankclaw/compose.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "the caddy service passes the Porkbun credentials from .env" {
|
||||||
|
grep -q 'PORKBUN_API_KEY: \${PORKBUN_API_KEY}' "$REPO_ROOT/dankclaw/compose.yaml"
|
||||||
|
grep -q 'PORKBUN_API_SECRET_KEY: \${PORKBUN_API_SECRET_KEY}' "$REPO_ROOT/dankclaw/compose.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test ".env.example carries the Porkbun secret under the module's name" {
|
||||||
|
grep -q 'PORKBUN_API_SECRET_KEY=' "$REPO_ROOT/dankclaw/.env.example"
|
||||||
|
! grep -q '^PORKBUN_API_SECRET=' "$REPO_ROOT/dankclaw/.env.example"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "the porkbun caddy image build lives in the repo" {
|
||||||
|
local cf="$REPO_ROOT/dankclaw/caddy/Containerfile"
|
||||||
|
[ -f "$cf" ] || { echo "missing $cf"; return 1; }
|
||||||
|
grep -q 'xcaddy build' "$cf"
|
||||||
|
grep -q 'github.com/caddy-dns/porkbun' "$cf"
|
||||||
|
}
|
||||||
Reference in new issue
Block a user