Letβs be very clear from the start π
If DNS is shaky, everything above it becomes unreliable, misleading, and borderline hostile.
You will blame Kubernetes.
You will blame cert-manager.
You will blame yourself.
And it will still be DNS π
This article builds a real DNS layer, outside the cluster, the way it is done in production environments.
Not fancy. Not clever. Just solid.
- DNS is not a feature, it is the foundation π§±
Most homelabs treat DNS like a checkbox:
- router default
- maybe 1.1.1.1
- maybe 8.8.8.8
- call it a day
This works until you introduce:
- multiple Proxmox nodes
- Kubernetes bootstrap
- internal TLS
- ExternalDNS
- service to service communication At that point, DNS stops being optional. It becomes infrastructure.
If this layer lies, everything else lies with it.
- what I assume before we start breaking things π§
This article assumes:
- you are comfortable with Linux and SSH
- you control your LAN
- you can assign static IPs
- you are fine rebuilding things if needed
Network baseline used in examples:
- LAN CIDR: 192.168.1.0/24
- router: 192.168.1.1
- DNS node: 192.168.1.169
- internal domain: home.arpa
Why home.arpa?
- defined by RFC 8375
- designed for private networks
- no collision with public DNS
- predictable behavior across operating systems
.local is convenient.
.local is also a trap β οΈ
- how DNS flows in this homelab, no magic involved π§
The DNS request path is intentionally boring:
client
β Pi-hole
β Unbound
β root servers
β TLD servers
β authoritative servers
Why this design?
Pi-hole sits in front:
- single DNS endpoint for the entire LAN
- full visibility into queries
- local DNS records for infrastructure
- API compatible with ExternalDNS later
Unbound sits behind:
- full recursive resolver
- DNSSEC validation
- zero dependency on Google or Cloudflare
- behaves like enterprise DNS stacks
One DNS server for the LAN:
- deterministic behavior
- easier troubleshooting
- required for Kubernetes stability
- building the DNS node, properly and calmly π§± A Raspberry Pi that does exactly one thing π₯§
Reserve a static IP from your router:
- hostname: dns-01
- ip: 192.168.1.169
Update the system:
sudo apt update
sudo apt upgrade -y
sudo apt install curl gnupg lsb-release -y
This node is sacred:
- DNS only
- no Docker
- no Kubernetes
- no experiments
If DNS breaks, you want zero doubt about the cause.
- installing Pi-hole, the DNS entry point π¦
Pi-hole will be the only DNS server exposed to the LAN.
Install it:
curl -sSL https://install.pi-hole.net | bash
During the installer:
- interface: eth0
- upstream DNS: custom
- do not select any public DNS provider
- enable the web admin interface
Once installed:
http://192.168.1.169/admin
Immediately change the admin password:
pihole -a -p
At this stage:
- Pi-hole is answering DNS queries
- but it should not resolve anything yet
- installing Unbound, the actual DNS resolver π
Unbound will do the real work: recursive resolution and DNSSEC validation.
Install Unbound:
sudo apt install unbound -y
Create a dedicated configuration for Pi-hole:
sudo tee /etc/unbound/unbound.conf.d/pi-hole.conf > /dev/null <<EOF
server:
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
root-hints: "/var/lib/unbound/root.hints"
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: yes
edns-buffer-size: 1232
prefetch: yes
verbosity: 1
forward-zone:
name: "."
forward-addr: 0.0.0.0
EOF
Fetch root hints:
sudo curl -o /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
Enable and start Unbound:
sudo systemctl restart unbound
sudo systemctl enable unbound
At this point:
- Unbound listens on 127.0.0.1:5335
- performs full recursive resolution
- validates DNSSEC
- connecting Pi-hole to Unbound, the clean way π
In the Pi-hole admin UI:
- settings
- dns
- upstream DNS servers
- custom server: 127.0.0.1#5335
- disable all other upstream resolvers
Now the chain is complete:
- clients talk to Pi-hole
- Pi-hole forwards to Unbound
- Unbound talks to the internet
No leaks.
No shortcuts.
- naming things like an adult π·οΈ
internal DNS records
Define local DNS records in Pi-hole:
proxmox-01.home.arpa 192.168.1.41
proxmox-02.home.arpa 192.168.1.42
proxmox-03.home.arpa 192.168.1.43
nas-01.home.arpa 192.168.1.50
ip plan
component | ip
router | 192.168.1.1
dns-01 | 192.168.1.169
proxmox-01 | 192.168.1.41
proxmox-02 | 192.168.1.42
proxmox-03 | 192.168.1.43
synology | 192.168.1.50
Predictable naming beats clever naming.
Every single time.
- proof before trust, always π
Test recursive resolution:
dig google.com @192.168.1.169
Test DNSSEC:
dig dnssec-failed.org @192.168.1.169
Expected result:
- SERVFAIL means DNSSEC is working β Test internal names:
dig proxmox-01.home.arpa
Watch live DNS traffic:
pihole -t
If something breaks later, this is where you start.
- how people accidentally sabotage their own DNS β οΈ
Adding a public DNS as secondary:
- internal names stop resolving
- ExternalDNS becomes unreliable
- cert-manager fails silently
Using .local:
- mDNS conflicts
- inconsistent resolution
- debugging hell
Running DNS inside Kubernetes first:
- bootstrap deadlock
- cluster depends on itself
- guaranteed pain
- why this DNS layer unlocks everything else π
With this setup in place:
- Proxmox nodes resolve consistently
- Kubernetes nodes bootstrap cleanly
- ExternalDNS can manage records safely
- cert-manager can issue internal certificates
- ingress becomes predictable
This is the point where the homelab stops feeling fragile.
Next article:
Teaching your router whoβs boss π§ π‘
- lessons learned the hard way, so you do not have to π
DNS should be boring.
Silent.
Predictable.
If DNS is exciting, something is wrong.
This setup is:
- reproducible
- observable
- production-inspired
- intentionally boring
Hard now.
Peace later.
Happy Clustering :)
Top comments (0)