Kubernetes Network Policies are surprisingly not about allowing traffic, but about denying it by default and then selectively opening specific doors.

Let’s see how this plays out in a simple scenario. Imagine we have two deployments: frontend and backend, both running pods. By default, without any Network Policies, pods in these deployments can talk to each other freely across the cluster.

# Deployment for frontend
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80

---
# Deployment for backend
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: api
        image: nginxdemos/hello:plain-text
        ports:
        - containerPort: 80

If we exec into a frontend pod and try to curl a backend pod’s IP, it works:

# Inside a frontend pod
kubectl exec -it <frontend-pod-name> -- bash
curl <backend-pod-ip>

Now, let’s introduce a Network Policy to restrict this. The core idea is that a Network Policy is a set of rules that define what traffic is allowed to a set of pods (the "pods to be selected"). If a pod matches a Network Policy, only the traffic explicitly allowed by that policy will reach it. If no policy matches a pod, all traffic is allowed. Crucially, if multiple policies apply, the union of allowed traffic is permitted.

Here’s a Network Policy that denies all ingress traffic to backend pods by default:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-ingress-to-backend
  namespace: default # Assuming pods are in the default namespace
spec:
  podSelector:
    matchLabels:
      app: backend # This policy applies to pods with the label 'app: backend'
  policyTypes:
  - Ingress # This policy defines rules for incoming traffic
  # No 'ingress' rules are defined here, meaning all ingress is denied

After applying this policy, if we try to curl the backend pod from the frontend pod again, it will fail with a timeout or connection refused.

The real power comes when we selectively allow traffic. To let frontend pods talk to backend pods, we add an ingress rule to the same policy, targeting the backend pods:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend # Allow traffic ONLY from pods labeled 'app: frontend'
    ports:
    - protocol: TCP
      port: 80 # Allow traffic ONLY on TCP port 80

Now, frontend pods can reach backend pods on port 80. What about egress (outbound) traffic? By default, pods can talk to anywhere. To control egress, we add policyTypes: [Egress] and define egress rules.

Let’s say we want frontend pods to only be able to reach backend pods on port 80, and nowhere else. We can achieve this with two policies: one for ingress to backend, and one for egress from frontend.

First, the ingress policy for backend (as above):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend-ingress
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 80

Second, an egress policy for frontend:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-egress-to-backend
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: frontend
  policyTypes:
  - Egress # This policy defines rules for outgoing traffic
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: backend # Allow traffic ONLY to pods labeled 'app: backend'
    ports:
    - protocol: TCP
      port: 80 # Allow traffic ONLY on TCP port 80
  - to: # Allow egress to DNS (if your pods need to resolve hostnames)
    - namespaceSelector: {} # Any namespace
      podSelector:
        matchLabels:
          k8s-app: kube-dns # Standard label for kube-dns pods
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

With these two policies, frontend pods can initiate connections to backend pods on port 80, and backend pods can receive them. frontend pods can also resolve DNS. Any other outbound traffic from frontend (e.g., to external services, or other pods not matching app: backend) would be blocked.

To make Network Policies work, your Kubernetes cluster must have a network plugin (CNI) that implements them. Popular choices include Calico, Cilium, and Weave Net. If your CNI doesn’t support Network Policies, they will be ignored.

The podSelector and namespaceSelector fields are key to defining who can talk to whom. A podSelector: {} without any matchLabels in the from or to fields means "any pod" within the specified namespace (or any namespace if namespaceSelector: {} is also used). Similarly, namespaceSelector: {} means "any namespace."

One detail that trips many people up is how Network Policies are additive for allowed traffic but exclusive for denied traffic. If a pod is selected by any Network Policy, then all ingress traffic not explicitly allowed by any of those policies is denied. Similarly for egress. If a pod has no Network Policies selecting it, all traffic is allowed. This means to deny all traffic, you create a policy that selects the target pods but has no ingress or egress rules defined.

The next step you’ll likely encounter is managing more complex network topologies, like service meshes or multi-cluster communication, and how Network Policies interact with those.

Want structured learning?

Take the full Containers & Kubernetes course →