CoreDNS can be configured to serve different DNS records to clients based on their network origin, a technique called split-horizon DNS.

Let’s see this in action. Imagine example.com has a public website at www.example.com and an internal-only service at internal.example.com. We want external clients to resolve www.example.com to a public IP, but internal clients to resolve it to a private IP, while internal.example.com is only resolvable internally.

Here’s a CoreDNS configuration (Corefile) that achieves this:

.:53 {
    log
    errors
    cache 30
    forward . 8.8.8.8 1.1.1.1

    # Internal network definition
    acl internal {
        192.168.1.0/24
        10.0.0.0/8
    }

    # Internal view
    route internal {
        # Serve internal-only records first
        file db.internal.local
        # If not found, fall back to external records
        forward . /etc/resolv.conf
    }

    # External view (default)
    route . {
        file db.external.local
        forward . /etc/resolv.conf
    }
}

And the corresponding zone files:

db.external.local:

$TTL 60
@   IN SOA ns1.example.com. admin.example.com. (
            2023010101 ; serial
            3600       ; refresh
            1800       ; retry
            604800     ; expire
            60 )       ; minimum TTL
    IN NS ns1.example.com.

ns1 IN A 203.0.113.10
www IN A 203.0.113.20

db.internal.local:

$TTL 60
@   IN SOA ns1.example.com. admin.example.com. (
            2023010101 ; serial
            3600       ; refresh
            1800       ; retry
            604800     ; expire
            60 )       ; minimum TTL
    IN NS ns1.example.com.

ns1 IN A 192.168.1.5
www IN A 192.168.1.20
internal IN A 192.168.1.30

When an internal client (e.g., 192.168.1.100) queries for www.example.com, CoreDNS sees it’s from the internal ACL. The route internal block is activated. It first checks db.internal.local and finds www IN A 192.168.1.20. This is returned.

When an external client (e.g., 203.0.113.50) queries for www.example.com, it doesn’t match the internal ACL. The default route . block is used. It checks db.external.local and finds www IN A 203.0.113.20. This is returned.

If the internal client queries for internal.example.com, it’s found in db.internal.local and resolved to 192.168.1.30. If an external client queries for internal.example.com, it’s not in db.external.local and the forward directive will attempt to resolve it externally, likely resulting in NXDOMAIN (non-existent domain) if it’s truly internal-only.

The acl directive defines named groups of IP networks. The route directive acts as a conditional plugin chain. If the client’s IP matches the specified acl, that route block’s plugins are used. Otherwise, the next route block (or the default route .) is considered. This allows you to stack different DNS views, with more specific routes (like internal) taking precedence. The file plugin serves zones from local files, while forward sends queries to upstream DNS servers.

The cache plugin is crucial here. For internal clients, the internal route might cache external lookups that fall through to the forward directive. However, because the file plugin for db.internal.local is checked first, internal-specific records will always be served from the local zone file before any caching or forwarding occurs.

A common pitfall is not defining the acl correctly or placing the route directives in the wrong order, leading to external clients receiving internal IPs or vice versa. Ensure your acl definitions are comprehensive for your internal networks and that the route for your internal network appears before the default route ..

The log and errors plugins are invaluable for debugging split-horizon setups, allowing you to see which queries are hitting which routes and if any resolution errors are occurring.

The cache directive’s TTL (Time To Live) affects how long responses are stored. For split-horizon, it’s important to understand that the cache is generally per-zone and per-plugin. A cached record from an internal view won’t be served to an external client, and vice-versa, because they are processed by different plugin chains.

When you need to manage very large internal DNS zones, CoreDNS’s ability to load them from files via the file plugin is efficient. For extremely high-traffic scenarios, you might explore distributing CoreDNS instances across your internal network segments to minimize latency.

The next challenge you’ll likely face is securing these internal DNS records, perhaps by restricting which internal clients can query specific internal zones.

Want structured learning?

Take the full Coredns course →