KnowledgebaseNetworking & DNS › Split-horizon DNS — different answers for inside vs outside your network

Split-horizon DNS — different answers for inside vs outside your network

Split-horizon DNS is the pattern where the same hostname resolves to different IPs depending on who's asking. Inside your network (or your VPN), db.example.com resolves to a private IP; from the public Internet, it resolves to nothing (or to a different IP, like a reverse proxy). This article covers when split-horizon helps, when it backfires, and how to set it up.

Why bother

  • Internal hostnames that shouldn't appear in public DNSdb.internal, monitoring.internal. You want VPN-connected clients to resolve them; you don't want them indexed by Internet scanners.
  • Same hostname, different paths — internal traffic to api.example.com goes direct to the application server (skipping the public load balancer); external traffic goes through the LB. Reduces latency for internal services.
  • Test environments mirroring production hostnames — your laptop on the dev VPN resolves app.example.com to a staging box, while the public sees production.

The two main approaches

Approach 1: separate resolvers per network

Public clients use public DNS (Route 53, Cloudflare, or whatever hosts your zone). VPN clients are configured to use an internal resolver (running on your VPN gateway, or a dedicated internal VPS). The internal resolver has its own zone or overlay that returns the private answers.

Pros: clean separation, easy to reason about. Public DNS doesn't even know the internal records exist.

Cons: requires VPN clients to be pushed the internal resolver via the VPN config (WireGuard's DNS = ... field, dnsmasq with split-DNS, NetworkManager VPN settings).

Approach 2: single resolver, views/policies

One DNS server runs both public and private zones, and uses ACLs ("views" in BIND, "match-clients" in dnsmasq) to decide which answer to return based on the client's IP. Internal subnet gets internal answer; everyone else gets the public answer.

Pros: one place to manage records. No client-side config needed (clients all use the same resolver IP).

Cons: the same server handles both — if it's compromised, both views are exposed. The config is non-trivial.

BIND views example

// /etc/bind/named.conf
acl internal {
    10.99.0.0/24;        // VPN subnet
    127.0.0.0/8;
};

view "internal" {
    match-clients { internal; };
    zone "example.com" {
        type master;
        file "/etc/bind/zones/example.com.internal";
    };
};

view "external" {
    match-clients { any; };
    zone "example.com" {
        type master;
        file "/etc/bind/zones/example.com.external";
    };
};

The two zone files have different content — internal has all the records including db, monitoring, etc. with private IPs; external has only what you want public.

Gotcha: BIND views require both zones to be served from the same daemon. You can't have a separate BIND for internal and your existing public zone elsewhere; mix them and you'll have inconsistent answers.

The simpler approach: dnsmasq on the gateway

For a small WireGuard road-warrior setup, run dnsmasq on the VPN gateway. WireGuard clients push DNS = 10.99.0.1 (the gateway's VPN IP); dnsmasq forwards public lookups upstream and serves overlay records for internal names.

# /etc/dnsmasq.conf on the VPN gateway
# Listen on the VPN interface only
interface=wg0
bind-interfaces

# Forward public lookups upstream
server=1.1.1.1
server=8.8.8.8

# Internal records
address=/db.internal/10.0.0.10
address=/monitoring.internal/10.0.0.20

# Override a public hostname for internal clients
host-record=app.example.com,10.0.0.30

Restart dnsmasq, connect over VPN, nslookup db.internal from the client returns 10.0.0.10. From a non-VPN device, the same lookup returns NXDOMAIN.

Cases where split-horizon backfires

  • HSTS / certificate pinning. If app.example.com serves a different cert internally (signed by an internal CA) than externally (signed by Let's Encrypt), browsers / mobile apps with strict HSTS or pinning will refuse the internal cert. Use the same cert + CA in both views, or use a different hostname for internal access.
  • Email DNS. Mail-related DNS (MX, SPF, DKIM, DMARC) should NOT be split-horizon — the values must be globally consistent. Splitting these breaks mail in subtle ways that take weeks to diagnose.
  • Public CDN with private origin. If your public LB lives at a CDN edge and your origin is on a private VPS, split-horizon to "shortcut" the CDN can cause cache-inconsistency between internal and external users.
  • Mobile clients that don't honor pushed DNS. iOS in particular has historically been finicky about VPN-pushed DNS. Test thoroughly on each client OS before relying on split-horizon to enforce private access.

When NOT to use split-horizon

If your only goal is "I don't want this service public-facing," the simpler answer is: bind the service to the VPN interface only and don't add a public DNS record at all. You then access it via the IP directly, or via a hostname in a separate top-level domain (.internal, or a subdomain like internal.example.com) that's only published in internal DNS. Cleaner mental model than split-views, fewer surprises.

Also Read

« « Back

Powered by WHMCompleteSolution