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.
Related Articles
- [DNS] Configuring Dynamic DNS (DDNS) with BIND9 and nsupdate – Production Guide
- [DNS] Configuring BIND9 Response Policy Zones (RPZ) for DNS Firewall Protection – Production Guide
- [DNS] Securing BIND9 Zone Transfers with TSIG Keys – Complete Production Guide
- [DNS] How to Configure DNSSEC on BIND9 (Ubuntu) – Complete Step-by-Step Guide
