CoreDNS can be a surprisingly powerful network proxy, not just a DNS server.

Let’s say you have a cluster running CoreDNS, and it’s handling all your internal service discovery. But now, you need it to resolve external hostnames, like google.com, by forwarding those requests to your existing upstream DNS servers (e.g., your company’s internal DNS or a public resolver like 8.8.8.8).

Here’s how you configure CoreDNS to do just that.

First, locate your CoreDNS configuration file. In a Kubernetes environment, this is typically a ConfigMap named coredns in the kube-system namespace. You can view it with kubectl get configmap coredns -n kube-system -o yaml.

The relevant part of the Corefile (the configuration for CoreDNS) looks like this:

.:53 {
    errors
    health {
       lameduck 5s
    }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
       pods insecure
       fallthrough in-addr.arpa ip6.arpa
    }
    prometheus :9153
    forward . 8.8.8.8 1.1.1.1 {
       max_concurrent 1000
    }
    cache 30
    loop
    reload
    loadbalance
}

Let’s break down the forward plugin.

The forward . 8.8.8.8 1.1.1.1 line is the core of the forwarding configuration.

  • .: This is the zone that the forward plugin will handle. The dot (.) represents the root zone, meaning all queries that aren’t explicitly handled by other plugins (like kubernetes for internal cluster names) will be forwarded.
  • 8.8.8.8 and 1.1.1.1: These are the IP addresses of your upstream DNS resolvers. CoreDNS will try to query these servers in order. If 8.8.8.8 is unresponsive, it will try 1.1.1.1. You can list multiple IPs here.

The max_concurrent 1000 option is a performance tuning parameter. It limits the number of simultaneous outgoing queries CoreDNS will send to the upstream servers. This prevents overwhelming your upstream resolvers and also helps CoreDNS manage its own resources. The default is usually sufficient, but for high-throughput environments, you might adjust this.

The health plugin with lameduck 5s is also crucial for robust forwarding. It periodically checks the health of the upstream servers. If an upstream server fails, lameduck prevents CoreDNS from immediately marking it as down. It gives the server a 5-second grace period to recover. This avoids flapping and unnecessary query retries.

The cache 30 plugin caches DNS responses for 30 seconds. This significantly reduces the load on both CoreDNS and your upstream resolvers, as frequently requested external domains will be answered directly from CoreDNS’s cache.

The reload plugin allows you to update the Corefile without restarting the CoreDNS pods. After you edit the ConfigMap, CoreDNS will pick up the changes automatically.

When a DNS query arrives at CoreDNS:

  1. It checks if the query is for cluster.local or other Kubernetes-specific zones. If so, the kubernetes plugin handles it.
  2. If not, it falls through to the forward plugin.
  3. The forward plugin sends the query to the configured upstream servers (8.8.8.8 and 1.1.1.1).
  4. The upstream server responds.
  5. CoreDNS receives the response, caches it (if cache is enabled), and returns it to the original client.

If you wanted to forward to your internal DNS servers at 10.10.0.53 and 10.10.0.54, you would change the forward line to:

forward . 10.10.0.53 10.10.0.54 {
   max_concurrent 1000
}

After modifying the ConfigMap, apply the changes: kubectl apply -f your-coredns-configmap.yaml. CoreDNS pods will automatically reload the configuration.

A common pitfall is forgetting the . in the forward directive. If you write forward 8.8.8.8 1.1.1.1, CoreDNS won’t know which zone this forwarding applies to, and it will likely be ignored. The . ensures it acts as the default resolver for all unhandled queries.

Another subtle point is the order of plugins. The kubernetes plugin must come before forward. This ensures that internal cluster DNS queries are resolved locally and not sent out to external resolvers. If forward were before kubernetes, a query for my-service.my-namespace.svc.cluster.local might incorrectly be sent to 8.8.8.8.

You can test this by exec-ing into a pod and using nslookup or dig. For example, to test resolving an external domain:

kubectl exec -it <your-pod-name> -- nslookup google.com

If you want to see the actual upstream servers CoreDNS is using, you can examine the resolv.conf file inside a pod. For example:

kubectl exec -it <your-pod-name> -- cat /etc/resolv.conf

This file is typically managed by the Kubernetes Kubelet and often points to the CoreDNS service IP. The magic of forwarding happens within CoreDNS itself.

The next thing you’ll likely want to configure is how CoreDNS handles DNSSEC validation for these forwarded queries.

Want structured learning?

Take the full Coredns course →