Part 23: The name-resolution checklist
TL;DR
- DNS has been a gap in this arc since the early posts: every networking post assumed names resolve and none of them explained what actually resolves them. This closes it.
- The resolver for the whole homelab is Blocky, wired as the upstream for the network's DHCP-assigned clients: DoT to the real upstream resolvers, light blacklists for the things worth blocking, and manual entries for the services that should resolve locally at the network level.
- Blocky over pihole, in my view, comes down to one decisive point: a much more comprehensive configuration surface for my needs, and built-in TLS for upstream queries - no additional unbound to stand up and babysit just to have DoT.
- The checklist format is deliberate: this is the small, unglamorous post that verifies the promises the other posts quietly assumed were true.
The gap this post closes
Somewhere around the mail post, DNS started appearing in the margins. Deliverability was "half DNS." The mail server needed a hostname that resolved the way the world expects. The border posts gave the cluster routable addresses and assumed the names would follow. And every reader who's run anything self-hosted knows the truth: DNS is the thing that's fine right up until it isn't, and when it isn't, everything isn't.
So this is the checklist post. Not the most glamorous entry in the series, but the ones before it quietly assumed a working name-resolution layer, and I'd rather verify the assumption than keep it quiet. The checklist format is deliberate, too: each item below is a promise one of the earlier posts made, and this post is where it gets checked off.
The resolver: Blocky
The homelab's resolver is Blocky, and it serves two roles at once, which is exactly why it earned a place on the estate under the filters from post 22: it carries a genuine privacy purpose - it's the point where the whole network's DNS queries get sanitized, encrypted, and filtered before they leave the house - and its operational rent is negligible, because Blocky is very light. A small Go binary that does one job and doesn't ask to be babysat.
Its job: be the single DNS answer for the homelab. DHCP hands out Blocky's address to every device on the network, and from that point on every query from every laptop, phone, and appliance flows through one small chokepoint I control. Which is the whole point: DNS is one of the last places a modern network leaks. Every hostname a device looks up is a small confession about what it's doing, and Blocky is where those confessions get read before they go anywhere.
DoT, and the unbound I didn't have to deploy
Upstream, Blocky talks DNS-over-TLS to real public resolvers. Plaintext DNS on port 53 to the ISP resolver was never an acceptable end of the pipeline - that's the leak the whole exercise exists to close - and DoT closes it without ceremony.
Here's the part that matters in a comparison-shopping sense: I don't have to have an unbound in place somewhere just to have TLS for DNS queries. The classical pihole setup for encrypted upstream is pihole plus unbound - one box to do the filtering, another to do the TLS - which is two services, two failure points, and two things to patch for one job. Blocky does both in one binary: filtering and encrypted upstream in the same config file. For my money that's also where its configuration set is simply more comprehensive than pihole's for what I need: upstreams as first-class config, per-upstream TLS, grouping and failover between them, and the blocking lists as a declarative layer rather than a UI toggle. IMHO, but a fairly settled one at this point.
Blacklists stay light, deliberately. The goal is the network's obvious annoyances - ads, telemetry, the worst tracking networks - not a maximal blocklist that turns DNS into a helpdesk ticket generator for the household. When something legitimate gets caught, the fix is a whitelist entry and ten seconds. The blocking is a convenience that pays rent in privacy; it isn't the point of the deployment.
Manual entries: the network-level pin
The third job, and the quiet reason a network-level resolver earns its keep in a homelab with a cluster: manual entries. Services that should resolve to local addresses, pinned at the network level so every device gets the answer without knowing anything about it.
This is the payoff of the whole design. When the mail server, the forge, or any cluster-fronted service should be reachable from the LAN by name, the answer comes from Blocky's config, not from each device's hosts file or a browser extension or an /etc/hosts gospel that only my machine has. It also means those local answers are version-controlled in the same repo as everything else on the estate - a DNS record is a one-line config change, committed, with the same GitOps discipline as the cluster's own state. Move a service, change a line, and the whole network follows.
And Blocky runs on the cluster itself, which is where BGP earns its keep. Cilium advertises the LoadBalancer address into the network, so the DNS endpoint is a single cluster IP that every subnet and VLAN already knows how to reach - no per-subnet routes to maintain, no "which firewall rule forwards port 53 where" archaeology. The router learns the route via BGP, and Cilium load-balances the queries across the Blocky replicas behind it. Out in the network, DHCP configurations vary - some clients get Blocky's address handed to them directly, others point at the router which forwards upstream to Blocky - but the chain always lands in the same place, and Blocky is in it either way. DNS gets the same treatment as everything else on this estate: a service, horizontally spread, with the network doing the routing homework instead of me.
flowchart LR
DEV["Every device on the network<br/>(via DHCP)"] --> BLK["Blocky<br/>(cluster IP via BGP,<br/>load-balanced by Cilium)"]
BLK -->|"manual entries<br/>(version-controlled)"| LOC["Local answers<br/>for estate services"]
BLK -->|"light blacklists"| DROP["Ads, telemetry, trackers:<br/>blocked"]
BLK -->|"DoT -<br/>encrypted upstream"| UP["Public resolvers"]
WORLD["The outside world"] --> R53["Public zones: Route53<br/>(delegated, DNS-01 for certs)"]
One precision, because the next paragraph depends on it: the public zones live in Route53. The private, local side of DNS is self-hosted - Blocky for the LAN, internal records where the estate needs them - but anything the outside world resolves goes through Route53, and that split matters more than it sounds. It means cert-manager needs automation-capable credentials for Route53: DNS-01 challenges against the public zone are how certificates for internal-sounding hostnames get issued at all, and a credential that can do that unattended is a privilege worth being deliberate about. That's its own story; more on that later.
The checklist
Since this is the checklist post, the checklist:
- Upstream privacy - every client's queries leave the house encrypted (DoT), through resolvers that aren't the ISP's default. ✓
- Filtering - light blocklists at the network level, applied to every device whether it knows or cares. ✓
- Local pinning - estate services resolve to local addresses for the whole LAN, from version-controlled config. ✓
- One component - filtering + TLS upstream + local entries in a single light binary, no unbound companion. ✓
- The cluster's own DNS - a separate question (CoreDNS and the pod network), which stays inside the cluster and was never this post's problem. ✓ by deliberate scoping.
- Public zones in Route53 - self-hosted where self-hosting makes sense (the LAN side), delegated where the world needs answers; cert-manager's DNS-01 automation leans on that split. ✓ (credentials story to come).
That's the checklist closed. DNS is boring, which is the highest compliment available for infrastructure - it means it's working, and this post exists so the boring is on the record rather than in my head.