Kubernetes Network Policies — Isolate Your Workloads Before It’s Too Late

Kubernetes Network Policy diagram showing isolated pods with controlled ingress and egress traffic in a production cluster

By default, every pod in your Kubernetes cluster can talk to every other pod. No restrictions. No firewall. If your frontend pod can freely open a TCP connection to your database pod, so can any other compromised workload sitting in the same cluster.

That’s not a theoretical risk — it’s the blast radius problem. A single exploited pod shouldn’t become a pivot point into your entire cluster. Network Policies are how you stop that.

This guide covers Kubernetes Network Policies from scratch: what they are, how to write them, and a set of real-world rules you can deploy today.


What Is a Kubernetes Network Policy?

A NetworkPolicy is a Kubernetes resource that controls which pods can communicate with which other pods — and which external endpoints pods can reach. Think of it as a firewall rule set, but expressed as YAML and enforced at the pod level.

Without any NetworkPolicy objects in your cluster, all pod-to-pod traffic is allowed in every direction. The moment you create a NetworkPolicy that selects a pod, that pod becomes isolated — only the traffic explicitly permitted by the policy is allowed. Everything else is dropped.

Important: NetworkPolicy is enforced by your cluster’s CNI (Container Network Interface) plugin, not by Kubernetes core. You need a CNI that supports it. The major ones that do: Calico, Cilium, Weave Net, and Antrea. The popular flannel does NOT support NetworkPolicy by default. If you’re on a managed Kubernetes service — EKS, GKE, AKS, DigitalOcean Kubernetes — network policy support is either built in or available as an add-on.


The NetworkPolicy Object — Key Concepts

Before writing any YAML, understand three things:

1. Pod selection via labels Policies select pods using podSelector with label matchers. If podSelector is empty ({}), the policy applies to all pods in the namespace.

2. Policy types: Ingress and Egress

  • Ingress rules control incoming traffic to the selected pods
  • Egress rules control outgoing traffic from the selected pods
  • A policy can define both, or just one

3. Default-deny vs allow Creating a NetworkPolicy that selects a pod immediately isolates it — any traffic not explicitly permitted is dropped. There’s no “deny” rule to write; isolation is the default the moment a policy exists for that pod.


Your First NetworkPolicy — Default Deny All

The best starting point for any namespace is a blanket deny-all policy. This is your zero-trust baseline — nothing gets in or out unless you explicitly permit it.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}       # selects ALL pods in this namespace
  policyTypes:
    - Ingress
    - Egress

Deploy this first. Then add allow rules for the traffic you actually need. Start strict, open deliberately.


Allowing Specific Traffic — Real Examples

Example 1: Allow Frontend → Backend Only

Your frontend pods should reach your backend API. The backend should not be reachable from anything else in the cluster.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend          # This policy applies to backend pods
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend  # Only allow traffic FROM frontend pods
      ports:
        - protocol: TCP
          port: 8080

Now your backend only accepts connections from pods labelled app: frontend on port 8080. Everything else — including other backend pods, monitoring agents, or a compromised sidecar — gets dropped.

Example 2: Allow Backend → Database Only

The database should only accept connections from the backend. Not from the frontend. Not from a rogue debug pod someone spun up.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-backend-to-database
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: database          # Applies to database pods
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: backend   # Only backend pods can connect
      ports:
        - protocol: TCP
          port: 5432          # PostgreSQL port

Example 3: Allow DNS Egress (Critical — Don’t Forget This)

When you deploy a default-deny-all egress policy, you’ll immediately break DNS lookups because pods can no longer reach kube-dns. Every service discovery lookup fails. Add this alongside any egress deny policy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector: {}          # All pods need DNS
  policyTypes:
    - Egress
  egress:
    - ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

This is the most commonly missed step when teams first deploy network policies and then wonder why everything is broken.

Example 4: Cross-Namespace Traffic

Real clusters have multiple namespaces. Your monitoring namespace needs to scrape metrics from the production namespace. Here’s how to allow it:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-prometheus-scrape
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend           # Allow scraping of backend pods
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoring   # From the monitoring namespace
          podSelector:
            matchLabels:
              app: prometheus                           # Specifically Prometheus pods
      ports:
        - protocol: TCP
          port: 9090          # Metrics port

Note the indentation: namespaceSelector and podSelector are at the same level under from. This means BOTH conditions must match (the pod must be in the monitoring namespace AND be labelled app: prometheus). If you put them as separate list items under from, it becomes an OR — either condition satisfies the rule. This is a subtle but critical distinction.

# AND (both must match — same list item):
from:
  - namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: monitoring
    podSelector:
      matchLabels:
        app: prometheus

