Lab note / DNS & networking

DNS with a deliberate path.

Pi-hole filtering, Unbound encrypted upstream resolution and CoreDNS: explicit boundaries and observable behavior.

LAN resolution / documented path
  1. RequestLAN client
  2. FilterPi-hole
  3. ResolveUnbound
  4. Encrypted upstreamDoT · TCP/853

Internal hops: TCP/UDP 53. CoreDNS external forwarding is a separate redesign item.

More than a name lookup

DNS is a shared dependency for home devices, Kubernetes workloads and internal services. My homelab separates those responsibilities rather than treating every lookup as the same problem.

Pi-hole provides filtering for LAN clients. Unbound handles upstream resolution. CoreDNS remains responsible for Kubernetes service discovery and selected internal hostname overrides. The purpose is a DNS path that can be explained, constrained and observed—not simply a resolver that answers.

Two paths, different responsibilities

LAN clients: filter first, then resolve

Home devices use a dedicated MetalLB LoadBalancer service for Pi-hole, with TCP and UDP DNS on port 53. They do not use CoreDNS directly. Pi-hole forwards to unbound.dns.svc.cluster.local.

The architecture document describes encrypted upstream connections through DNS-over-TLS on TCP/853, with no internet-bound port 53 egress from the Unbound namespace.

The encryption boundary matters: client-to-Pi-hole and Pi-hole-to-Unbound traffic remains DNS on port 53. This is encrypted upstream resolution, not end-to-end encryption from every device.

Pi-hole also has DNSSEC and query logging enabled. DNSSEC validation and transport encryption serve different purposes; neither removes the need to control access to the resolver.

Kubernetes: keep service discovery internal

CoreDNS resolves cluster.local names and overrides selected internal hostnames so they resolve to the internal Kong entrypoint rather than public addresses.

This split-horizon approach avoids unnecessary trips through the public edge. It is relevant to internal API calls and OIDC callbacks: a familiar hostname can resolve internally without a connection leaving and returning through hairpin NAT.

The gap that still needs closing

The documented CoreDNS upstream path is not yet the target design. Non-cluster lookups follow the node resolver configuration to a public resolver over plaintext DNS.

The planned change sends those external lookups through Pi-hole and Unbound, allowing Kubernetes and LAN clients to share the controlled upstream exit. That path should be defined in Git rather than inherited from node configuration.

This is a design target, not a claim that all DNS traffic is already encrypted. The source also calls out avoiding resolver loops, with Unbound using dnsPolicy: Default for its own resolution needs.

Boundaries that match the service

The documented DNS namespace policies start with deny-by-default and add explicit paths.

BoundaryAllowed purpose
LAN and CoreDNS → Pi-holeDNS over TCP/UDP 53
Pi-hole → UnboundUpstream DNS over TCP/UDP 53
Unbound → selected upstreamsDNS-over-TLS over TCP/853
Cloudflare Tunnel → Pi-holeAdministrative interface on TCP/80
Observability → exportersMetrics collection
Pi-hole → internetHTTPS filtering-list updates

Allowing CoreDNS to contact Pi-hole is not the same as configuring it to forward there. Application configuration and network policy must agree.

The Pi-hole administrative interface is published through Cloudflare Tunnel. This separates its access route from the LAN-facing DNS service. The source does not establish a particular identity policy for that tunnel, so this note makes no additional access-control claim.

Make the resolver path observable

The operational view combines metrics from all three components rather than relying on one service being reachable.

ComponentMetrics endpointDashboard focus
CoreDNSBuilt-in metrics, port 9153Request rate, latency, cache hits and response errors
Pi-holeExporter, port 9617Query volume, blocked percentage, clients and domains
UnboundExporter, port 9167Upstream latency, cache performance and SERVFAIL responses

ServiceMonitors connect these endpoints to Prometheus. Grafana dashboards are provisioned as ConfigMaps, with a DNS Overview alongside component-specific views. Unbound query logs also flow through Promtail and Loki into Grafana.

The documented alert conditions are concrete:

  • CoreDNS SERVFAIL responses exceed 5% for five minutes.
  • CoreDNS p99 latency exceeds 500 ms for five minutes.
  • Unbound SERVFAIL responses exceed 1% for five minutes.
  • The Pi-hole exporter target is absent for five minutes.

These are configured thresholds, not measured availability or performance results. They provide a starting point for detecting a failing dependency before it becomes an unexplained application problem.

What this work demonstrates

This is a small environment with real architectural decisions: separate service discovery from client filtering, define the upstream trust boundary, constrain network access and collect evidence of behavior.

The remaining CoreDNS change is part of that story. An explicit gap with a defined target is more actionable than a blanket statement that DNS is secure. The aim is a repeatable service whose routing, policy and operational signals remain understandable as the homelab evolves.

Implementation reference

Based on the homelab DNS architecture document, reviewed on 5 September 2026. This article summarizes the documented design; it is not an independent verification of the running cluster.

For the wider metrics approach, see Giving home systems a common metrics language.