ClusterIP resolves but times out: a NetworkPolicy whose namespaceSelector matches nothing
Namespace prod runs Deployment api (3 replicas, containerPort 8080, readiness probe GET /healthz). Service api is ClusterIP on port 80 with targetPort 8080; kubectl get endpoints api -n prod lists all three pod IPs and all pods are Ready. Ingress api with host api.example.com points at that Service. From a debug pod in namespace dev, getent hosts api.prod.svc.cluster.local returns 10.96.44.12, but curl -m 5 http://api.prod.svc.cluster.local hangs until the timeout and prints nothing. Requests through the ingress controller return HTTP 504 after 30 seconds. Two NetworkPolicies exist in prod: deny-all-ingress (podSelector {}, policyTypes: Ingress) and allow-ingress (podSelector app=api, ingress from a namespaceSelector matching name: ingress-nginx, port 8080). Namespace dev has a default-deny egress policy that allows UDP/TCP 53 to kube-system only.
1. Localise the fault. What do the successful DNS lookup and the timeout (rather than connection refused) tell you, and which checks confirm it?
Read the worked answer
Work from the outside in, and use the shape of the failure as evidence:
- DNS works because
dev's egress policy permits 53 to kube-system, and CoreDNS is not inprod. So name resolution tells you nothing about reachability ofapi. - The connection times out rather than being refused. A refusal means the packet reached a host stack with nothing listening - wrong
targetPort, or an explicit REJECT. A silent drop is what a NetworkPolicy denial looks like: the CNI programs a drop in the pod's ingress path, the SYN is discarded, and the client retries untilcurl -m 5gives up. - Endpoints are populated and pods are Ready, so this is not a selector typo, not a readiness failure, and not a kube-proxy/iptables problem - if kube-proxy were broken, the ClusterIP itself would not be programmed and you would typically see a refusal or no route.
So the packet is being DNATed to a real pod IP and dropped on arrival: the prod ingress policy. Confirm with:
kubectl exec -n ingress-nginx <controller-pod> -- curl -m 3 http://api.prod.svc.cluster.local- if this also hangs, the fault is not specific todev.kubectl get netpol -n prod -o yaml- note thatdeny-all-ingressmakes the namespace default-deny, and allow rules are additive, never subtractive.kubectl get ns ingress-nginx --show-labels- check whether thename: ingress-nginxlabel actually exists.
2. Given the evidence, why does the ingress controller get a 504, and what is the single root cause behind both symptoms?
Read the worked answer
Check the label. Since Kubernetes 1.21 every namespace automatically carries kubernetes.io/metadata.name: <name>, but it does not automatically carry name: <name>. A namespaceSelector matching name: ingress-nginx therefore selects an empty set, the allow-ingress rule grants nothing, and deny-all-ingress is the only rule that takes effect for both sources.
That explains both symptoms from one cause:
- The ingress controller's upstream connection to the pod IP:8080 is dropped, so nginx waits on its
proxy_read_timeout(30s here) and returns 504 Gateway Timeout. A 503 would have meant nginx had no endpoints at all - different fault, different fix. - The
devdebug pod hits the same drop and itscurlhangs.
Note that the traffic never reaches the application: api pods are Ready and serving /healthz, and there is nothing to see in their logs. That is a strong hint that the drop is in the network layer, not in the app. Verify the hypothesis cheaply before editing anything: temporarily add the correct label with kubectl label ns ingress-nginx name=ingress-nginx and re-run both curls - if both recover, the diagnosis is confirmed. Then remove the ad-hoc label and fix the policy properly, because relying on an out-of-band label is exactly how this broke in the first place.
3. Write the policy that restores access for both the ingress controller and `dev`, and justify each field.
Read the worked answer
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-ingress
namespace: prod
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: dev
ports:
- protocol: TCP
port: 8080
Justification:
port: 8080is the container port, not the Service port. NetworkPolicy is enforced at the pod's network interface, after kube-proxy has DNATed 80 → 8080. Writingport: 80is the most common way this policy gets built wrong and silently does nothing.- Two list items in
fromare ORed. A single item containing bothnamespaceSelectorandpodSelectorwould AND them, granting only pods that are in those namespaces and match the labels - not what we want. - The built-in
kubernetes.io/metadata.namelabel removes the need to label namespaces by hand and cannot drift. deny-all-ingressstays. Policies are additive; there is no need - and no way - to delete a deny rule from another policy object. Do not remove the default deny, it is doing useful work for every other pod inprod.- Leave
dev's egress policy alone; it already permits 53, and DNS proved it. Ifdevwere also default-deny on egress toprod, this policy would look correct and still fail - always check both directions.