Skip to content

Caddy and Technitium Migration Plan

Current Verified State

This page records the current migration target for replacing the historical Traefik and Pi-hole/Cloudflared design with Caddy and Technitium DNS.

Verified on June 22, 2026:

  • The active firewall is OPNsense. It was reachable with ssh router during the June 22 validation, but that alias is not available in every workspace.
  • ssh router reported hostname OPNsense.internal on FreeBSD/OPNsense during the June 22 validation.
  • Proxmox is reachable with ssh pvessh in the current workspace.
  • VM 100 router is the running firewall VM in Proxmox.
  • CT 101 podman-lxc is running at 192.168.2.20/24 on VLAN 2 and remains unprivileged.
  • CT 101 uses rootless Podman as user podsvc.
  • CT 101 currently publishes restored services on high ports:
  • Forgejo web: 192.168.2.20:30080
  • Forgejo SSH: 192.168.2.20:2222
  • Vaultwarden: 192.168.2.20:30081
  • Adminer: 192.168.2.20:30082
  • Dozzle: 192.168.2.20:30083
  • CT 101 used temporary resolver 1.1.1.1 during bootstrap and was moved back to Technitium at 192.168.2.2 after DNS validation.

The older documentation still contains pfSense, Pi-hole, Cloudflared, Docker, aproxy, and Traefik references. Treat those references as historical unless a page explicitly says it has been migrated to OPNsense, Technitium, Caddy, and rootless Podman.

Read-only validation on June 22, 2026:

  • ssh router 'hostname; uname -a' still reports OPNsense.internal on the FreeBSD 14.3 OPNsense line.
  • ssh pve 'qm list' shows VM 100 router running.
  • ssh pve 'pct list' shows CT 101 podman-lxc running.
  • ssh pve 'pct config 101' showed unprivileged CT 101, 192.168.2.100/24, gateway 192.168.2.1, VLAN tag 2, and temporary nameserver 1.1.1.1 during bootstrap. A later check showed nameserver: 192.168.2.2.
  • From inside CT 101, Forgejo, Vaultwarden, and Adminer answered 200 on 127.0.0.1:30080, 127.0.0.1:30081, and 127.0.0.1:30082.
  • From the administration workstation used for this edit, direct connections to 192.168.2.100:30080, :30081, and :30082 failed immediately. Treat that as a workstation-to-DMZ routing or firewall path issue until tested from an approved network path.

Additional read-only validation on June 26, 2026:

  • ssh pve 'qm list; pct list' showed VM 100 router running and CTs 101, 102, 103, and 104 running.
  • CT 101 Proxmox config showed nameserver: 192.168.2.2.
  • CT 101 rootless Podman showed postgres, forgejo, vaultwarden, adminer, and dozzle running. PostgreSQL was healthy.
  • CT 101 user systemd showed runner.service failed.
  • CT 102 listened on 192.168.2.2:53 and 127.0.0.1:53.
  • CT 102 web console listened on both 127.0.0.1:5380 and 192.168.2.2:5380; restrict it with OPNsense, Caddy OIDC, VPN, or source-IP policy.
  • CT 103 ran Caddy v2.11.4 and listened on 80 and 443.
  • CT 103 Caddy modules included dns.providers.cloudflare v0.2.4 and security v1.1.62.
  • Local Caddy route checks from CT 103 returned 200 for git.kh3group.com, pass.kh3group.com, office.kh3group.com, monitor.kh3group.com, and dns.kh3group.com; dbgui.kh3group.com returned 403.

Connection-path update on July 1, 2026:

  • Use ssh pvessh for Proxmox from this workspace.
  • Do not assume ssh router exists locally. The OPNsense UI or a verified SSH path is required for current firewall interface/firewall inspection.
  • VM 100 router has no QEMU guest agent configured, so qm guest exec 100 cannot be used for live OPNsense commands.
  • 192.168.100.1 identified as Starlink, not OPNsense LAN; old 192.168.100.0/24 references are historical until recaptured.

Implemented State on June 22, 2026

The first implementation pass created the replacement DNS and HTTP ingress LXCs. It did not yet change OPNsense DHCP or firewall enforcement.

