Agent Handoff: Caddy, Technitium, and HTTPS
Updated: 2026-06-22
Use this file as the short operational context for future agents. It is intentionally small and does not contain secrets.
Live Topology
| Role | Target | Address | Notes |
|---|---|---|---|
| Firewall/router | OPNsense VM 100 |
192.168.2.1 on DMZ |
DHCP, routing, firewall policy |
| Apps | CT 101 podman-lxc |
192.168.2.20 |
Rootless Podman app host |
| DNS | CT 102 technitium-dns |
192.168.2.2 |
Internal DNS resolver |
| Ingress | CT 103 caddy-ingress |
192.168.2.3 |
Caddy HTTP/HTTPS ingress |
Known app upstreams on CT 101:
| App | Upstream |
|---|---|
| Forgejo | http://192.168.2.20:30080 |
| Vaultwarden | http://192.168.2.20:30081 |
| Adminer/dbgui | http://192.168.2.20:30082 |
| Dozzle/monitor | http://192.168.2.20:30083 |
| Technitium console | http://192.168.2.2:5380 |
| Synology office route | https://192.168.0.50:5001 |
Current internal DNS records in Technitium:
| Hostname | Address |
|---|---|
git.kh3group.com |
192.168.2.3 |
pass.kh3group.com |
192.168.2.3 |
dbgui.kh3group.com |
192.168.2.3 |
Current expected HTTPS behavior:
| Hostname | Expected result |
|---|---|
https://git.kh3group.com |
200, proxies to Forgejo |
https://pass.kh3group.com |
200, proxies to Vaultwarden |
https://dbgui.kh3group.com |
403, intentionally blocked until OIDC/admin policy exists |
https://office.kh3group.com |
200, proxies to Synology HTTPS with an upstream self-signed-certificate exception |
https://monitor.kh3group.com |
200, proxies to Dozzle; must be restricted before broad use |
https://dns.kh3group.com |
200, proxies to Technitium web console; must be restricted before broad use |
Important Paths
On CT 103 caddy-ingress:
| Purpose | Path |
|---|---|
| Caddy binary | /usr/local/bin/caddy |
| Caddyfile | /opt/caddy/config/Caddyfile |
| Caddy env file | /opt/caddy/env/caddy.env |
| Caddy data and certificates | /opt/caddy/data |
| systemd unit | /etc/systemd/system/caddy.service |
On CT 102 technitium-dns:
| Purpose | Path |
|---|---|
| Technitium backup directory | /root/technitium-backups |
| Web console listener | 127.0.0.1:5380 and 192.168.2.2:5380 |
Do not print or commit raw env files, Caddy data, Technitium config/backups, OPNsense exports, tokens, private keys, or ACME account material.
Current Caddy Design
Caddy was custom-built as v2.11.4 with these required modules:
dns.providers.cloudflaresecurity
Caddy admin is disabled. Because of that, caddy reload and
systemctl reload caddy are not usable. Validate the Caddyfile, then restart:
ssh pve 'pct exec 103 -- sh -c "set -a; . /opt/caddy/env/caddy.env; set +a; /usr/local/bin/caddy validate --config /opt/caddy/config/Caddyfile"'
ssh pve 'pct exec 103 -- systemctl restart caddy'
The systemd service must not use --environ, because that prints environment
variables, including tokens, into the journal.
Good service start shape:
ExecStart=/usr/local/bin/caddy run --config /opt/caddy/config/Caddyfile
Remove any ExecReload= line unless Caddy admin is intentionally re-enabled.
Caddyfile Pattern
The working pattern uses explicit public ACME DNS resolvers. This matters because
CT 103 uses internal DNS, and the Cloudflare provider can fail zone discovery
with expected 1 zone, got 0 if it follows internal DNS for kh3group.com.
{
admin off
email [email protected]
storage file_system /opt/caddy/data
acme_ca https://acme-v02.api.letsencrypt.org/directory
}
(tls_cloudflare) {
tls {
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
resolvers 1.1.1.1 1.0.0.1
}
}
git.kh3group.com {
import tls_cloudflare
encode zstd gzip
reverse_proxy 192.168.2.20:30080
}
pass.kh3group.com {
import tls_cloudflare
encode zstd gzip
reverse_proxy 192.168.2.20:30081
}
dbgui.kh3group.com {
import tls_cloudflare
respond "dbgui disabled until OIDC/admin policy is configured" 403
}
/opt/caddy/env/caddy.env should include:
CADDY_HOME=/opt/caddy
CLOUDFLARE_API_TOKEN=REDACTED
The Cloudflare token must be a scoped API token, not the old global API key. Required permissions:
Zone / Zone / Read
Zone / DNS / Edit
Zone Resources: Include / Specific zone / kh3group.com
Validation Commands
Do not print secrets while validating.
Check modules:
ssh pve 'pct exec 103 -- /usr/local/bin/caddy list-modules --versions | grep -E "dns.providers.cloudflare|security"'
Validate Caddyfile with env loaded:
ssh pve 'pct exec 103 -- sh -c "set -a; . /opt/caddy/env/caddy.env; set +a; /usr/local/bin/caddy validate --config /opt/caddy/config/Caddyfile"'
Restart and check listeners:
ssh pve 'pct exec 103 -- systemctl restart caddy; pct exec 103 -- ss -ltnp | grep -E ":80|:443"'
Validate current routes from CT 103:
ssh pve 'pct exec 103 -- sh -c '"'"'for h in git.kh3group.com pass.kh3group.com dbgui.kh3group.com office.kh3group.com monitor.kh3group.com dns.kh3group.com; do printf "%s " "$h"; curl -sS -o /dev/null -w "%{http_code} verify=%{ssl_verify_result}\n" --resolve "$h:443:127.0.0.1" "https://$h/"; done'"'"''
Expected:
git.kh3group.com 200 verify=0
pass.kh3group.com 200 verify=0
dbgui.kh3group.com 403 verify=0
office.kh3group.com 200 verify=0
monitor.kh3group.com 200 verify=0
dns.kh3group.com 200 verify=0
Validate internal DNS:
ssh pve 'pct exec 102 -- dig +short @192.168.2.2 git.kh3group.com A'
Validate from a Mac client:
dig git.kh3group.com +short
curl -Iv https://git.kh3group.com
curl -Iv https://pass.kh3group.com
curl -Iv https://dbgui.kh3group.com
Troubleshooting Lessons
- If manual
caddy validatesaysmissing API token, load the env file in the shell before validating. - If systemd starts but manual validation fails, remember systemd reads
/opt/caddy/env/caddy.env; your shell does not. - If logs say
contact email has forbidden domain "example.com", replace the placeholder email in the Caddyfile with a real address. - If logs say
Invalid access token, the Cloudflare token is wrong, revoked, or lacks required permissions. - If direct Cloudflare token verification succeeds but Caddy says
expected 1 zone, got 0, add public resolvers in thetlsblock. - If certificates are issued but a route returns
502, check the upstream port in the Caddyfile and validate direct connectivity from CT103. - Caddy admin is disabled by design. Do not add public admin exposure.
- Adminer/dbgui must stay blocked until OIDC/admin policy is configured and validated.
- Dozzle and Technitium console routes also require OIDC, VPN, or source-IP restrictions before they are treated as safe for normal operations.