Kubernetes' Default CoreDNS Configuration Is *Insecure*
CoreDNS is the DNS server that powers most (all?) Kubernetes distributions, ever since it was made the default in v1.13 in December 2018.
Chances are, you’ve never heard of or used its predecessor: kube-dns. Lucky for us, CoreDNS still has a config option to remind us. pods insecure is the default configuration option for most (again - all?) Kubernetes distributions, and was introduced as an option to provide “backward compatibility with kube-dns”. The simple fact that the option’s value is insecure should be enough to make us reconsider.
But alas… it is not. So, what does it do? And, what are the implications?
What does it do?
Let’s go to our favorite K8s playground, spin up a cluster, and quickly confirm we are working with the insecure configuration:
$ kubectl -n kube-system get cm coredns -oyaml
And indeed we are:
apiVersion: v1
kind: ConfigMap
data:
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 {
disable success cluster.local
disable denial cluster.local
}
loop
reload
loadbalance
}
As the documentation states, this option forces CoreDNS to “always return an A record with IP from request (without checking k8s)”. Basically, this gives us the option to mint arbitrary DNS A records. Let’s create a pod and have some fun:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: attacker
namespace: default
labels:
app: attacker
spec:
containers:
- name: attacker
image: ubuntu:noble
command: ["sleep", "3600"]
resources:
limits:
memory: "256Mi"
cpu: "250m"
EOF
kubectl exec attacker -it -- bash
Inside the pod:
$ apt update && apt install -y curl dnsutils
$ dig +short 169-254-169-254.default.pod.cluster.local
169.254.169.254
What are the implications?
As the CoreDNS documentation further states, “this option is vulnerable to abuse if used maliciously in conjunction with wildcard SSL certs”. While this is definitely an issue, I’d like to take a different route here.
Let’s assume we have Cilium installed in our cluster to handle network policies. Let’s further assume we have a network policy like so:
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: limit-to-local
namespace: default
spec:
endpointSelector:
matchLabels:
app: attacker
egress:
# Rule 1: Allow DNS resolution (Required for FQDN rules to work)
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
rules:
dns:
- matchPattern: "*"
# Rule 2: Allow internal traffic only, or...?
- toFQDNs:
- matchPattern: "**.cluster.local"
toPorts:
- ports:
- port: "443"
protocol: TCP
- port: "80"
protocol: TCP
One would assume that this policy prevents any outbound network traffic… and indeed it does:
$ kubectl exec attacker -it -- curl ifconfig.me
203.0.113.0
$ kubectl apply -f netpol.yaml
ciliumnetworkpolicy.cilium.io/limit-to-local created
$ kubectl exec attacker -it -- curl ifconfig.me
**cricket noises**
Let’s check which IP this is trying to connect to:
$ kubectl exec attacker -it -- curl -v ifconfig.me
* Host ifconfig.me:80 was resolved.
* IPv6: 2600:1901:0:b2bd::
* IPv4: 34.160.111.145
If we show Cilium that this IP belongs to *.cluster.local, it will generously let us pass:
$ kubectl exec attacker -it -- dig +short 34-160-111-145.default.pod.cluster.local
34.160.111.145
$ kubectl exec attacker -it -- curl ifconfig.me
203.0.113.0
What happened here?
Cilium keeps a list of all the IPs and FQDNs it has seen and will make policy decisions based on these values.
$ kubectl -n kube-system exec cilium-vvlqz -- cilium fqdn cache list
Endpoint Source FQDN TTL ExpirationTime IPs
198 connection 34-160-111-145.default.pod.cluster.local. 0 2026-05-18T20:45:11.234Z 34.160.111.145
198 connection ifconfig.me. 0 2026-05-18T20:45:11.234Z 34.160.111.145
I have reported this to the Cilium project, and they have improved their documentation to mitigate this issue.
cilium command in a cilium pod that shares the node with your attacker pod.Conclusion
All of this is to say: It has been 8 years since we made the switch from kube-dns to CoreDNS. I think it is time to also make the switch from the insecure compatibility flag to verified and make our cluster DNS a little bit more secure.
I have raised this issue during the SIG Security meeting in April, and raised an issue with the CoreDNS project. I hope that this blog post servers as one more reason to start changing the default to verified in every Kubernetes distribution.
If you are a K8s distribution maintainer, please reach out! I am happy to share my findings to aid in your migration.