Role Live target Address Status
DNS CT 102 technitium-dns 192.168.2.2 Running, unprivileged Debian LXC
Ingress CT 103 caddy-ingress 192.168.2.3 Running, unprivileged Debian LXC
Apps CT 101 podman-lxc 192.168.2.20 Running rootless Podman app host

Technitium:

  • Installed with the official Linux installer. The installer installs the ASP.NET Core runtime because Technitium DNS Server is a .NET application.
  • DNS listens on 192.168.2.2:53 and 127.0.0.1:53.
  • The web console currently listens on 127.0.0.1:5380 and 192.168.2.2:5380. It must be restricted by firewall, VPN, Caddy OIDC, or source-IP policy and must not be exposed to the public internet.
  • Recursion is restricted to loopback and private client ranges.
  • Exact internal authoritative records were created:
  • git.kh3group.com -> 192.168.2.3
  • pass.kh3group.com -> 192.168.2.3
  • dbgui.kh3group.com -> 192.168.2.3
  • Pre-change and post-change backups were stored inside CT 102 under /root/technitium-backups.

Caddy:

  • Built with xcaddy on CT 103.
  • Required Caddy modules validated with caddy list-modules:
  • dns.providers.cloudflare
  • security
  • Installed binary: /usr/local/bin/caddy.
  • Low-port binding is handled with cap_net_bind_service on the Caddy binary; do not lower rootless privileged-port limits for this.
  • Caddy admin is disabled.
  • Persistence layout:
  • /opt/caddy/config
  • /opt/caddy/data
  • /opt/caddy/runtime
  • /opt/caddy/env/caddy.env
  • /opt/caddy/build
  • Current Caddy config uses HTTPS with Cloudflare DNS-01. OIDC/admin policy is still pending for admin surfaces.
  • Current routes:
  • git.kh3group.com proxies to 192.168.2.20:30080 and validated 200.
  • pass.kh3group.com proxies to 192.168.2.20:30081 and validated 200.
  • dbgui.kh3group.com returns 403 until OIDC/admin policy is configured.
  • office.kh3group.com proxies to https://192.168.0.50:5001 and validated 200; it currently uses an upstream self-signed-certificate exception.
  • monitor.kh3group.com proxies to Dozzle at 192.168.2.20:30083 and validated 200; restrict this before normal use.
  • dns.kh3group.com proxies to Technitium at 192.168.2.2:5380 and validated 200; restrict this before normal use.

CT 101 resolver:

  • Proxmox CT config now has nameserver: 192.168.2.2.
  • The running /etc/resolv.conf was also updated because Proxmox did not rewrite the live file immediately.
  • A pre-change resolver backup was kept inside CT 101 as /root/resolv.conf.pre-technitium-*.

OPNsense:

  • A rollback export was saved outside the repository before firewall/DHCP work: /tmp/opnsense-config-before-dns-caddy-20260622T161716Z.xml.
  • The active DHCP service is Dnsmasq, not ISC DHCP or Kea.
  • LAN Dnsmasq DHCP option 6 was added for interface lan with value 192.168.2.2.
  • Dnsmasq generated config validated: dhcp-option=tag:vtnet0,6,192.168.2.2.
  • Router-to-Technitium validation returned git.kh3group.com -> 192.168.2.3 and public recursion for example.com.
  • Clients such as the admin iMac may continue to resolve public Cloudflare answers until they renew DHCP and flush local DNS cache.
  • DNS redirect/block enforcement for clients attempting alternate resolvers is still pending.

Lessons Learned

  • Technitium on Linux requires the ASP.NET Core runtime; seeing the .NET runtime during install is expected.
  • Bind admin web consoles to loopback during bootstrap. Expose them only through an explicit admin path, VPN, or OIDC policy.
  • Creating narrow authoritative zones for exact FQDNs avoids taking authority over all of kh3group.com before the full DNS design is ready.
  • Proxmox pct set --nameserver updates persisted CT config but may not update the live /etc/resolv.conf in a running CT. Validate and update the live file or restart the CT intentionally.
  • On this OPNsense install, active DHCP lives under the Dnsmasq plugin. Do not edit inactive dhcpd or Kea sections when changing client DNS.
  • Keep Caddy and Technitium in dedicated unprivileged LXCs. CT 101 remains a rootless high-port app/data host.
  • Use file capability on the Caddy binary for ports 80/443; do not casually lower privileged-port limits.
  • Do not expose Adminer/dbgui until OIDC/admin authorization is implemented and tested.

