CoursesKubernetes administrationNetwork troubleshooting

Network troubleshooting

DNS, kube-proxy, and CNI checks in order.

Advanced10 min · lesson 65 of 65
In plain terms
A network problem is a dropped call. Check the phone book (DNS), that someone’s actually on the line (endpoints), the switchboard wiring (kube-proxy), and whether a new “do not connect” rule (a NetworkPolicy) quietly blocked it.

"The database is down." That's what the alert said. Ten services throwing unknown host: db.prod, pages firing, someone already spinning up an incident channel. But the database pods are up. A pod is the smallest unit Kubernetes runs, one or more containers that share a single network address, and these ones are healthy, answering connections from a debug pod sitting right next to them. Nothing is actually down. This is the shape of almost every Kubernetes network incident. The symptom screams "everything is broken," and the cause is one quiet layer.

Think of one pod reaching another as a letter moving through a giant building's mailroom. It has to clear five desks before it arrives. First it needs the address: DNS, the Domain Name System, is the cluster's phone book, and it turns the name db.prod into an IP address. Then someone has to be sitting at that desk, because a Service, the stable front door for a group of pods, needs ready pods behind it, called its endpoints. Then the building's internal routing has to carry the envelope, and that job is kube-proxy, which programs the kernel so a Service's virtual IP forwards to a real pod. Then security at the door has to wave it through, because a NetworkPolicy can block it. And under all of it, the corridors have to exist at all: the CNI, or Container Network Interface, is the plugin (Calico, Cilium, and friends) that hands each pod its IP address and moves packets between machines. Check the five desks in the order they actually fail, most common first, and stop at the first "no."

Start with DNS, because it lies to you

DNS breaks more often than anything else down here, and when it breaks the whole cluster looks like it caught fire, because suddenly every service can't find any other service. So you check it first. And you check it from inside an affected pod, not from your laptop and not by logging into the node, because the pod is the one whose view of the world is broken. Its own DNS config, its own network namespace. Only the pod sees what the pod sees.

resolve the Service name from inside the pod
kubectl exec -it deploy/web -n prod -- nslookup db.prod
what you see when DNS is blocked
;; connection timed out; no servers could be reached
command terminated with exit code 1

Read the failure, don't just log that it failed. A timeout means the query never reached CoreDNS, the DNS server that itself runs as a few pods in the kube-system namespace. Never reaching the server is a routing or firewall problem, not a naming problem. Compare that to NXDOMAIN, which means the query did reach CoreDNS and the name genuinely doesn't exist, usually a typo or the wrong namespace. Two different failures, two different fixes. A timeout that shows up right after someone shipped a new NetworkPolicy points almost straight at the cause.

The one-rule outage

A NetworkPolicy is a firewall rule for pods, and it's enforced by the CNI plugin, not by the API server. That surprises people. The classic trap is the "default-deny" pattern. You add a policy that blocks all egress, meaning outbound traffic, so your pods can only reach what you explicitly allow. Good instinct. But DNS lookups are egress too. If your allow-list forgets to permit port 53 to kube-system, every pod the policy selects goes deaf. It can't resolve a single name, so every dependency it has looks dead at once. One forgotten rule, a namespace-wide "outage."

inspect the policy that landed
kubectl describe networkpolicy default-deny-egress -n prod
egress is denied and nothing carves out DNS
Name: default-deny-egress
Namespace: prod
Spec:
PodSelector: <none> (Allowing the specific traffic to all pods in this namespace)
Allowing egress traffic:
<none> (Selected pods are isolated for egress connectivity)
Policy Types: Egress

There it is. Egress set to <none> for every pod in the namespace, and no exception carved out for DNS. The fix doesn't touch the deny at all. NetworkPolicies are additive, so you leave the block in place and stack a second policy on top that allows port 53 to kube-system. This one finds kube-system by its built-in namespace label, the kubernetes.io/metadata.name label that Kubernetes stamps on every namespace automatically.

allow-dns-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: prod
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
apply it
$ kubectl apply -f allow-dns-egress.yaml
networkpolicy.networking.k8s.io/allow-dns-egress created
resolution comes back the moment the rule lands
kubectl exec -it deploy/web -n prod -- nslookup db.prod
now it resolves to the Service ClusterIP
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: db.prod.svc.cluster.local
Address: 10.96.140.22
A "default-deny" that forgets DNS looks like a total outage
This is the most common self-inflicted network wound in Kubernetes. You add a default-deny egress policy to lock a namespace down, and it quietly blocks those same pods from reaching DNS on port 53 as well. Now nothing can resolve a name, every dependency looks dead, and you get paged like the cluster fell over. It's one missing rule. Before you restart CoreDNS or blame the CNI, check whether a recent policy blocked egress to kube-system on port 53, then add an allow-DNS rule that stacks on top of the deny. And test resolution from an affected pod, never from your laptop, because only the pod sees the policy.

When it isn't DNS

Say the name resolves fine and the app still can't connect. Next desk: is anyone actually answering? A Service points at pods through a label selector, a simple "match everything tagged app=db" filter. Only ready pods get listed as endpoints, the ones passing their readiness probe. That probe is the health check Kubernetes uses to decide whether a pod is allowed to receive traffic yet. No endpoints means no target, and an empty target reads like a network fault but isn't one.

