Introduction
Split-horizon DNS — also known as split-brain DNS or split DNS — is a technique where a DNS server returns different answers depending on the origin of the query. Internal clients querying from within the corporate network receive private IP addresses, while external clients on the internet receive public-facing addresses. This is critical for any organisation running internal infrastructure that is also partially exposed to the internet.
BIND9 implements split-horizon DNS through a powerful feature called views. A view defines a set of DNS zones and the set of clients (by IP range or ACL) that will be served by that view. This guide walks through a complete, production-ready split-horizon DNS setup using BIND9 on Ubuntu 22.04 LTS for the domain
solvethenetwork.com, hosted on the server ns-infrarunbook-01.solvethenetwork.com.
Scenario: The web serverwww.solvethenetwork.comhas an internal IP of10.10.1.50and a public IP of203.0.113.100. Internal staff should resolve to10.10.1.50; external users should resolve to203.0.113.100.
Prerequisites
- Ubuntu 22.04 LTS server with root or sudo access
- BIND9 installed (
apt install bind9 bind9utils bind9-doc
) - Internal subnet:
10.10.0.0/16
- External interface with public IP:
203.0.113.1
(DNS server public IP) - Domain:
solvethenetwork.com
Step 1 — Install BIND9
If BIND9 is not yet installed on ns-infrarunbook-01, install it now:
apt update
apt install -y bind9 bind9utils bind9-doc dnsutils
systemctl enable named
systemctl start named
Verify BIND9 is running and listening:
systemctl status named
ss -tulnp | grep :53
Step 2 — Define ACLs in named.conf.options
Access Control Lists (ACLs) define which client ranges are considered "internal". Open
/etc/bind/named.conf.optionsand add your ACL definitions at the very top, before the
optionsblock:
acl "infrarunbook-internal" {
10.10.0.0/16;
127.0.0.1;
::1;
};
options {
directory "/var/cache/bind";
recursion yes;
allow-recursion { infrarunbook-internal; };
listen-on { any; };
listen-on-v6 { any; };
dnssec-validation auto;
auth-nxdomain no;
// Do not allow zone transfers to anyone by default
allow-transfer { none; };
// Prevent DNS amplification attacks
allow-query { any; };
};
Key points:
- Recursion is restricted to
infrarunbook-internal
— external clients cannot use this server as a recursive resolver. - Zone transfers are denied globally; they will be enabled per-view if needed for secondaries.
allow-query { any; }
permits all clients to query authoritative zones (both views will handle this selectively).
Step 3 — Configure Views in named.conf.local
Views are defined in
/etc/bind/named.conf.local. Each view must list its
match-clientsand reference the relevant zone files. The internal view must appear first. BIND evaluates views in order — the first match wins.
// Internal view — served to corporate clients on 10.10.0.0/16
view "internal" {
match-clients { infrarunbook-internal; };
recursion yes;
allow-recursion { infrarunbook-internal; };
zone "solvethenetwork.com" {
type master;
file "/etc/bind/zones/internal/db.solvethenetwork.com";
allow-transfer { none; };
};
zone "10.10.in-addr.arpa" {
type master;
file "/etc/bind/zones/internal/db.10.10";
allow-transfer { none; };
};
include "/etc/bind/named.conf.default-zones";
};
// External view — served to all other clients (internet)
view "external" {
match-clients { any; };
recursion no;
zone "solvethenetwork.com" {
type master;
file "/etc/bind/zones/external/db.solvethenetwork.com";
allow-transfer { none; };
};
};
Important: When using views, ALL zones — including the default zones (
localhost,
.hint, etc.) — must be inside a view. The
include "/etc/bind/named.conf.default-zones";line inside the internal view handles this. Do not include it outside any view.
Step 4 — Create the Zone File Directories
mkdir -p /etc/bind/zones/internal
mkdir -p /etc/bind/zones/external
chown -R root:bind /etc/bind/zones
chmod -R 775 /etc/bind/zones
Step 5 — Write the Internal Zone File
Create
/etc/bind/zones/internal/db.solvethenetwork.com. Internal clients will receive private IP addresses:
; Internal zone for solvethenetwork.com
; Served only to: 10.10.0.0/16
$TTL 300
@ IN SOA ns-infrarunbook-01.solvethenetwork.com. hostmaster.solvethenetwork.com. (
2024022701 ; Serial (YYYYMMDDNN)
3600 ; Refresh
900 ; Retry
604800 ; Expire
300 ) ; Negative cache TTL
; Name servers
@ IN NS ns-infrarunbook-01.solvethenetwork.com.
; A records — INTERNAL addresses
ns-infrarunbook-01 IN A 10.10.0.10
www IN A 10.10.1.50
app IN A 10.10.1.51
db-primary IN A 10.10.2.10
db-replica IN A 10.10.2.11
smtp IN A 10.10.3.5
vpn IN A 10.10.0.254
; CNAME records
api IN CNAME app.solvethenetwork.com.
Step 6 — Write the Internal Reverse Zone File
Create
/etc/bind/zones/internal/db.10.10for PTR records of the
10.10.0.0/16subnet:
; Reverse zone for 10.10.0.0/16
$TTL 300
@ IN SOA ns-infrarunbook-01.solvethenetwork.com. hostmaster.solvethenetwork.com. (
2024022701 ; Serial
3600 ; Refresh
900 ; Retry
604800 ; Expire
300 ) ; Negative cache TTL
@ IN NS ns-infrarunbook-01.solvethenetwork.com.
; PTR records
10.0 IN PTR ns-infrarunbook-01.solvethenetwork.com.
50.1 IN PTR www.solvethenetwork.com.
51.1 IN PTR app.solvethenetwork.com.
10.2 IN PTR db-primary.solvethenetwork.com.
11.2 IN PTR db-replica.solvethenetwork.com.
5.3 IN PTR smtp.solvethenetwork.com.
254.0 IN PTR vpn.solvethenetwork.com.
Step 7 — Write the External Zone File
Create
/etc/bind/zones/external/db.solvethenetwork.com. External clients receive only public IPs. Never expose RFC 1918 addresses here:
; External zone for solvethenetwork.com
; Served to: any (internet clients)
$TTL 3600
@ IN SOA ns-infrarunbook-01.solvethenetwork.com. hostmaster.solvethenetwork.com. (
2024022701 ; Serial
3600 ; Refresh
900 ; Retry
604800 ; Expire
3600 ) ; Negative cache TTL
; Name servers
@ IN NS ns-infrarunbook-01.solvethenetwork.com.
; A records — PUBLIC addresses only
ns-infrarunbook-01 IN A 203.0.113.1
www IN A 203.0.113.100
smtp IN A 203.0.113.105
vpn IN A 203.0.113.110
; MX record
@ IN MX 10 smtp.solvethenetwork.com.
; TXT records for email authentication
@ IN TXT "v=spf1 mx a:smtp.solvethenetwork.com ~all"
Note the significantly higher TTL (
3600seconds) in the external zone — external records change less frequently and benefit from longer caching. The internal zone uses
300seconds for faster propagation of internal changes.
Step 8 — Validate Configuration
Always validate before reloading BIND9 in production:
# Check global named configuration
named-checkconf
# Check the internal forward zone
named-checkzone internal /etc/bind/zones/internal/db.solvethenetwork.com
# Check the internal reverse zone
named-checkzone 10.10.in-addr.arpa /etc/bind/zones/internal/db.10.10
# Check the external forward zone
named-checkzone external /etc/bind/zones/external/db.solvethenetwork.com
All checks should return
OKwith no errors. Fix any reported issues before proceeding.
Step 9 — Reload BIND9
systemctl reload named
# OR use rndc for a graceful reload without stopping the daemon:
rndc reload
Check the systemd journal for errors:
journalctl -u named --since "1 minute ago"
Step 10 — Test Split-Horizon Resolution
Test from an internal host (10.10.x.x range):
# From an internal client (expects 10.10.1.50)
dig @10.10.0.10 www.solvethenetwork.com A +short
# Expected: 10.10.1.50
# Reverse lookup
dig @10.10.0.10 -x 10.10.1.50 +short
# Expected: www.solvethenetwork.com.
Test from an external host (or simulate with localhost using a different source):
# From an external client (expects 203.0.113.100)
dig @203.0.113.1 www.solvethenetwork.com A +short
# Expected: 203.0.113.100
# Confirm recursion is refused for external clients
dig @203.0.113.1 google.com A +short
# Expected: connection refused / REFUSED (recursion not available)
Advanced: Adding a Secondary DNS Server with View-Aware Zone Transfers
Secondary DNS servers in a split-horizon setup must also use views and must use TSIG keys for secure zone transfers. Here is an example using TSIG to allow a secondary (
ns-infrarunbook-02at
10.10.0.11) to transfer the internal view zone:
# Generate TSIG key on ns-infrarunbook-01
tsig-keygen -a HMAC-SHA256 infrarunbook-xfer > /etc/bind/tsig-xfer.key
# Output looks like:
# key "infrarunbook-xfer" {
# algorithm hmac-sha256;
# secret "base64encodedkeyhere==";
# };
Include the key file in
/etc/bind/named.confand reference it in the view:
include "/etc/bind/tsig-xfer.key";
view "internal" {
match-clients { infrarunbook-internal; key infrarunbook-xfer; };
...
zone "solvethenetwork.com" {
type master;
file "/etc/bind/zones/internal/db.solvethenetwork.com";
allow-transfer { key infrarunbook-xfer; };
also-notify { 10.10.0.11; };
};
};
Troubleshooting Common Issues
- Error: "not in any view" — You have zones defined outside all views. Move all zones (including defaults) inside views.
- Error: "view 'internal' not yet defined" — ACL used in a view is defined after the view. Define all ACLs in
named.conf.options
beforenamed.conf.local
is included. - External clients getting internal IPs — Your view order is wrong, or the
match-clients
ACL overlaps. External view must usematch-clients { any; };
and appear last. - Recursion available to external clients — Ensure
recursion no;
is set inside the external view, overriding any global setting. - Zone serial number not updated — After editing zone files, always increment the serial number or BIND9 secondary servers will not pull the new zone.
Frequently Asked Questions
Q: What is split-horizon DNS and why do I need it?
A: Split-horizon DNS returns different DNS answers based on the client's source IP. It allows internal users to access services via private IPs (lower latency, no hairpinning through NAT) while external users receive public IPs. Without it, internal clients may fail to reach services if NAT hairpinning is not configured on the firewall.
Q: Can I use split-horizon DNS with DNSSEC?
A: Yes, but each view must be signed independently. The internal and external zones are separate zone files and will have separate DNSSEC signatures. Internal zones do not need to be DNSSEC-signed if all internal resolvers trust the authoritative server directly, but external zones should always be signed.
Q: Do both views need to include the same hostname records?
A: No. You only need to include records that are relevant to each view's audience. For example, internal database server records (
db-primary,
db-replica) should only appear in the internal view. External-facing services like
wwwand
smtpappear in both views with their respective IPs.
Q: What happens if a client's IP is not matched by any view?
A: BIND9 returns REFUSED. Since the external view uses
match-clients { any; };, all clients will always match at least one view. The order matters — internal clients match the internal view first.
Q: How do I update a zone record in production without downtime?
A: Edit the zone file, increment the serial number, run
named-checkzoneto validate, then run
rndc reload. This reloads zone files without dropping any connections or queries in flight. There is no downtime during a zone reload.
Q: Can I have more than two views?
A: Yes. BIND9 supports an unlimited number of views. You could have separate views for
management-vlan(10.10.100.0/24),
dmz(172.16.50.0/24), and
external. Each view is matched in order, so the most specific ACLs should appear first.
Q: How do I configure secondary (slave) DNS servers in a split-horizon setup?
A: Secondary servers must also be configured with matching views and must use TSIG keys for zone transfers. The secondary's internal view pulls from the primary's internal view zone, and the external view pulls from the external zone. Without matching views on the secondary, zone transfers will fail.
Q: What is the recommended TTL for internal vs external zones?
A: Internal zones benefit from a lower TTL (300 seconds / 5 minutes) because internal IP changes propagate faster and clients flush their cache quickly. External zones can use higher TTLs (3600 seconds / 1 hour) because public IP changes are less frequent and longer caching reduces query load.
Q: Will split-horizon DNS break DNSSEC validation for internal clients?
A: Only if the internal zone has different records from the signed external zone and the internal resolver attempts to validate against the signed external zone data. To avoid this, either sign the internal zone separately or set the internal resolver to use the internal view directly (which uses the local authoritative data without needing to validate upstream).
Q: How do I verify which view a client is hitting?
A: Use
rndc querylog onto enable query logging, then query from the client. The BIND9 log will show the view name alongside each query. Alternatively, check the answer section — if
www.solvethenetwork.comreturns
10.10.1.50, you are in the internal view; if it returns
203.0.113.100, you are in the external view.
Q: Is split-horizon DNS a substitute for a firewall?
A: Absolutely not. Split-horizon DNS is a convenience and performance tool, not a security boundary. An attacker who can reach internal services by guessing their IP addresses does not need DNS at all. Always enforce network-level access controls (firewall rules, VPN requirements) for internal services, regardless of DNS configuration.
