CoreDNS as Authoritative DNS in Kubernetes: Corefile and Zones
Published: 2026-06-16
CoreDNS is the only DNS server in a Kubernetes cluster by default. It is authoritative for the cluster.local zone — it answers definitively without touching upstream. Everything outside that zone goes to forwarding. This post covers how CoreDNS becomes authoritative, how to add custom zones, and what to look at when DNS stops responding.
How CoreDNS lands in the cluster
When a cluster is installed (k0s, k3s, kubeadm), CoreDNS is deployed as a Deployment in kube-system. Two pods, one ConfigMap holding the Corefile, one Service with a ClusterIP — that IP is written into /etc/resolv.conf of every pod.
bashkubectl get pods -A | grep coredns
# kube-system coredns-7f9fb5c85d-4btrq 1/1 Running 0 6d
# kube-system coredns-7f9fb5c85d-9kxrp 1/1 Running 0 6d
kubectl get svc -n kube-system kube-dns
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 6d
Inside any pod:
bashcat /etc/resolv.conf
# nameserver 10.96.0.10
# search default.svc.cluster.local svc.cluster.local cluster.local
# options ndots:5
ndots:5 means: if a name has fewer than five dots, search suffixes from the search list are appended in order. That's why redis resolves to redis.default.svc.cluster.local — CoreDNS answers authoritatively.
Corefile: plugin structure
All CoreDNS configuration is a single Corefile, stored in a ConfigMap:
bashkubectl get configmap coredns -n kube-system -o yaml
Standard Corefile from k0s/kubeadm:
corefile.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
Each zone { plugins } block is a server. Plugin execution order within a block is fixed at CoreDNS compile time — not by the order of lines in the file. Lines in the Corefile only configure plugins, not their invocation order.
The kubernetes plugin — the authoritative core
The kubernetes plugin owns the cluster.local zone. It reads objects from the Kubernetes API via an informer cache and returns answers without hitting upstream.
corefilekubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
| Parameter | Effect |
|---|---|
pods insecure |
Creates A records for pods as 1-2-3-4.namespace.pod.cluster.local without verification |
pods verified |
Same, but verifies the IP actually belongs to the pod — guards against DNS rebinding |
fallthrough in-addr.arpa ip6.arpa |
PTR queries the plugin can't handle pass to the next plugin |
ttl 30 |
Response TTL in seconds |
What this plugin resolves authoritatively:
# Service
redis.default.svc.cluster.local → ClusterIP
redis.default.svc.cluster.local (SRV) → port + ClusterIP
# Headless Service
redis.default.svc.cluster.local → IPs of all endpoints
# Pod (pods insecure)
10-244-1-5.default.pod.cluster.local → 10.244.1.5
# ExternalName Service
ext.default.svc.cluster.local → CNAME → external.host
Adding a custom authoritative zone
Option 1: file plugin (static zone file)
Good for small zones that change infrequently. The zone file goes into a ConfigMap and is mounted into the CoreDNS pod.
ConfigMap with zone file:
yamlapiVersion: v1
kind: ConfigMap
metadata:
name: coredns-custom-zones
namespace: kube-system
data:
internal.example.com.db: |
$ORIGIN internal.example.com.
$TTL 300
@ IN SOA ns1.internal.example.com. admin.internal.example.com. (
2026061601 ; serial
3600 ; refresh
900 ; retry
604800 ; expire
300 ) ; minimum TTL
@ IN NS ns1.internal.example.com.
ns1 IN A 10.96.0.10
api IN A 10.244.1.100
db IN A 10.244.2.50
* IN A 10.244.3.1
Patch CoreDNS ConfigMap — add the zone block:
yamlapiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
internal.example.com:53 {
file /etc/coredns/zones/internal.example.com.db
log
errors
}
.:53 {
errors
health {
lameduck 5s
}
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
Mount the zone file into the CoreDNS Deployment:
bashkubectl edit deployment coredns -n kube-system
Add to spec.template.spec:
yamlvolumes:
- name: custom-zones
configMap:
name: coredns-custom-zones
containers:
- name: coredns
volumeMounts:
- name: custom-zones
mountPath: /etc/coredns/zones
readOnly: true
The reload plugin watches the Corefile and reloads it automatically on ConfigMap changes — no restart needed. It does not watch zone files: after updating a zone file ConfigMap, run kubectl rollout restart deployment/coredns -n kube-system.
Option 2: stub zone — forward a specific zone to an external server
If a zone is served by an external DNS (corporate AD, internal bind9), configure a stub:
corefile.:53 {
errors
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
forward corp.example.com 192.168.1.53 192.168.1.54 {
policy round_robin
health_check 5s
}
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
reload
}
Queries for *.corp.example.com go to 192.168.1.53/192.168.1.54. Everything else goes to the system resolver. The order of forward blocks matters: CoreDNS matches the first fitting one.
Verification
bash# Launch a debug pod
kubectl run dnstest --image=busybox:1.36 --rm -it --restart=Never -- sh
# Inside the pod:
nslookup redis.default.svc.cluster.local
nslookup api.internal.example.com
nslookup google.com
# Check what Corefile CoreDNS is actually running
kubectl exec -n kube-system $(kubectl get pod -n kube-system -l k8s-app=kube-dns -o name | head -1) \
-- cat /etc/coredns/Corefile
# DNS trace from a pod
nslookup -type=A -debug redis.default.svc.cluster.local 10.96.0.10
CoreDNS Prometheus metrics (if scraping is set up):
promql# Requests per second by zone
rate(coredns_dns_requests_total[5m])
# SERVFAIL errors
rate(coredns_dns_responses_total{rcode="SERVFAIL"}[5m])
# p99 latency
histogram_quantile(0.99, rate(coredns_dns_request_duration_seconds_bucket[5m]))
What can go wrong
CoreDNS returns NXDOMAIN for a valid service
bash# Check the service exists
kubectl get svc redis -n default
# Check endpoints
kubectl get endpoints redis -n default
# If endpoints are empty — the problem is not DNS, it's selector/pod labels
kubectl describe endpoints redis -n default
CoreDNS only returns an A record if the Service has at least one Ready endpoint (headless) or a ClusterIP (regular). A Service without pods but with a ClusterIP resolves fine — traffic just has nowhere to go.
Loop detected — CoreDNS in CrashLoopBackOff
bashkubectl logs -n kube-system -l k8s-app=kube-dns | grep -i loop
# [ERROR] plugin/loop: Loop (127.0.0.1:53 -> :53) detected
The loop plugin found that /etc/resolv.conf on the node points to 127.0.0.1 (systemd-resolved or dnsmasq). CoreDNS forwards queries back to itself.
Fix — specify upstream explicitly instead of /etc/resolv.conf:
corefileforward . 8.8.8.8 8.8.4.4 {
max_concurrent 1000
}
Or configure /etc/resolv.conf on the node to not point at localhost.
Slow DNS — every query takes 5 seconds
Symptom: ndots:5 causes the full search suffix list to be tried for external names. A query for google.com becomes 6 queries:
google.com.default.svc.cluster.local → NXDOMAIN
google.com.svc.cluster.local → NXDOMAIN
google.com.cluster.local → NXDOMAIN
google.com. → answer
Fix — use a trailing dot (FQDN) for external names in the application, or lower ndots in the Pod spec:
yamlspec:
dnsConfig:
options:
- name: ndots
value: "2"
Zone file did not reload after ConfigMap update
The reload plugin only watches the Corefile, not zone files. After updating the ConfigMap containing the zone file:
bashkubectl rollout restart deployment/coredns -n kube-system
kubectl rollout status deployment/coredns -n kube-system
Summary
- CoreDNS is authoritative for
cluster.localvia thekubernetesplugin — answers from the API informer cache, no upstream involved - Custom zones are added via the
fileplugin (static zone file in a ConfigMap) orforward(stub to an external DNS) - The
reloadplugin auto-reloads the Corefile; zone files only reload after a pod restart - For debugging:
kubectl run dnstest,nslookup -debug, metrics on:9153 - ndots:5 is the cause of slow DNS for external names — fix with
dnsConfig.optionsin the Pod spec