Target Architecture

Role Target Address Notes
Firewall OPNsense VM 100 router 192.168.2.1 on DMZ Routing, NAT, DHCP, DNS enforcement, WAN failover
DNS Technitium DNS 192.168.2.2 Replaces Pi-hole/Cloudflared at the existing resolver address
Ingress Caddy CT 103 192.168.2.3 Replaces Traefik for HTTP/HTTPS ingress and certificate automation
Apps CT 101 podman-lxc 192.168.2.20 Rootless Podman application and data services
Proxmox pve 192.168.2.10 if unchanged Virtualization host

Recommended placement:

  • Keep CT 101 focused on restored application services and data.
  • Run Technitium separately from CT 101 because DNS is recovery-critical and needs stable port 53.
  • Run Caddy separately from CT 101 because it is public ingress, needs 80/443, and will hold certificate/OIDC secrets.
  • Keep all LXCs unprivileged unless a specific blocker is documented and approved.

Why Not Put Caddy and Technitium in CT 101

CT 101 is intentionally rootless and high-port oriented. It is a good fit for PostgreSQL, Forgejo, Vaultwarden, and Adminer. It is a poor fit for DNS and public ingress because:

  • DNS needs 53/tcp and 53/udp.
  • HTTP/HTTPS ingress normally needs 80/tcp, 443/tcp, and optionally 443/udp.
  • Lowering net.ipv4.ip_unprivileged_port_start for rootless containers increases the impact of any process in the container namespace.
  • Combining DNS, ingress, databases, and applications increases blast radius.
  • DNS should come up early during recovery, before application routing depends on it.

Use OPNsense NAT or port forwarding only where it simplifies the boundary; do not depend on CT 101 rootless containers binding privileged ports directly.

Caddy Migration Plan

Caddy replaces Traefik as the HTTP/HTTPS ingress layer.

Build a custom Caddy binary or image with xcaddy and these modules:

xcaddy build \
  --with github.com/caddy-dns/cloudflare \
  --with github.com/greenpau/caddy-security

Required capabilities:

  • Cloudflare DNS-01 ACME automation using the Caddy Cloudflare DNS provider.
  • OIDC authentication for Google Workspace domain kh3group.com.
  • No public exposure of Caddy admin APIs.
  • Reverse proxy routes to the rootless Podman high ports on CT 101.

Initial route map:

Hostname Caddy upstream Notes
git.kh3group.com http://192.168.2.20:30080 Forgejo web
pass.kh3group.com http://192.168.2.20:30081 Vaultwarden
dbgui.kh3group.com http://192.168.2.20:30082 Adminer; require OIDC and admin/VPN policy
monitor.kh3group.com http://192.168.2.20:30083 Dozzle; require OIDC and admin/VPN policy
dns.kh3group.com http://192.168.2.2:5380 Technitium console; require OIDC and admin/VPN policy
office.kh3group.com https://192.168.0.50:5001 Synology HTTPS route; document upstream trust exception

Google OIDC behavior:

  • Configure the allowed hosted domain as kh3group.com where the plugin supports domain restrictions or claims checks.
  • Do not force Google's prompt=select_account if the goal is to avoid account selection when an active session exists.
  • Prefer normal OIDC flow with domain restriction; use login_hint only where the user identity is known.
  • Use prompt=none only after testing failure behavior, because Google returns an error when no active session or consent exists.
  • If greenpau/caddy-security cannot pass the needed Google authorization parameters or enforce the desired claim checks, use Caddy for TLS/reverse proxy and put OAuth2 Proxy or Authelia behind it for authentication.

Caddy persistence:

Purpose Suggested path
Caddyfile or JSON config /opt/caddy/config/
Caddy data and certificates /opt/caddy/data/
Runtime config /opt/caddy/runtime/
Secret env file /opt/caddy/env/caddy.env
Build metadata /opt/caddy/build/

Never commit Cloudflare API tokens, Google client secrets, Caddy storage keys, cookies, or exported runtime config containing secrets.

