Skip to content
Codeloom
Kubernetes

Kubernetes Network Policies: Pod-to-Pod Security Guide

Lock down pod communication with Kubernetes NetworkPolicies. Learn ingress and egress rules, label selectors, and namespace isolation.

·6 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How NetworkPolicies control traffic between pods
  • Writing ingress and egress rules with label and namespace selectors
  • Default deny patterns and namespace isolation strategies

Prerequisites

  • Basic Kubernetes knowledge — see Pods and Services
  • Understanding of Kubernetes labels and namespaces

By default, every pod in a Kubernetes cluster can talk to every other pod. There are no firewalls, no access controls, nothing stopping a compromised frontend pod from connecting directly to your database. NetworkPolicies fix this by letting you define exactly which pods can communicate with which other pods.

How NetworkPolicies Work

A NetworkPolicy is a Kubernetes resource that selects pods using labels and defines allowed ingress (incoming) and egress (outgoing) traffic rules. Once you apply any NetworkPolicy that selects a pod, all traffic not explicitly allowed by a policy is denied for that pod.

Important: NetworkPolicies require a CNI plugin that supports them. Calico, Cilium, and Weave Net support NetworkPolicies. The default kubenet and some versions of Flannel do not. If your CNI does not support them, the policies are accepted by the API server but have no effect.

Default Deny All Traffic

The first step in securing a namespace is to deny all traffic by default, then allow specific flows:

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

The empty podSelector: {} matches all pods in the namespace. With no ingress or egress rules defined, all traffic is blocked. This includes DNS resolution, so pods cannot resolve service names.

Allow DNS so services can still resolve names:

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

Ingress Rules: Controlling Incoming Traffic

Allow the API to receive traffic only from the frontend:

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

This policy says: pods labeled app: api accept TCP traffic on port 3000 only from pods labeled app: frontend in the same namespace.

Egress Rules: Controlling Outgoing Traffic

Restrict the API to only connect to the database and Redis:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    # Allow connections to the database
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    # Allow connections to Redis
    - to:
        - podSelector:
            matchLabels:
              app: redis
      ports:
        - protocol: TCP
          port: 6379
    # Allow DNS
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53

Cross-Namespace Policies

To allow traffic from pods in a different namespace, combine namespaceSelector with podSelector:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-allow-api-from-staging
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              environment: staging
          podSelector:
            matchLabels:
              app: api
      ports:
        - protocol: TCP
          port: 5432

Note the indentation. When namespaceSelector and podSelector are at the same level under a single from entry, they act as AND — the pod must match both selectors. If they are separate entries in the from array, they act as OR.

# AND: pod must be in staging namespace AND have label app=api
- from:
    - namespaceSelector:
        matchLabels:
          environment: staging
      podSelector:
        matchLabels:
          app: api

# OR: any pod in staging namespace OR any pod with label app=api
- from:
    - namespaceSelector:
        matchLabels:
          environment: staging
    - podSelector:
        matchLabels:
          app: api

This is a common source of misconfiguration. Always double-check whether you want AND or OR logic.

CIDR-Based Rules

Allow egress to external services by IP range:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-allow-external
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    # Allow HTTPS to specific external APIs
    - to:
        - ipBlock:
            cidr: 203.0.113.0/24
      ports:
        - protocol: TCP
          port: 443
    # Allow all internal cluster traffic
    - to:
        - ipBlock:
            cidr: 10.0.0.0/8
            except:
              - 10.0.5.0/24

The except field excludes a subnet from the allowed range. Use this to allow broad cluster traffic while blocking specific sensitive subnets.

A Complete Three-Tier Example

Here is a full set of policies for a frontend, API, and database architecture:

# 1. Default deny everything
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: myapp
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
# 2. Allow DNS for all pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: myapp
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
---
# 3. Frontend: accept external traffic, connect to API
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-policy
  namespace: myapp
spec:
  podSelector:
    matchLabels:
      tier: frontend
  policyTypes: [Ingress, Egress]
  ingress:
    - ports:
        - protocol: TCP
          port: 80
  egress:
    - to:
        - podSelector:
            matchLabels:
              tier: api
      ports:
        - protocol: TCP
          port: 3000
---
# 4. API: accept from frontend, connect to database
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-policy
  namespace: myapp
spec:
  podSelector:
    matchLabels:
      tier: api
  policyTypes: [Ingress, Egress]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              tier: frontend
      ports:
        - protocol: TCP
          port: 3000
  egress:
    - to:
        - podSelector:
            matchLabels:
              tier: database
      ports:
        - protocol: TCP
          port: 5432
---
# 5. Database: accept from API only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: database-policy
  namespace: myapp
spec:
  podSelector:
    matchLabels:
      tier: database
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              tier: api
      ports:
        - protocol: TCP
          port: 5432

Testing Network Policies

Use a temporary pod to test connectivity:

# Test if frontend can reach the API
kubectl run test-frontend --rm -it --image=busybox \
  --labels="tier=frontend" -n myapp -- wget -qO- http://api:3000/health

# Test if frontend can reach the database (should fail)
kubectl run test-frontend --rm -it --image=busybox \
  --labels="tier=frontend" -n myapp -- wget -qO- --timeout=5 http://postgres:5432

If a connection that should work is blocked, check your label selectors, namespace labels, and port numbers.

Summary

NetworkPolicies are essential for production Kubernetes clusters. Start with a default deny policy in each namespace, then add specific allow rules for the traffic flows your application needs. Always remember to allow DNS egress, be careful with AND vs OR logic in selectors, and test your policies with temporary pods. The patterns here — default deny, three-tier isolation, cross-namespace controls — cover the majority of real-world use cases.