DevOpsil
Kubernetes
92%
Needs Review

Kubernetes Network Policies: Implementing Zero-Trust Pod Communication

Muhammad HassanMuhammad Hassan6 min read

The Default Kubernetes Network Is a Flat Trust Model

Every pod in a Kubernetes cluster can talk to every other pod by default. Your payment API can reach your logging sidecar in a different namespace. A compromised node agent can probe every service endpoint in the cluster. There are no questions asked.

This flat model is convenient for development and genuinely difficult to work with at scale. Zero-trust networking in Kubernetes means inverting that default: nothing is allowed unless explicitly permitted. NetworkPolicies are the mechanism.

Before writing a single YAML file, understand the prerequisite: NetworkPolicies are only enforced by your CNI plugin. The Kubernetes API accepts them regardless. If you're running Flannel, those policies do nothing. You need Calico, Cilium, WeaveNet, or another policy-aware CNI.

# Check your CNI
kubectl get pods -n kube-system | grep -E 'calico|cilium|weave|canal'

# Verify policies are being enforced (create a test policy and check enforcement)
kubectl get networkpolicies --all-namespaces

Step 1: Deny All Traffic by Default

Start with a namespace-level default deny. Apply this to every production namespace:

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

An empty podSelector matches every pod in the namespace. Specifying both Ingress and Egress in policyTypes with no rules means both are denied.

Apply it and immediately verify nothing can communicate:

kubectl apply -f default-deny-all.yaml

# Test: try to curl between pods
kubectl exec -it debug-pod -n production -- curl --max-time 3 http://backend-service
# Expected: connection timeout — policy is working

Step 2: Allow DNS Resolution

The first thing that breaks after a default-deny is DNS. Your pods can't resolve service names. You need to explicitly permit UDP port 53 egress to kube-dns:

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

# Verify DNS works now
kubectl exec -it debug-pod -n production -- nslookup backend-service

Step 3: Define Service-to-Service Policies

Now allow only the traffic your application actually needs. Label your pods with meaningful labels — you'll use these in selectors.

# pod labels to set on your deployments
labels:
  app: frontend
  tier: web
  version: v2

Allow frontend to reach backend on port 8080:

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

Allow backend to reach the database on port 5432:

# backend-to-db.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-backend-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: backend
      ports:
        - protocol: TCP
          port: 5432

Allow Prometheus to scrape all pods on port 9090:

# allow-prometheus-scrape.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-prometheus-scrape
  namespace: production
spec:
  podSelector: {}     # All pods
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoring
          podSelector:
            matchLabels:
              app: prometheus
      ports:
        - protocol: TCP
          port: 9090

Cross-Namespace Policies

NetworkPolicies are namespace-scoped, but you can reference other namespaces. The namespaceSelector + podSelector combination (nested in the same from entry) requires BOTH selectors to match. If you put them in separate list items, it's an OR.

# BOTH namespace AND pod label must match (AND logic)
from:
  - namespaceSelector:
      matchLabels:
        env: production
    podSelector:
      matchLabels:
        app: api-gateway

# Either namespace OR pod label matches (OR logic — usually not what you want)
from:
  - namespaceSelector:
      matchLabels:
        env: production
  - podSelector:
      matchLabels:
        app: api-gateway

This AND vs OR distinction is one of the most common policy mistakes. When in doubt, test explicitly.

Policy Reference Table

ScenarioPolicy TypeKey Selectors
Allow ingress from same namespaceIngresspodSelector only
Allow ingress from specific namespaceIngressnamespaceSelector + podSelector
Allow all egress to internetEgressEmpty to block (allows all)
Allow specific external IPEgressipBlock.cidr
Deny all ingress and egressIngress + EgressEmpty podSelector, no rules
Allow health check from kubeletIngressipBlock.cidr = node CIDR

Allowing External Traffic

For pods that need to reach external services (APIs, S3, package registries):

# allow-external-https.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-external-https
  namespace: production
spec:
  podSelector:
    matchLabels:
      needs-internet: "true"
  policyTypes:
    - Egress
  egress:
    - ports:
        - protocol: TCP
          port: 443
    - ports:
        - protocol: TCP
          port: 80

For specific CIDR ranges (e.g., your on-prem or a third-party API):

egress:
  - to:
      - ipBlock:
          cidr: 10.100.0.0/16
    ports:
      - protocol: TCP
        port: 443
  - to:
      - ipBlock:
          cidr: 0.0.0.0/0
          except:
            - 10.0.0.0/8
            - 172.16.0.0/12
            - 192.168.0.0/16
    ports:
      - protocol: TCP
        port: 443

Validating Your Policies

Don't assume policies work. Test them:

# Deploy a test pod with network tools
kubectl run nettest --image=nicolaka/netshoot -it --rm -n production -- bash

# From inside nettest, verify these should FAIL (default-deny):
curl --max-time 3 http://some-service-you-shouldnt-reach:8080

# Verify these should SUCCEED:
nslookup kubernetes.default
curl --max-time 3 http://backend-service:8080

With Cilium, you get additional tooling:

# Cilium network policy verdicts in real time
cilium monitor --type drop

# Hubble UI shows traffic flows and policy decisions visually
hubble observe --namespace production --verdict DROPPED

Common Mistakes

Forgetting egress policies: Most guides only show ingress. If you declare policyTypes: [Ingress, Egress], you must also write egress rules or all egress is blocked. A pod that can receive traffic but can't send responses is useless.

Not labeling namespaces: namespaceSelector matches namespace labels, not names. Add labels to namespaces: kubectl label namespace monitoring kubernetes.io/metadata.name=monitoring. The kubernetes.io/metadata.name label is automatically set since Kubernetes 1.21.

Testing in the wrong namespace: A policy in production doesn't affect pods in staging even if the services have the same names.

Zero-trust networking is not a single policy — it's a discipline. Start with default-deny, add policies as you document your service dependencies, and treat any podSelector: {} with Egress to all destinations as a debt that needs to be paid down.

Share:

Was this article helpful?

Muhammad Hassan
Muhammad Hassan

Network & Traffic Engineer

Packets don't lie. I design and troubleshoot the network layer that everything else depends on — Nginx, Envoy, HAProxy, DNS, CDNs, and everything in between. If it touches a socket, it's my problem.

Related Articles

More in Kubernetes

View all →

Discussion