Starter Caddyfile shape:

{
    admin off
    email {$ACME_EMAIL}
    acme_dns cloudflare {$CLOUDFLARE_API_TOKEN}
}

git.kh3group.com {
    reverse_proxy 192.168.2.20:30080
}

pass.kh3group.com {
    reverse_proxy 192.168.2.20:30081
}

dbgui.kh3group.com {
    route {
        # Add caddy-security or fallback OAuth2 Proxy/Authelia policy here
        # before exposing this route beyond approved admin networks.
        reverse_proxy 192.168.2.20:30082
    }
}

Keep this as a shape, not a final secret-bearing config. The final auth block must be tested with Google Workspace kh3group.com claim enforcement before Adminer is made broadly reachable.

Traefik Removal Plan

Traefik labels and aproxy-only routing are historical Docker-era patterns. During migration:

  1. Inventory current compose files and Quadlets for labels beginning with traefik..
  2. Record each hostname and backend service before deleting labels.
  3. Move routing into Caddy configuration.
  4. Remove Traefik-specific labels from service definitions after the matching Caddy route validates.
  5. Remove aproxy membership only when the service no longer needs that Docker network for any non-Traefik purpose.
  6. Keep backend data networks such as kh3-backend or database-only networks intact.
  7. Stop Traefik only after all required hostnames validate through Caddy.
  8. Preserve the old Traefik config and certificate data in restricted backup storage until the Caddy migration has passed a full renewal cycle.

Inventory commands:

rg -n 'traefik\.' /root /opt 2>/dev/null
ssh pve 'pct exec 101 -- bash -lc "find /opt/podman /home/podsvc/.config/containers/systemd -maxdepth 6 -type f -print 2>/dev/null | sort"'

Do not paste raw env files, ACME stores, or application configs into this repository. Record only hostname, backend address, middleware/auth requirement, and replacement Caddy route.

Validation for each migrated route:

curl -I https://git.kh3group.com
curl -I https://pass.kh3group.com
curl -I https://dbgui.kh3group.com

For protected routes, also validate unauthorized access, authorized Google Workspace login, logout, and direct upstream access policy.

Technitium Migration Plan

Technitium replaces Pi-hole/Cloudflared at 192.168.2.2.

Placement:

  • Preferred: a dedicated unprivileged Debian LXC using address 192.168.2.2/24, gateway 192.168.2.1, VLAN 2.
  • Alternative: reuse the existing DNS LXC identity only after exporting anything needed from Pi-hole/Cloudflared and confirming rollback.
  • Avoid placing Technitium in CT 101 rootless Podman because DNS port 53 should be simple and reliable during recovery.

Technitium persistence:

Purpose Suggested path
DNS configuration and zones /opt/technitium/config or native /etc/dns
Logs /opt/technitium/logs or native /var/log/technitium/dns
Secret env file /opt/technitium/env/technitium.env
Backup exports /opt/technitium/backups

Initial DNS policy:

  • Listen on 53/tcp and 53/udp for LAN, DMZ infrastructure, and VPN clients.
  • Keep the web console on 5380/tcp or behind Caddy with OIDC, restricted to AdminPCs/VPN.
  • Configure recursion ACLs so Technitium is not an open resolver.
  • Import or recreate internal zones and static records that replaced Pi-hole local DNS records.
  • Decide whether upstream forwarding uses plain recursive resolution, DoT, DoH, or selected forwarders.

Backup and restore:

  • Back up Technitium config/zones before and after major DNS policy changes.
  • Store admin credentials and OIDC secrets outside the repository.
  • Include Technitium restore validation in the disaster recovery checklist.
  • After Technitium is live and validated, update CT 101 nameserver from temporary 1.1.1.1 to 192.168.2.2.

Suggested rollout sequence:

  1. Create or prepare the dedicated unprivileged Debian LXC at 192.168.2.2.
  2. Install Technitium and bind DNS only to intended interfaces.
  3. Restrict the web console on 5380/tcp to AdminPCs/VPN or route it through Caddy with OIDC.
  4. Recreate required local zones and records for git.kh3group.com, pass.kh3group.com, and dbgui.kh3group.com.
  5. Validate recursion from approved LAN/DMZ/VPN clients.
  6. Validate recursion fails or is blocked from untrusted networks.
  7. Move DHCP DNS options in OPNsense to 192.168.2.2. On the current install this is Dnsmasq DHCP option 6 for interface lan.
  8. Update CT 101 resolver after Technitium is stable.

