Kubernetes Security — Part 2

Kubernetes Network Policies — Locking Down Pod-to-Pod Traffic

Tisighe Livinstone

Tisighe Livinstone

18 April 2025·4 min read
Kubernetes Network Policies — Locking Down Pod-to-Pod Traffic

In a default Kubernetes cluster, every pod can talk to every other pod - across all namespaces, without any restriction. Your application pod can try to connect directly to your database, to your monitoring stack, to pods in completely unrelated namespaces. If a pod is compromised, the attacker gets the same network access the pod had.

Network Policies fix this. They let you define exactly which pods can talk to which, and block everything else. This is Part 2 of the security work we started with RBAC hardening. That locked down the Kubernetes API. This locks down the network.

What You Need

  • Install kubectl — configured against your cluster
  • A CNI that supports Network Policies: Calico, Cilium, or Weave. If on EKS, see below.
# Verify your CNI supports Network Policies
kubectl get pods -n kube-system | grep -E "calico|cilium|weave"

# Calico, Cilium, Weave: supported
# Flannel: NOT supported — you'll need to install Calico alongside it

# On EKS, enable Network Policy with Amazon VPC CNI (EKS 1.25+):
kubectl set env daemonset aws-node -n kube-system ENABLE_NETWORK_POLICY=true

In a default Kubernetes cluster, every pod can talk to every other pod — across all namespaces. If an application pod is compromised, an attacker can attempt connections to your database, your internal APIs, your monitoring systems. Network Policies restrict this.

This is Part 2 of the Kubernetes Security series. Part 1 covered RBAC hardening — restricting what Kubernetes API actions pods can perform. Network Policies restrict which pods can communicate with each other.

Step 1: Apply Default Deny

The first policy I apply to any production namespace blocks everything. Then I add back only what needs to work:

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

# Verify it's applied
kubectl get networkpolicies -n production
# NAME              POD-SELECTOR   AGE
# default-deny-all           5s

Important: after applying default-deny, your pods lose all connectivity immediately — including DNS. Apply the DNS allowance below right away.

Step 2: Allow DNS (Required for Everything)

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

Step 3: Allow Your App to Reach Its Database

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-app-to-postgres
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres        # This policy applies TO postgres pods
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: my-app      # Only my-app can connect to postgres
    ports:
    - protocol: TCP
      port: 5432

Step 4: Allow Ingress Controller to Reach Your App

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-to-app
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: my-app
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
      podSelector:
        matchLabels:
          app.kubernetes.io/name: ingress-nginx
    ports:
    - protocol: TCP
      port: 8080

Step 5: Allow App Egress to External Services

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-app-external-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: my-app
  policyTypes:
  - Egress
  egress:
  - ports:
    - protocol: TCP
      port: 443    # HTTPS to external APIs (Stripe, SendGrid, etc.)
  - to:
    - podSelector:
        matchLabels:
          app: postgres
    ports:
    - protocol: TCP
      port: 5432

Testing Your Policies

# Spawn a debug pod in the namespace
kubectl run test-pod   --image=busybox   --rm -it   --restart=Never   -n production   -- sh

# Inside test-pod — test what should fail
nc -zv postgres-service 5432
# nc: postgres-service (10.x.x.x:5432): Connection refused  ← correct, test-pod is not my-app

# Test DNS still works (should succeed — we allowed it)
nslookup kubernetes.default.svc.cluster.local
# Apply all policies at once
kubectl apply -f network-policies/

# Verify what policies exist
kubectl get networkpolicies -n production
# NAME                      POD-SELECTOR   AGE
# default-deny-all                   1m
# allow-dns                          30s
# allow-app-to-postgres     app=postgres   30s
# allow-ingress-to-app      app=my-app     30s
# allow-app-external-egress app=my-app     30s

The most common mistake when implementing Network Policies for the first time: applying default-deny without mapping all traffic paths first. Use Cilium Hubble or kubectl sniff to observe actual pod communications for a week before locking things down. That way you know what to allow before you block it.

Combined with the RBAC policies from Part 1, Network Policies give you defence in depth — even if a pod is compromised, it cannot freely communicate with the rest of your infrastructure.

Tisighe Livinstone

Tisighe Livinstone

Cloud & DevOps Engineer

Writing about real infrastructure challenges — Kubernetes, Terraform, GitOps, observability, and cloud security. Based on things I've actually built and broken in production.

Comments

All comments are reviewed before appearing.

Leave a Comment