Fractal Techware

Guides /

Kubernetes default-deny NetworkPolicy that still allows DNS

Manifests validated with kubeconform -strict against the Kubernetes 1.35 and 1.37 schemas. The connectivity checks below need a CNI that enforces NetworkPolicy; run them in your own cluster.

By default, every pod in a Kubernetes cluster can talk to every other pod and to the internet. A compromised pod in one namespace can reach your database in another. The standard fix is default deny: block all traffic in a namespace, then allow only the paths you need.

The first thing that breaks is almost always DNS. Egress is blocked, so pods cannot reach CoreDNS, and every connection fails with could not resolve host, including the ones you meant to allow. Here is the pair of policies that avoids that.

The policies

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: shop
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: shop
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - {protocol: UDP, port: 53}
        - {protocol: TCP, port: 53}

How it works

NetworkPolicies are additive allow lists. As soon as any policy selects a pod for a direction (Ingress or Egress), that direction becomes deny-by-default for the pod, and only traffic allowed by some policy passes. There is no “deny” rule, and policy order does not matter.

podSelector: {} selects every pod in the namespace, including pods created later. policyTypes: [Ingress, Egress] with no rules means nothing is allowed in either direction.

The DNS rule has one to entry with both selectors. namespaceSelector and podSelector are in the same list item (no - before podSelector), so the rule means “pods labelled k8s-app: kube-dns in kube-system”. If you add a dash before podSelector, it becomes two separate items: “any pod in kube-system or any pod labelled kube-dns in this namespace”. That is a very common and much broader mistake.

kubernetes.io/metadata.name is set automatically on every namespace (Kubernetes 1.22+), so you do not need to label kube-system yourself.

Both UDP and TCP 53. DNS normally uses UDP, but falls back to TCP for large responses. Without TCP you get intermittent failures that are hard to reproduce.

Allowing real traffic

Now add one policy per real path. For example, allowing the ingress controller to reach the web pods on port 8080:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-to-web
  namespace: shop
spec:
  podSelector:
    matchLabels:
      app: web
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
      ports:
        - {protocol: TCP, port: 8080}

Remember that the port is the container port the pod listens on, not the Service port. NetworkPolicy is evaluated after Service translation.

Traffic between two pods needs to be allowed on both sides when both namespaces use default deny: egress from the client and ingress to the server.

Validate the YAML

kubeconform -strict -summary -kubernetes-version 1.35.0 netpol.yaml allow-web.yaml
# Summary: 3 resources found in 2 files - Valid: 3, Invalid: 0, Errors: 0, Skipped: 0

-strict rejects unknown fields, which catches indentation mistakes that would otherwise silently produce a different policy.

Test that it actually works

Schema validation does not tell you whether traffic is blocked. Test in the cluster after applying:

kubectl apply -f netpol.yaml

# 1. DNS still resolves (expect an address)
kubectl -n shop run dnstest --rm -it --restart=Never --image=busybox:1.37 -- \
  nslookup kubernetes.default.svc.cluster.local

# 2. Egress to the internet is blocked (expect a timeout)
kubectl -n shop run egresstest --rm -it --restart=Never --image=busybox:1.37 -- \
  wget -qO- -T 5 http://example.com

# 3. Ingress from another namespace is blocked (expect a timeout)
kubectl -n shop run target --image=nginxinc/nginx-unprivileged:1.31-alpine --port=8080
kubectl -n shop wait --for=condition=Ready pod/target
TARGET_IP=$(kubectl -n shop get pod target -o jsonpath='{.status.podIP}')
kubectl -n default run probe --rm -it --restart=Never --image=busybox:1.37 -- \
  wget -qO- -T 5 "http://$TARGET_IP:8080"
kubectl -n shop delete pod target

If check 2 or 3 succeeds, your CNI does not enforce NetworkPolicy. The API server happily stores policies that nothing enforces. Flannel does not enforce them. Neither does kindnet in older kind releases, and some managed clusters need a network-policy add-on enabled. Calico, Cilium and Antrea do enforce them.

If check 1 fails, see the pitfalls below.

Pitfalls

Next steps