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 routerduring the June 22 validation, but that alias is not available in every workspace. ssh routerreported hostnameOPNsense.internalon FreeBSD/OPNsense during the June 22 validation.- Proxmox is reachable with
ssh pvesshin the current workspace. - VM
100 routeris the running firewall VM in Proxmox. - CT
101 podman-lxcis running at192.168.2.20/24on VLAN2and remains unprivileged. - CT
101uses rootless Podman as userpodsvc. - CT
101currently 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
101used temporary resolver1.1.1.1during bootstrap and was moved back to Technitium at192.168.2.2after 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 reportsOPNsense.internalon the FreeBSD 14.3 OPNsense line.ssh pve 'qm list'shows VM100 routerrunning.ssh pve 'pct list'shows CT101 podman-lxcrunning.ssh pve 'pct config 101'showed unprivileged CT101,192.168.2.100/24, gateway192.168.2.1, VLAN tag2, and temporary nameserver1.1.1.1during bootstrap. A later check showednameserver: 192.168.2.2.- From inside CT
101, Forgejo, Vaultwarden, and Adminer answered200on127.0.0.1:30080,127.0.0.1:30081, and127.0.0.1:30082. - From the administration workstation used for this edit, direct connections to
192.168.2.100:30080,:30081, and:30082failed 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 VM100 routerrunning and CTs101,102,103, and104running.- CT
101Proxmox config showednameserver: 192.168.2.2. - CT
101rootless Podman showedpostgres,forgejo,vaultwarden,adminer, anddozzlerunning. PostgreSQL was healthy. - CT
101user systemd showedrunner.servicefailed. - CT
102listened on192.168.2.2:53and127.0.0.1:53. - CT
102web console listened on both127.0.0.1:5380and192.168.2.2:5380; restrict it with OPNsense, Caddy OIDC, VPN, or source-IP policy. - CT
103ran Caddyv2.11.4and listened on80and443. - CT
103Caddy modules includeddns.providers.cloudflare v0.2.4andsecurity v1.1.62. - Local Caddy route checks from CT
103returned200forgit.kh3group.com,pass.kh3group.com,office.kh3group.com,monitor.kh3group.com, anddns.kh3group.com;dbgui.kh3group.comreturned403.
Connection-path update on July 1, 2026:
- Use
ssh pvesshfor Proxmox from this workspace. - Do not assume
ssh routerexists locally. The OPNsense UI or a verified SSH path is required for current firewall interface/firewall inspection. - VM
100 routerhas no QEMU guest agent configured, soqm guest exec 100cannot be used for live OPNsense commands. 192.168.100.1identified as Starlink, not OPNsense LAN; old192.168.100.0/24references 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:53and127.0.0.1:53. - The web console currently listens on
127.0.0.1:5380and192.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.3pass.kh3group.com -> 192.168.2.3dbgui.kh3group.com -> 192.168.2.3- Pre-change and post-change backups were stored inside CT
102under/root/technitium-backups.
Caddy:
- Built with
xcaddyon CT103. - Required Caddy modules validated with
caddy list-modules: dns.providers.cloudflaresecurity- Installed binary:
/usr/local/bin/caddy. - Low-port binding is handled with
cap_net_bind_serviceon 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.comproxies to192.168.2.20:30080and validated200.pass.kh3group.comproxies to192.168.2.20:30081and validated200.dbgui.kh3group.comreturns403until OIDC/admin policy is configured.office.kh3group.comproxies tohttps://192.168.0.50:5001and validated200; it currently uses an upstream self-signed-certificate exception.monitor.kh3group.comproxies to Dozzle at192.168.2.20:30083and validated200; restrict this before normal use.dns.kh3group.comproxies to Technitium at192.168.2.2:5380and validated200; restrict this before normal use.
CT 101 resolver:
- Proxmox CT config now has
nameserver: 192.168.2.2. - The running
/etc/resolv.confwas also updated because Proxmox did not rewrite the live file immediately. - A pre-change resolver backup was kept inside CT
101as/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
6was added for interfacelanwith value192.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.3and public recursion forexample.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.combefore the full DNS design is ready. - Proxmox
pct set --nameserverupdates persisted CT config but may not update the live/etc/resolv.confin 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
dhcpdor Kea sections when changing client DNS. - Keep Caddy and Technitium in dedicated unprivileged LXCs. CT
101remains 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
101focused on restored application services and data. - Run Technitium separately from CT
101because DNS is recovery-critical and needs stable port53. - Run Caddy separately from CT
101because it is public ingress, needs80/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/tcpand53/udp. - HTTP/HTTPS ingress normally needs
80/tcp,443/tcp, and optionally443/udp. - Lowering
net.ipv4.ip_unprivileged_port_startfor 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.comwhere the plugin supports domain restrictions or claims checks. - Do not force Google's
prompt=select_accountif the goal is to avoid account selection when an active session exists. - Prefer normal OIDC flow with domain restriction; use
login_hintonly where the user identity is known. - Use
prompt=noneonly after testing failure behavior, because Google returns an error when no active session or consent exists. - If
greenpau/caddy-securitycannot 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:
- Inventory current compose files and Quadlets for labels beginning with
traefik.. - Record each hostname and backend service before deleting labels.
- Move routing into Caddy configuration.
- Remove Traefik-specific labels from service definitions after the matching Caddy route validates.
- Remove
aproxymembership only when the service no longer needs that Docker network for any non-Traefik purpose. - Keep backend data networks such as
kh3-backendor database-only networks intact. - Stop Traefik only after all required hostnames validate through Caddy.
- 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, gateway192.168.2.1, VLAN2. - Alternative: reuse the existing DNS LXC identity only after exporting anything needed from Pi-hole/Cloudflared and confirming rollback.
- Avoid placing Technitium in CT
101rootless Podman because DNS port53should 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/tcpand53/udpfor LAN, DMZ infrastructure, and VPN clients. - Keep the web console on
5380/tcpor 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
101nameserver from temporary1.1.1.1to192.168.2.2.
Suggested rollout sequence:
- Create or prepare the dedicated unprivileged Debian LXC at
192.168.2.2. - Install Technitium and bind DNS only to intended interfaces.
- Restrict the web console on
5380/tcpto AdminPCs/VPN or route it through Caddy with OIDC. - Recreate required local zones and records for
git.kh3group.com,pass.kh3group.com, anddbgui.kh3group.com. - Validate recursion from approved LAN/DMZ/VPN clients.
- Validate recursion fails or is blocked from untrusted networks.
- Move DHCP DNS options in OPNsense to
192.168.2.2. On the current install this is Dnsmasq DHCP option6for interfacelan. - Update CT
101resolver 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.2as DNS for client VLANs. - Allow client VLANs to
192.168.2.2on53/tcpand53/udp. - Redirect client DNS attempts to any other resolver back to
192.168.2.2where appropriate. - Block direct outbound
53/tcpand53/udpto non-Technitium destinations. - Block or tightly control
853/tcpfor 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:
- Create aliases first:
InternalDNS,DNSPorts,AdminHosts,InfrastructureHosts, and optionalKnownDoHResolvers. - Add explicit allow rules from client VLANs to
InternalDNSon53/tcpand53/udp. - Add NAT redirect rules for client DNS to non-Technitium destinations only where interception is desired.
- Add block rules for direct outbound
53/tcp,53/udp, and unapproved853/tcp. - Add or update DHCP server DNS values to hand out only
192.168.2.2. - 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:
- Mark VM
100 routeras OPNsense in current inventory pages. - Add or update an OPNsense page and leave the old pfSense page as historical if needed.
- Replace Pi-hole/Cloudflared operational DNS references with Technitium once
192.168.2.2is migrated. - Replace Traefik operational ingress references with Caddy once routes are live.
- Record every removed Traefik label and its replacement Caddy route.
- Record every OPNsense NAT, alias, DHCP, and firewall rule changed for DNS enforcement.
- Record rollback steps for each stage.
- 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
101direct high-port validation working during the ingress migration. - Keep one admin or recovery client exempt from DNS interception until final validation passes.