
What is split-horizon DNS?
Split-horizon DNS (also called split-brain or split-view DNS) is a setup where the same domain name resolves to different answers depending on who's asking. Users inside your network, such as office staff or servers in your private cloud, get internal records like private IP addresses, while users on the public internet get the public records. For example, app.example.com might resolve to 10.0.1.20 for employees on the corporate network and to 203.0.113.20 for everyone else.
It lets you use one set of familiar hostnames everywhere, keeps internal infrastructure out of public DNS, and sends internal traffic along the shortest path. It also adds complexity, and a lot of confusing DNS problems come from split-horizon setups that have drifted out of sync. In this article you'll learn how split-horizon DNS works, why organisations use it, how to build it with BIND, Unbound, Windows Server DNS and cloud private zones, and the pitfalls to watch out for.
How Split-Horizon DNS Works
A DNS server running split horizon keeps two or more versions of a zone and picks one based on the source of each query.
- A client sends a query for
app.example.comto a DNS server. - The server checks where the query came from, usually by matching the source IP address against a list of internal networks. It might also use the network interface the query arrived on, a TSIG key, or, in cloud environments, which virtual network sent it.
- It selects a view: an internal one for trusted networks, an external one for everyone else.
- It answers from that view's copy of the zone.
In many organisations the two views live on different servers altogether. The public zone is hosted with a managed DNS provider, and internal resolvers have their own copy of example.com with private records. The result is the same: the answer depends on which server, and therefore which network, you ask.
A Simple Example
| Name | Internal answer | External answer |
|---|---|---|
www.example.com | 10.0.1.10 | 203.0.113.10 |
app.example.com | 10.0.1.20 | 203.0.113.20 |
git.example.com | 10.0.2.15 | No record (NXDOMAIN) |
mail.example.com | 10.0.3.25 | 203.0.113.25 |
Staff on the office network who open git.example.com reach the internal Git server. Anyone outside gets no answer at all, because the name only exists in the internal view.
You can see the effect with dig by asking each server directly:
$ dig @10.0.0.53 app.example.com A +short
10.0.1.20
$ dig @ns1.example.com app.example.com A +short
203.0.113.20
Why Use Split-Horizon DNS?
Keep Internal Names Private
Internal hostnames often reveal a lot: vpn-old, hr-payroll, jenkins-test. Publishing them in public DNS gives attackers a free map of your infrastructure. With split horizon, internal-only names never appear in public DNS.
Hiding names isn't a security control on its own, because the services still need proper authentication and firewalling. But there's no reason to advertise them.
Use the Same Name Everywhere
Without split horizon, staff might need app.example.com from home and app.corp.local in the office. Certificates, bookmarks, configuration files and single sign-on redirects all get more complicated when the name changes by location. With split horizon, everyone uses one name and DNS works out the right address.
This also matters for TLS certificates. Public certificate authorities only issue certificates for publicly registered domain names, not for made-up internal suffixes. Using a real domain such as example.com internally lets you use normal public certificates on internal services.
Avoid Hairpinning
When an internal user connects to a service's public IP address, traffic may have to leave the network and come straight back in through the firewall, known as hairpin NAT or NAT loopback. Some firewalls don't support it, and those that do add latency and load. Returning the private address internally sends traffic directly to the server.
Direct Cloud Traffic Privately
In the cloud, you might want db.example.com to resolve to a private endpoint inside your virtual network, so traffic never crosses the internet, while the public name still points somewhere else or doesn't exist publicly at all.
Choosing Your Internal Domain
Before you build split horizon, decide which name to use internally. There are three common choices:
| Option | Example | Notes |
|---|---|---|
| Same as public domain | example.com | Classic split horizon. One name everywhere, but you must maintain both views carefully. |
| Subdomain of public domain | corp.example.com or internal.example.com | Often the cleanest choice. Only the subdomain is internal, and the public zone is untouched. |
| Special-use internal name | home.arpa for home networks | Reserved for local use, but public CAs won't issue certificates for it. |
Avoid invented suffixes such as .corp, .local or .internal on business networks unless you have a clear reason. .local is used by multicast DNS (mDNS) and causes conflicts on Apple devices and Linux. .internal has been reserved for private use by ICANN, so it won't be delegated publicly, but you still can't get public certificates for it. Names that later become real public TLDs cause collisions.
A dedicated subdomain like corp.example.com gives you most of the benefits of split horizon with far fewer of its headaches, because the internal and external zones never contain the same names.
How to Set Up Split-Horizon DNS
BIND Views
BIND supports split horizon natively through views. Each view has a match-clients list and its own copy of the zones it serves:
acl "internal" {
10.0.0.0/8;
172.16.0.0/12;
192.168.0.0/16;
localhost;
};
view "internal" {
match-clients { internal; };
recursion yes;
zone "example.com" {
type primary;
file "/etc/bind/zones/internal/db.example.com";
};
};
view "external" {
match-clients { any; };
recursion no;
zone "example.com" {
type primary;
file "/etc/bind/zones/external/db.example.com";
};
};
Views are checked in order, so the internal view must come first and any must be last. Once you use views, every zone must be inside a view, including the default zones BIND normally loads.
The internal zone file then holds private addresses:
$TTL 300
@ IN SOA ns1.example.com. hostmaster.example.com. (
2026102101 3600 900 1209600 300 )
@ IN NS ns1.example.com.
ns1 IN A 10.0.0.53
www IN A 10.0.1.10
app IN A 10.0.1.20
git IN A 10.0.2.15
mail IN A 10.0.3.25
Check and reload:
sudo named-checkconf
sudo named-checkzone example.com /etc/bind/zones/internal/db.example.com
sudo rndc reload
Unbound: Override Only Some Names
If your public zone lives with a managed provider and you only need a few internal overrides, a recursive resolver such as Unbound can answer those names locally and resolve everything else normally. This avoids maintaining a full copy of the zone:
server:
interface: 10.0.0.53
access-control: 10.0.0.0/8 allow
# Answer these names locally; anything else under example.com
# is resolved from public DNS as usual
local-zone: "app.example.com." static
local-data: "app.example.com. 300 IN A 10.0.1.20"
local-zone: "git.example.com." static
local-data: "git.example.com. 300 IN A 10.0.2.15"
The static type means Unbound answers from local-data for that name and returns NXDOMAIN or NODATA for anything else at or below it, rather than asking the internet. Using a separate local-zone for each overridden name keeps the rest of example.com resolving publicly.
For a whole internal subdomain, you can declare it once:
server:
local-zone: "corp.example.com." static
local-data: "git.corp.example.com. 300 IN A 10.0.2.15"
local-data: "wiki.corp.example.com. 300 IN A 10.0.2.16"
Test the configuration and reload:
sudo unbound-checkconf
sudo unbound-control reload
dnsmasq on a Small Network
On a home lab or small office router running dnsmasq, a single line per name does the job:
address=/app.example.com/10.0.1.20
address=/git.example.com/10.0.2.15
Be aware that address=/name/ also matches every subdomain below that name.
Windows Server DNS Policies
Windows Server DNS supports split horizon through zone scopes and query resolution policies. This PowerShell example creates an internal scope for example.com and serves it to clients in the 10.0.0.0/8 range:
# Define the internal client subnet
Add-DnsServerClientSubnet -Name "InternalSubnet" -IPv4Subnet "10.0.0.0/8"
# Create a scope inside the existing zone
Add-DnsServerZoneScope -ZoneName "example.com" -Name "InternalScope"
# Add the internal version of the record to that scope
Add-DnsServerResourceRecord -ZoneName "example.com" -A -Name "app" `
-IPv4Address "10.0.1.20" -ZoneScope "InternalScope"
# Serve the internal scope to internal clients
Add-DnsServerQueryResolutionPolicy -Name "SplitBrainPolicy" -Action ALLOW `
-ClientSubnet "eq,InternalSubnet" -ZoneScope "InternalScope,1" `
-ZoneName "example.com"
Clients that don't match the policy get the records in the zone's default scope. In Active Directory environments, an alternative is simply to host an internal copy of the zone on domain controllers, which then act as the internal view.
Cloud Private Zones
Cloud providers offer split horizon as a managed feature:
- Amazon Route 53 private hosted zones are associated with one or more VPCs. Resources in those VPCs get the private answers; everyone else gets the public hosted zone for the same name.
- Azure Private DNS zones are linked to virtual networks in the same way.
- Google Cloud DNS private zones are visible only to the networks you authorise.
For example, creating a Route 53 private hosted zone with the AWS CLI:
aws route53 create-hosted-zone \
--name example.com \
--caller-reference "private-example-$(date +%s)" \
--vpc VPCRegion=eu-west-2,VPCId=vpc-0abc1234def567890 \
--hosted-zone-config Comment="Internal view",PrivateZone=true
In a private zone, a name that isn't defined returns NXDOMAIN for clients in the associated VPC, even if the public zone has it. If you want missing names to fall through to public DNS, you need to copy those records into the private zone too, or use a dedicated subdomain instead.
Common Pitfalls
The Two Views Drift Apart
The single biggest problem with split horizon is maintenance. Someone adds a record to the public zone and forgets the internal one, or the other way round, and suddenly a service works from home but not in the office. To avoid it:
- Generate both views from a single source, for example a script or DNS-as-code repository that outputs both.
- Only override the records that genuinely need different answers, and resolve the rest publicly where your software allows.
- Prefer a dedicated internal subdomain, so the views don't overlap at all.
Email and Verification Records
Records such as MX, SPF, DKIM, DMARC and domain verification TXT records usually only matter in the public view. But if internal mail servers or applications look them up through internal DNS, they need to exist in the internal view too. Missing TXT records internally can cause mail and authentication failures that only happen on the corporate network.
Laptops That Move Between Networks
A laptop that cached the internal answer in the office and then moves to a café will keep using the private address until the cache expires. Short TTLs on overridden records, such as five minutes, keep this window small. VPN clients should also be configured to use internal resolvers while connected, and to stop using them when disconnected.
Browsers Using Encrypted DNS
Browsers with DNS over HTTPS enabled may send queries straight to a public resolver, bypassing your internal DNS and its split-horizon answers. Firefox checks the canary domain use-application-dns.net, and if your resolver returns NXDOMAIN for it, Firefox disables its default DoH. Enterprise browser policies for Chrome, Edge and Firefox let you control DoH settings centrally, which is the more reliable approach on managed devices.
You can check what your internal resolver returns for the canary domain:
dig @10.0.0.53 use-application-dns.net A +noall +comments | grep status
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 41377
DNSSEC Complications
If your public zone is signed with DNSSEC and your internal view isn't, validating resolvers inside your network may reject the internal answers, because they expect signatures for example.com. The usual fixes are to configure internal resolvers to treat the internal zone as authoritative locally (so they answer directly rather than validating from the root), add a negative trust anchor for the internal zone, or use a separate internal subdomain that's delegated in a way your resolvers understand.
Debugging Confusion
When someone reports "the site is down", the first question becomes "from where?". Always check which resolver the user's device is using, and query both views explicitly:
# Which resolver is this machine using? (systemd-resolved)
resolvectl status | grep "DNS Servers"
# Compare both views
dig @10.0.0.53 app.example.com A +short
dig @1.1.1.1 app.example.com A +short
FAQ: Split-Horizon DNS
Yes. Split-horizon, split-brain and split-view DNS all describe the same idea: giving different answers to the same query depending on the network it comes from.
Partly. It keeps internal hostnames and private addresses out of public DNS, which reduces information leakage. But it doesn't protect the services themselves, so you still need firewalls, authentication and access controls.
A dedicated subdomain such as corp.example.com is usually easier to manage, because internal and external zones never overlap. Using the full public domain internally works too, but it requires keeping both views in sync.
In a split-horizon setup, the most common cause is a record that exists in the public view but is missing or out of date in the internal view. Query the internal resolver directly with dig to compare.
Yes, as long as the VPN client sends DNS queries to your internal resolvers while it's connected. Many VPNs support split DNS, sending only queries for internal domains to internal servers and everything else to the normal resolver.
Yes. Route 53 private hosted zones, Azure Private DNS and Google Cloud DNS private zones all provide internal-only answers for networks you associate with them, alongside public zones for the same name.
Conclusion
Split-horizon DNS answers the same question differently for internal and external users, so staff and internal systems reach private addresses while the public sees only what you choose to publish. It keeps internal names private, avoids hairpin routing, and lets everyone use one consistent set of hostnames and certificates.
The trade-off is extra moving parts. Keep the views generated from a single source, override only what you need, use short TTLs on internal answers, and think about VPNs, encrypted DNS in browsers and DNSSEC before you roll it out. Where you can, a dedicated internal subdomain gives you most of the benefits with far less to maintain.