# OR (either satisfies the rule — separate list items):
from:
  - namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: monitoring
  - podSelector:
      matchLabels:
        app: prometheus

This AND vs OR behavior trips up almost everyone the first time.


A Complete Production-Grade Example

Here’s how you’d secure a typical three-tier app (frontend, backend, database) in a single namespace with tight network policies.

# 1. Default deny everything
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
# 2. Allow DNS for all pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
---
# 3. Allow ingress to frontend from internet (via ingress controller)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-to-frontend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: frontend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
---
# 4. Allow frontend to call backend API
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: frontend
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: backend
      ports:
        - protocol: TCP
          port: 8080
---
# 5. Allow backend to receive from frontend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-backend-ingress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
---
# 6. Allow backend to reach database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-backend-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: database
      ports:
        - protocol: TCP
          port: 5432
---
# 7. Allow database to receive from backend only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-db-ingress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: backend
      ports:
        - protocol: TCP
          port: 5432

Apply all of these and your three-tier app has a locked-down network topology: traffic can only flow frontend → backend → database, and nothing else.


How to Test and Debug Network Policies

Deploying a network policy blindly is a recipe for a production outage. Test before you enforce.

Test connectivity with a debug pod:

# Spin up a debug pod in the same namespace
kubectl run debug --image=nicolaka/netshoot -n production --rm -it -- bash

# From inside the pod, test connectivity to backend
curl http://backend-service:8080/health

# Test if the database port is reachable from a pod that shouldn't have access
nc -zv database-service 5432

Check which policies apply to a pod:

# List all NetworkPolicy objects in a namespace
kubectl get networkpolicy -n production

# Describe a specific policy
kubectl describe networkpolicy allow-backend-to-db -n production

Label verification — confirm your pods have the right labels:

kubectl get pods -n production --show-labels

If your policy selects app: backend but your pods are labelled app: api, the policy silently does nothing. Always verify labels match.

Cilium-specific — use hubble for live traffic visibility:

# If you're running Cilium with Hubble enabled
hubble observe --namespace production --follow

Hubble gives you a live feed of which connections are allowed and dropped — invaluable when debugging why something isn’t connecting.


Common Mistakes

1. Forgetting DNS egress after a deny-all The most common. Apply default-deny-all egress, everything breaks immediately. Always add the DNS allow rule in the same deployment.

2. AND vs OR confusion in from rules namespaceSelector and podSelector under the same list item = AND. Under separate list items = OR. Get this wrong and you either block too much or allow too much.

3. Using NetworkPolicy with a CNI that doesn’t support it If your CNI is flannel and you’re deploying NetworkPolicy objects, nothing happens. They get created successfully (Kubernetes accepts them) but they’re never enforced. You’ll think you’re secure and you’re not. Verify your CNI supports NetworkPolicy before relying on it.

4. Only writing ingress rules and forgetting egress Traffic flows both ways. A backend pod with an ingress rule that allows traffic from frontend — but no egress rule — can’t send its response back if you have a default-deny egress. You need both sides.

5. Not testing after applying Always run a connectivity test after deploying a new policy. A misconfigured policy can silently drop traffic with no error messages beyond a connection timeout.


Best Practices

  • Start with default-deny-all in every namespace. Add allow rules deliberately.
  • Use labels consistently — NetworkPolicy is only as good as your pod labelling discipline.
  • One policy per concern — don’t pile all your rules into one massive policy. Separate policies are easier to read, debug, and update.
  • Always include DNS egress when you use egress policies.
  • Namespace your policies — always set the namespace explicitly in the metadata. Don’t rely on kubectl apply -f context.
  • Keep policies in Git — treat them like any other infrastructure config. Review changes. Audit history.
  • Test in staging first — never deploy a network policy change directly to production without validating in a lower environment.

What’s Next

Network Policies give you network-level isolation, but they’re one layer of a complete Kubernetes security posture. The natural next steps are:

  • Pod Security Admission — control what containers can do at runtime (privilege escalation, running as root, host path mounts)
  • Secrets management — locking down how pods access credentials (External Secrets Operator, HashiCorp Vault)
  • Kubernetes RBAC — if you haven’t already locked down who can do what in the cluster, start with the RBAC guide

If you’re running a managed Kubernetes cluster and want to experiment with these policies without managing the control plane yourself, DigitalOcean Kubernetes supports Cilium-based network policies out of the box — worth a look for testing these patterns.

Network policies are one of the highest-impact security controls you can add to a running cluster with relatively low operational overhead. There’s no good reason to leave your pods in an open-by-default state once you understand how to lock them down.

Leave a Reply