Server

Kubernetes Network Policies

By Prabath Thalangama· October 24, 2026· 3 min read
#kubernetes#networking#security

Introduction

By default, every pod can reach every other pod in the cluster, across namespaces. NetworkPolicy objects restrict that. They're namespaced, additive (union of all policies selecting a pod), and — critically — only enforced if your CNI supports them (Calico, Cilium, Antrea, Weave; not Flannel alone).

The model

  • A pod is non-isolated until some NetworkPolicy selects it. Then it becomes isolated for the direction(s) that policy specifies, and only traffic matching an allow rule gets through.
  • Rules are allow-only. Isolation + no matching rule = denied.
  • policyTypes: [Ingress], [Egress], or both.

Default deny (the baseline)

Apply this per namespace, then open holes:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: team-a
spec:
  podSelector: {}          # all pods in the namespace
  policyTypes: [Ingress, Egress]

Now nothing in team-a can send or receive — including DNS. So immediately add:

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

Allow specific traffic

# api pods accept ingress from web pods (same ns) and from the ingress controller
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-ingress
  namespace: team-a
spec:
  podSelector:
    matchLabels: { app: api }
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels: { app: web }
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: ingress-nginx }
      ports:
        - { protocol: TCP, port: 8080 }
# api pods may egress to the database namespace and to the internet on 443
  egress:
    - to:
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: data }
          podSelector:
            matchLabels: { app: postgres }
      ports: [{ protocol: TCP, port: 5432 }]
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except: [10.0.0.0/8, 169.254.169.254/32]   # no cluster-internal, no metadata
      ports: [{ protocol: TCP, port: 443 }]

Selector gotcha: namespaceSelector + podSelector in the same from entry (no - between them) means "pods matching X in namespaces matching Y". Separate list items (- namespaceSelector then - podSelector) means "OR".

Beyond standard NetworkPolicy

Standard policy is L3/L4 only, namespace-scoped, no ordering, no deny rules, no logging. For more, use your CNI's CRDs:

  • Cilium CiliumNetworkPolicy / CiliumClusterwideNetworkPolicy — L7 (HTTP paths/methods, DNS names, Kafka), cluster-wide, deny rules.
  • Calico GlobalNetworkPolicy — ordered, deny, logging, host endpoints.

Verification and troubleshooting

kubectl get networkpolicy -A
kubectl describe networkpolicy api-ingress -n team-a

# Test from inside a pod
kubectl run t --rm -it --image=nicolaka/netshoot -n team-a -- bash
  nc -zv api.team-a 8080          # should work from an allowed source
  nc -zv postgres.data 5432
  curl -m3 https://example.com
  nslookup kubernetes.default
  • Applied a default-deny and everything broke — DNS. Add the allow-dns egress policy. Also health probes: the kubelet probes from the node IP — ingress from the node CIDR may need allowing (some CNIs exempt probes, some don't).
  • Policy has no effect — the CNI doesn't enforce NetworkPolicy (plain Flannel), or it's not in "policy" mode. kubectl get pods -n kube-system for calico/cilium.
  • namespaceSelector not matching — pre-1.22 namespaces lacked the automatic kubernetes.io/metadata.name label; label them, or use your own.
  • Egress to a Service IP blocked despite allowing the pod — kube-proxy DNATs the ClusterIP to a pod IP; the policy is evaluated against the pod IP after DNAT, so allow the backend pods, not the Service. Some CNIs handle Service IPs directly.
  • Cross-namespace allow not working — both a namespaceSelector (to pick the namespace) AND the traffic must match; and the target pod's namespace is where the policy lives.
  • Can't reach the API server / node-local DNS — those may be on the host network; ipBlock the node CIDR or use the CNI's host-endpoint handling.
  • Intermittent drops after a deploy — new pods get IPs before the policy's ipBlock/selectors converge; brief. Persistent = a selector typo.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.