check the Service actually has backends
kubectl get endpoints db -n prod
empty means nobody is ready to serve
NAME ENDPOINTS AGE
db <none> 15m

That <none> sends you to one of two places. Either the Service's selector doesn't match any pod's labels (a typo of app=db versus app=database will do it), or the pods are running but failing readiness, so Kubernetes is deliberately holding them out of rotation. Under the hood this list now lives in EndpointSlices, and kubectl get endpointslices -l kubernetes.io/service-name=db -n prod shows the same picture with more detail. If endpoints do exist, hit the ClusterIP and port directly. That exercises kube-proxy, the component that programs iptables or IPVS rules on every node so a virtual Service IP actually forwards to a live pod.

test the kube-proxy path to a backend
kubectl exec -it deploy/web -n prod -- nc -zv db.prod 5432
the ClusterIP forwarded to a real pod
Connection to db.prod (10.96.140.22) 5432 port [tcp/postgresql] succeeded!

"Succeeded" means kube-proxy did its job and the packet reached a pod. If that same call hangs while endpoints clearly exist, confirm kube-proxy is even running on the nodes with kubectl get pods -n kube-system -l k8s-app=kube-proxy. Only when DNS resolves, endpoints exist, the ClusterIP answers, and no policy is blocking do you finally suspect the CNI. Its pods run as a DaemonSet, which just means Kubernetes keeps exactly one copy on every node. If Calico or Cilium is crash-looping on a node, every pod on that node loses networking at the same time, and that's a genuine outage instead of a one-rule slip. Walking the desks in order is the whole trick. It's what keeps you from reinstalling the CNI at 3 a.m. when the real problem was a forgotten port 53.

Network troubleshooting drill
App can't reach db.prod
work outward, stop at the first "no"
DNS resolves?
No: CoreDNS or DNS egress
default-deny that forgot port 53 to kube-system
Endpoints exist?
No: selector or readiness
<none> = no ready, label-matched pod
ClusterIP:port answers?
No: kube-proxy / kernel rules
iptables or IPVS not programmed, or kube-proxy down
NetworkPolicy allows it?
No: missing ingress/egress rule
the CNI is correctly enforcing a block
all four pass?
Yes: suspect the CNI
Calico/Cilium DaemonSet crash-looping on a node
Check DNS, then endpoints, then kube-proxy, then NetworkPolicy, then the CNI, in that order of likelihood. Stop at the first check that fails; that's your cause.

Working pod IP and failing ClusterIP points at Services or kube-proxy. Working ClusterIP and failing name points at DNS.

NetworkPolicy denies look like timeouts, not polite RSTs. Check policies early in locked namespaces.

Node-local problems affect only some clients. Map which worker your client pod landed on.

Try this

From a client pod, nslookup the Service, curl the ClusterIP, then curl a pod IP directly. The layer that fails first is where you dig.

terminal
$ kubectl exec -it deploy/web -n prod -- nslookup db.prod
$ kubectl describe networkpolicy default-deny-egress -n prod
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
$ metadata:
name: allow-dns-egress
namespace: prod
$ spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
$ kubectl apply -f allow-dns-egress.yaml
networkpolicy.networking.k8s.io/allow-dns-egress created
$ kubectl exec -it deploy/web -n prod -- nslookup db.prod
$ kubectl get endpoints db -n prod

Takeaway

Network troubleshooting is DNS, then Service/endpoints/kube-proxy, then CNI. Order beats intuition.

Quick check
01Right after a default-deny egress NetworkPolicy rollout, pods across a namespace start failing with "unknown host," and nslookup from an affected pod times out. The fastest fix is to…
Incorrect — CoreDNS is fine; the query never reaches it because egress to port 53 is blocked.
Correct — the default-deny forgot DNS, and an additive allow-DNS rule restores resolution immediately.
Incorrect — The Service type is irrelevant when name resolution itself is blocked.
Incorrect — The CNI is enforcing your policy correctly; the policy is the problem, not the plugin.
02Running nslookup from inside an affected pod can fail two ways: 'connection timed out; no servers could be reached', or NXDOMAIN. What's the difference?
Incorrect — only one implies a delivery problem; the two messages describe genuinely different failures.
Correct — a timeout is a delivery failure to fix at the network or policy layer, while NXDOMAIN is a naming failure like a typo or the wrong namespace.
Incorrect — that's backwards; the timeout is the unreachable case and NXDOMAIN is the name-doesn't-exist case.
Incorrect — both are DNS-resolution outcomes and have nothing to do with readiness probes or kube-proxy.
03The name db.prod resolves fine to a ClusterIP, but the app still can't connect. kubectl get endpoints db -n prod shows <none>, and the db pods are Running. What are the two things to check?
Incorrect — a dead CNI is the last suspect and wouldn't show as empty endpoints; you only reach it after these earlier desks pass.
Incorrect — kube-proxy programs routing from the endpoint list, but with an empty list there's nothing for it to program; the gap is upstream of it.
Correct — an empty endpoints list next to Running pods means a label/selector mismatch or pods that aren't passing their readiness probe.
Incorrect — the name already resolved correctly to the ClusterIP, so DNS did its job; the break is behind the Service.

Related