Every connection to anything starts with a name being turned into an address. In this lab that translation is done by two machines the lab owns, running Unbound, rather than by the internet provider’s resolver or by whatever the router was shipped with.
Drafted by an AI agent (claude-opus-5) from this lab’s own runbooks, deployment log and errata. Reviewed before publication by the site owner. How this site is written →
That is not a purity argument. A network with private addressing in it has names that only mean something inside the network, and something has to know what they mean.
What it replaced
The provider’s resolver, which is what a network uses if nobody decides otherwise, and a public resolver configured on the router, which is the usual first improvement.
Both have the same disqualifying property for this lab: they can only answer with public information. An internal-only service has a name and a private address, and no resolver outside the network has ever heard of either.
A resolver built into something else was the other option, since most router firmware and several other tools in this lab could serve local names. That would have worked and was rejected on placement rather than capability, for reasons the next section covers.
Why this one
Two properties decided it, and the second matters more than it first appears.
It does both jobs in one place. Unbound is a recursive resolver, meaning it can answer public queries, and it can also be authoritative for a private zone of its own. So one service answers “what is the address of this internal machine” and “what is the address of some website”, and clients need to know about exactly one resolver.
Split-horizon resolution keeps internal traffic internal. This is the part that changes how the network behaves rather than just where answers come from.
Services in this lab are reachable from the public internet through a rented gateway. If an internal machine resolved a service the same way an outside visitor does, it would get the gateway’s public address, and traffic between two machines sitting on the same switch would travel out of the house, across the internet to a rented server, back down the tunnel, and into the machine next to it. That is called hairpinning, and it converts a local file copy into an internet round trip.
Internal names resolve to private addresses instead. A laptop on the network talking to the file server talks to it directly. The same service is still reachable from outside, through the gateway, over the same public name, and certificates are valid in both cases because the certificate belongs to the name rather than to the route.
DNSSEC validation is the smaller third reason. It is on, it is the default posture for a recursive resolver, and it costs nothing to have.
How it is actually used here
Two resolvers, live, on physically separate hypervisor hosts. The runbook records that as non-negotiable, and the reasoning is worth repeating because it generalizes past DNS: two copies of a service that share a physical machine are not redundant, they are one failure with extra configuration.
External queries are forwarded upstream over an encrypted transport to a filtering resolver rather than resolved from the root servers directly. That is a deliberate trade of some independence for content filtering and a smaller query footprint.
One resolver also watches the other and raises an alert if the secondary fails. That check exists for a specific reason: a failed secondary produces no symptoms at all while the primary is healthy, so without a check watching it, the failure would be discovered on the day the primary went down too, which is the worst possible day to discover it.
Why the split is two names rather than one name with two answers
The classic split-horizon setup gives one name two different answers depending on who asks. This lab does something slightly different: internal machines have their own names under an internal-only subdomain, and public services have public names, so there are two distinct records rather than one record resolving two ways.
The practical benefit is that the answer to “which one am I talking to” is visible in the name itself instead of depending on which resolver answered. Debugging a resolution problem where the same name legitimately returns different addresses depending on the asker is genuinely unpleasant, and this avoids the whole class of it.
The cost is that the internal names have to be maintained as a real zone, which is the zone file the resolvers are authoritative for.
What it cost
A verification failure that broke every external query while looking healthy. The forwarding configuration asked for the upstream’s certificate to be verified but never said what to verify it against, so every upstream connection failed at the handshake and every external name returned an error.
What makes it a good story rather than a typo is how thoroughly it hid. Three separate checks reassured: the configuration checker reported no errors, because it validates syntax rather than whether a handshake can complete. The service reported active, because it was genuinely running and answering. And internal resolution kept working perfectly, because internal names are answered from the local zone and never touch an upstream at all.
So the resolvers were up, valid, and correctly serving every name that was tested first. It was found by a test script’s first real run, not by a person noticing.
Running your own resolver means DNS failures are yours. When the provider’s resolver breaks, it breaks for everyone and somebody else is paged. When yours breaks, everything in the lab appears to break simultaneously, because everything starts with a name lookup. DNS failures also present as almost anything else, which the blog has already been caught by more than once.
The upstream is still a dependency. Forwarding to a filtering resolver buys filtering and costs independence: if that upstream is unreachable, external resolution stops, and a fallback that does not depend on it is a gap this project has noted rather than closed.
What would change the decision
The agent’s reasoning from the record rather than the human’s stated position.
- If the internal zone stopped needing to be authoritative. The whole argument rests on there being private names to serve. A lab with no internal-only services could use a public resolver and lose nothing.
- If resolving from the root became preferable to forwarding. That is a configuration change rather than a change of software, so it would not replace Unbound. It is listed because it is the most likely thing to actually change here.
About Unbound
Unbound is an open source validating, recursive, caching DNS resolver developed by NLnet Labs. It supports DNSSEC validation, encrypted transports for upstream queries, and serving authoritative answers for local zones alongside its recursive function. It is written in C with an emphasis on a small, auditable codebase, and is released under the BSD license.
- Website — nlnetlabs.nl/projects/unbound
- Documentation — unbound.docs.nlnetlabs.nl
- Source — github.com/NLnetLabs/unbound
- Community — issue tracker