Skip to content

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 heezy namespace, 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=/heezy.info/192.168.1.25
address=/heezy.local/192.168.1.25
address=/heezy.blog/192.168.1.25

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-dnsmasq VM (192.168.1.29)
  • Managed by: Ansible role dnsmasq → playbook playbook-dnsmasq-execution.yml
  • Config dir: /opt/dnsmasq/
  • Zones (authoritative, never forwarded upstream):
  • heezy.local
  • trentnielsen.me
  • heezy.info
  • heezy.blog
  • 1.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

  1. Edit the appropriate template in ansible-heezy/roles/dnsmasq/templates/
  2. Infra host → infra-services.j2
  3. k8s service → k8s-services.j2
  4. Reverse DNS → reverse-dns.j2
  5. Push to Gitea → playbook-dnsmasq-execution.yml auto-runs
  6. Verify: dig @192.168.1.29 <name>.heezy.local

Add a new heezy.info subdomain (split DNS)

  1. Add to ansible-heezy/roles/dnsmasq/templates/heezy-info-overrides.j2
  2. Add SWAG proxy config in heezy-containers/dockerfiles/swag/
  3. Push both repos → pipelines auto-deploy
  4. Verify from LAN: dig @192.168.1.27 <new>.heezy.info
  5. 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:

kubectl apply -f apps/pihole/configmap-dns.yaml
kubectl -n heezy rollout restart deployment/pihole

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