DNS Architecture¶
Last updated: 2026-06-23
Overview¶
heezy uses split DNS with two internal DNS servers serving different clients:
| Server | IP | Role |
|---|---|---|
| Pi-hole | 192.168.1.27 (MetalLB VIP) | Primary DNS for all LAN clients. Ad-blocking. Wildcard zone resolution → SWAG. |
| dnsmasq | 192.168.1.29 (VM: shared-dnsmasq) | Per-host authoritative DNS for k8s pods via CoreDNS. Full zone records. |
Resolution path depends on the client:
- LAN hosts → Pi-hole (192.168.1.27) → wildcard address rules → SWAG VIP (192.168.1.25)
- k8s pods → CoreDNS (cluster) → forwards zones to dnsmasq (192.168.1.29)
DNS Resolution Flow¶
LAN Client (heezy.local or heezy.info)¶
LAN host
→ systemd-resolved
→ Pi-hole 192.168.1.27 (DHCP-assigned by FortiGate)
→ Pi-hole ConfigMap: address=/heezy.local/192.168.1.25
address=/heezy.info/192.168.1.25
address=/heezy.blog/192.168.1.25
→ Returns 192.168.1.25 (SWAG MetalLB VIP)
→ SWAG reverse proxy → target service
Note: All LAN queries for *.heezy.local, *.heezy.info, and *.heezy.blog resolve to the SWAG VIP (192.168.1.25). Individual host IPs (e.g. nfs.heezy.local) are not directly resolvable from LAN — all traffic routes through SWAG. For direct host access from LAN, use the IP address.
k8s Pod (heezy.local)¶
k8s pod
→ CoreDNS (kube-system)
→ heezy.local zone: forward . 192.168.1.29 (dnsmasq)
→ dnsmasq returns per-host IP from /opt/dnsmasq/hosts/
→ Direct IP returned (no SWAG hop)
k8s Pod (heezy.info / trentnielsen.me / heezy.blog)¶
k8s pod
→ CoreDNS
→ heezy.info zone: forward . 192.168.1.27 (Pi-hole)
→ trentnielsen.me zone: forward . 192.168.1.27 (Pi-hole)
→ heezy.blog zone: forward . 192.168.1.27 (Pi-hole)
→ Pi-hole returns 192.168.1.25 (SWAG)
→ SWAG → target service
External query (from LAN, public domain)¶
LAN host
→ Pi-hole 192.168.1.27
→ No local match → upstream: 8.8.8.8 (fallback, FortiGate DHCP dns_server2)
→ Or: Pi-hole configured upstreams (1.1.1.1 / 8.8.8.8)
→ Public IP returned
Pi-hole (192.168.1.27)¶
- Deployment: k8s
heezynamespace,deployment/pihole - VIP: 192.168.1.27 (MetalLB LoadBalancer)
- Config source:
heezy-k8s/apps/pihole/configmap-dns.yaml - Zone rules (mounted as
/etc/pihole/dnsmasq.d/99-heezy-forwarding.conf):
address= in dnsmasq is a wildcard — returns the specified IP for all queries under that zone. All LAN service access routes through SWAG.
FortiGate DHCP (all LAN VLANs) hands out 192.168.1.27 as dns_server1.
dnsmasq (192.168.1.29)¶
- Deployment: Docker on
shared-dnsmasqVM (192.168.1.29) - Managed by: Ansible role
dnsmasq→ playbookplaybook-dnsmasq-execution.yml - Config dir:
/opt/dnsmasq/ - Zones (authoritative, never forwarded upstream):
heezy.localtrentnielsen.meheezy.infoheezy.blog1.168.192.in-addr.arpa(reverse DNS)
Generated hosts files¶
| File | Purpose |
|---|---|
/opt/dnsmasq/hosts/hosts |
Per-host entries from Ansible inventory |
/opt/dnsmasq/hosts/infra-services |
Infrastructure: grafana, prometheus, proxmox, nfs, etc. |
/opt/dnsmasq/hosts/k8s-services |
k8s services: all SWAG-proxied apps + dedicated VIPs |
/opt/dnsmasq/hosts/heezy-info-overrides |
All *.heezy.info → SWAG or direct VIP |
/opt/dnsmasq/hosts/trentnielsen-overrides |
All *.trentnielsen.me → SWAG or direct |
/opt/dnsmasq/hosts/reverse-dns |
PTR records for 192.168.1.x |
To update DNS records: edit Ansible templates and run playbook-dnsmasq-execution.yml via Gitea.
CoreDNS (k8s internal)¶
- Config:
heezy-k8s/base/coredns-configmap.yaml - Zone forwarding:
| Zone | Forward to | Reason |
|---|---|---|
heezy.local |
192.168.1.29 (dnsmasq) | Per-host records, direct IPs |
heezy.info |
192.168.1.27 (Pi-hole) | → SWAG via Pi-hole wildcard |
trentnielsen.me |
192.168.1.27 (Pi-hole) | → SWAG via Pi-hole wildcard |
heezy.blog |
192.168.1.27 (Pi-hole) | → SWAG via Pi-hole wildcard |
cluster.local |
k8s internal | Service DNS |
* (default) |
/etc/resolv.conf |
Node's upstream |
MetalLB VIP Allocations¶
Pool: 192.168.1.25–192.168.1.32
| VIP | Assignment | DNS Name (dnsmasq) |
|---|---|---|
| 192.168.1.25 | SWAG (main ingress) | swag.heezy.local |
| 192.168.1.26 | (unassigned) | — |
| 192.168.1.27 | Pi-hole | pihole.heezy.local |
| 192.168.1.28 | Plex direct | plex.heezy.local |
| 192.168.1.29 | (used by dnsmasq VM, not MetalLB) | dnsmasq.heezy.local |
| 192.168.1.30 | SWAG-Plex | swag-plex.heezy.local |
| 192.168.1.31 | Gitea | gitea.heezy.local |
| 192.168.1.32 | OpenBao | openbao.heezy.local |
Hardcoded IP Audit (heezy-k8s)¶
| File | IP | Type | Status |
|---|---|---|---|
base/nfs-media-static-pvs.yaml (×6) |
192.168.1.200 | NFS PV server field | Known exception — k8s NFS PV spec.nfs.server requires IP; cannot use hostname. Immutable after creation. |
base/coredns-configmap.yaml |
192.168.1.29, 192.168.1.27 | CoreDNS forwarders | Intentional — DNS bootstrap cannot use DNS to find DNS. |
base/metallb/metallb-config.yaml |
192.168.1.25–192.168.1.32 | VIP pool range | Intentional — MetalLB pool definition. |
apps/gitea/deployment.yaml |
192.168.1.31 | loadBalancerIPs annotation |
Intentional — VIP assignment. |
apps/openbao/nodeport-svc.yaml |
192.168.1.32 | loadBalancerIPs annotation |
Intentional — VIP assignment. |
apps/pihole/deployment.yaml |
192.168.1.27 | loadBalancerIPs annotation |
Intentional — VIP assignment. |
apps/plex/deployment.yaml |
192.168.1.28 | loadBalancerIP |
Intentional — VIP assignment. |
apps/swag-plex/deployment.yaml |
192.168.1.30 | loadBalancerIP |
Intentional — VIP assignment. |
apps/gluetun/deployment.yaml |
192.168.0.0/16 | VPN bypass CIDR | Intentional — CIDR range, not a hostname. |
apps/qbittorrent/deployment.yaml |
10.0.0.0/8, 192.168.0.0/16 | VPN bypass CIDRs | Intentional — CIDR ranges. |
apps/tailscale/deployment.yaml |
192.168.0.0/20 | Tailscale routes | Intentional — CIDR range. |
apps/pihole/configmap-dns.yaml |
192.168.1.25 | Zone wildcard target (SWAG) | Intentional — wildcard address= must be IP. |
Conclusion: No actionable hardcoded IPs in heezy-k8s. All occurrences are VIP assignments, NFS PV server fields (immutable), CoreDNS bootstrap forwarders, or CIDR ranges — none are candidates for DNS name substitution.
Known Exceptions¶
Gitea act-runner (DinD)¶
act-runner jobs run in Docker-in-Docker containers. From inside DinD, k8s CoreDNS is not reachable, so Gitea must be addressed by IP + NodePort:
GITEA_INSTANCE_URL=http://192.168.1.15:30360(nebula-1 NodePort)GITEA_INSTANCE_URL=http://192.168.1.31:3000(MetalLB VIP) — preferred; reachable from DinD
This is a known limitation — document it, don't try to DNS-ify it.
NFS PersistentVolumes¶
k8s NFS PV manifests require spec.nfs.server to be an IP address. DNS names are not supported by the NFS PV driver. The NFS server (192.168.1.200 / TrueNAS) has a dnsmasq entry nfs.heezy.local for human reference, but PV manifests will always contain the raw IP.
Minecraft/UT2K4 (DMZ)¶
mc.trentnielsen.me and minecraft.trentnielsen.me are intentionally set to 76.112.11.44 (WAN IP) in trentnielsen-overrides.j2 with a TODO comment. Pending USERS→DMZ FortiGate policy addition in terraform-heezy/environments/dmz/.
Maintenance¶
Add a new heezy.local record¶
- Edit the appropriate template in
ansible-heezy/roles/dnsmasq/templates/ - Infra host →
infra-services.j2 - k8s service →
k8s-services.j2 - Reverse DNS →
reverse-dns.j2 - Push to Gitea →
playbook-dnsmasq-execution.ymlauto-runs - Verify:
dig @192.168.1.29 <name>.heezy.local
Add a new heezy.info subdomain (split DNS)¶
- Add to
ansible-heezy/roles/dnsmasq/templates/heezy-info-overrides.j2 - Add SWAG proxy config in
heezy-containers/dockerfiles/swag/ - Push both repos → pipelines auto-deploy
- Verify from LAN:
dig @192.168.1.27 <new>.heezy.info - Verify from k8s:
kubectl -n heezy run -it --rm debug --image=alpine --restart=Never -- nslookup <new>.heezy.info
Update Pi-hole zone config¶
Pi-hole zone config lives in heezy-k8s/apps/pihole/configmap-dns.yaml. Apply via:
Debug DNS resolution¶
# LAN resolution (Pi-hole path)
dig @192.168.1.27 sonarr.heezy.local
dig @192.168.1.27 receipts.heezy.info
# k8s authoritative path (dnsmasq)
dig @192.168.1.29 nfs.heezy.local
dig @192.168.1.29 grafana.heezy.local
# From inside k8s pod
kubectl -n heezy run -it --rm dns-debug --image=alpine --restart=Never -- \
nslookup sonarr.heezy.local
# Check CoreDNS
kubectl -n kube-system get configmap coredns -o yaml
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=50
Architecture Diagram¶
FortiGate DHCP
dns_server1: 192.168.1.27 (Pi-hole)
dns_server2: 8.8.8.8
┌─────────────────────────────────────────────────────────────────┐
│ LAN CLIENTS │
│ *.heezy.local ──┐ │
│ *.heezy.info ──┤──▶ Pi-hole (192.168.1.27) ──▶ 192.168.1.25 (SWAG)
│ *.heezy.blog ──┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ k8s PODS │
│ *.heezy.local ──▶ CoreDNS ──▶ dnsmasq (192.168.1.29) ──▶ direct IP
│ *.heezy.info ──▶ CoreDNS ──▶ Pi-hole (192.168.1.27) ──▶ 192.168.1.25 (SWAG)
│ *.trentnielsen.me ▶ CoreDNS ──▶ Pi-hole (192.168.1.27) ──▶ 192.168.1.25 (SWAG)
│ cluster.local ──▶ CoreDNS (internal k8s SVC DNS) │
└─────────────────────────────────────────────────────────────────┘
MetalLB VIP Pool: 192.168.1.25–192.168.1.32
.25 = SWAG .27 = Pi-hole .28 = Plex
.30 = SWAG-Plex .31 = Gitea .32 = OpenBao