Validation:

dig @192.168.2.2 example.com
dig @192.168.2.2 git.kh3group.com
dig @192.168.2.2 pass.kh3group.com
dig @192.168.2.2 dbgui.kh3group.com

From an untrusted network, recursion should fail or be blocked.

OPNsense DNS Enforcement Plan

The user requirement is that clients use Technitium and cannot easily bypass it.

OPNsense policy goals:

  • DHCP hands out only 192.168.2.2 as DNS for client VLANs.
  • Allow client VLANs to 192.168.2.2 on 53/tcp and 53/udp.
  • Redirect client DNS attempts to any other resolver back to 192.168.2.2 where appropriate.
  • Block direct outbound 53/tcp and 53/udp to non-Technitium destinations.
  • Block or tightly control 853/tcp for DNS over TLS.
  • Maintain aliases for known DoH resolvers and block them where operationally safe.
  • Use browser or device management for DoH controls where firewall-only enforcement is not reliable.
  • Prevent clients from using alternate internal DNS servers unless explicitly approved.
  • Preserve exceptions for OPNsense, Proxmox, NAS, Technitium, Caddy, CT 101, recovery laptop, and break-glass admin hosts.

Safe rule-order shape:

  1. Create aliases first: InternalDNS, DNSPorts, AdminHosts, InfrastructureHosts, and optional KnownDoHResolvers.
  2. Add explicit allow rules from client VLANs to InternalDNS on 53/tcp and 53/udp.
  3. Add NAT redirect rules for client DNS to non-Technitium destinations only where interception is desired.
  4. Add block rules for direct outbound 53/tcp, 53/udp, and unapproved 853/tcp.
  5. Add or update DHCP server DNS values to hand out only 192.168.2.2.
  6. Validate after every change before moving to the next interface/VLAN.

Export OPNsense configuration before these changes and keep the raw export out of the repository.

Suggested aliases:

Alias Type Value
InternalDNS host 192.168.2.2
DNSPorts port 53, 853
AdminHosts host/network approved admin workstations and recovery laptop
InfrastructureHosts host/network OPNsense, Proxmox, NAS, Caddy, Technitium, CT 101
KnownDoHResolvers host/network maintained resolver IP list

Validation from a client VLAN:

dig @192.168.2.2 example.com
dig @1.1.1.1 example.com
nc -vz 1.1.1.1 853
curl -I https://cloudflare-dns.com/dns-query

Expected result:

  • Direct Technitium query succeeds.
  • Direct external DNS is redirected or blocked according to rule design.
  • DoT to unapproved resolvers fails.
  • Known DoH endpoints are blocked where the alias/list policy covers them.

Documentation Tasks During Migration

Update documentation in small commits as each action is completed:

  1. Mark VM 100 router as OPNsense in current inventory pages.
  2. Add or update an OPNsense page and leave the old pfSense page as historical if needed.
  3. Replace Pi-hole/Cloudflared operational DNS references with Technitium once 192.168.2.2 is migrated.
  4. Replace Traefik operational ingress references with Caddy once routes are live.
  5. Record every removed Traefik label and its replacement Caddy route.
  6. Record every OPNsense NAT, alias, DHCP, and firewall rule changed for DNS enforcement.
  7. Record rollback steps for each stage.
  8. Keep raw firewall exports, env files, tokens, keys, Caddy data, and Technitium config backups out of Git unless redacted.

Rollback Strategy

  • Before OPNsense changes, export the firewall configuration through the OPNsense UI and store it outside the repository.
  • Before replacing DNS, preserve Pi-hole Teleporter export and Cloudflared configuration/token references in restricted storage.
  • Before replacing Traefik, preserve Traefik config and certificate state in restricted storage.
  • Migrate one hostname at a time to Caddy.
  • Keep CT 101 direct high-port validation working during the ingress migration.
  • Keep one admin or recovery client exempt from DNS interception until final validation passes.