Introduction to Kubernetes Network Policies: Implementing Zero-Trust Networking

By default, Kubernetes operates with a completely flat network model where any pod can communicate with any other pod across all namespaces without restrictions, a convenient setup for development and learning environments, but an extremely dangerous configuration for production clusters handling sensitive data or multi-tenant workloads. This unrestricted communication model means that if a single container is compromised through an application vulnerability, SQL injection, remote code execution exploit, or supply chain attack, the attacker immediately gains network access to potentially every other service in your entire cluster including databases storing customer personally identifiable information, internal APIs exposing business logic and data, payment processing services handling credit cards, authentication services managing user credentials, and backend systems that should never be accessible from frontend application tiers.

Without Network Policies implementing network segmentation and micro-segmentation, your Kubernetes cluster is effectively one large, flat security zone where the compromise of any single pod perhaps a vulnerable WordPress instance or an unpatched Node.js application with a known CVE can lead to complete cluster-wide compromise through lateral movement as attackers pivot from the initial foothold to progressively more sensitive systems, exfiltrating data, deploying cryptominers, or establishing persistent backdoors for long-term access.

Kubernetes Network Policies are the firewall rules for your cluster, providing Layer 3/4 network segmentation that controls which pods can communicate with which other pods based on label selectors, which namespaces can reach which other namespaces based on namespace selectors, and which external IP addresses or CIDR blocks pods are allowed to connect to for egress traffic filtering. Properly implemented Network Policies provide critical defense-in-depth security by implementing zero-trust networking principles where nothing can communicate unless explicitly allowed, limiting the blast radius of security incidents by containing compromised pods within their intended network segment, preventing lateral movement by blocking attackers from pivoting to other services after initial compromise, meeting compliance requirements for network segmentation required by PCI-DSS, HIPAA, and other regulatory frameworks, enabling multi-tenant cluster security by isolating different teams or customers from each other at the network layer, and providing defense against internal threats from malicious insiders or compromised service accounts that have valid authentication but shouldn't have network access to all resources.

This comprehensive technical guide teaches you everything about Kubernetes Network Policies from foundational concepts to advanced production patterns, covering: how Network Policies actually work under the hood with CNI plugin integration, which CNI plugins support Network Policies and which don't (critical Flannel doesn't support them!), implementing default deny-all policies as security foundation following zero-trust principles, creating explicit allow policies for legitimate traffic patterns while blocking everything else, understanding ingress versus egress policy types and when to use each, pod selectors using labels for targeting policies to specific applications, namespace selectors for cross-namespace communication control, IP block selectors for external traffic filtering, common Network Policy patterns solving real security requirements (frontend-to-backend, database isolation, egress restrictions), advanced techniques like combining selectors with AND/OR logic, testing Network Policies before enforcement to avoid breaking applications, troubleshooting connectivity issues caused by overly restrictive policies, and how to base policies on observed pod-to-pod traffic rather than guesswork, so they allow only legitimate traffic while blocking everything else, a data-driven approach that prevents both over-permissive policies that fail to provide security and over-restrictive policies that break application functionality.

By mastering Network Policies and implementing the patterns in this guide, you'll establish network-layer security that significantly hardens your Kubernetes infrastructure against attacks, reduces the blast radius of incidents, enables safe multi-tenancy, and meets compliance requirements while maintaining application functionality.

How Kubernetes Network Policies Work: Architecture and CNI Integration

The Fundamental Network Policy Model

Kubernetes Network Policies specify allowed network traffic, but Kubernetes itself doesn't enforce them; that's the job of your Container Network Interface (CNI) plugin. This separation of policy specification from enforcement is important to understand because it means Network Policies are declarative intent ("I want to allow traffic from frontend to backend on port 8080") but whether they actually work depends entirely on whether your CNI plugin supports and correctly implements Network Policy enforcement.

Key Concepts:

  • Ingress: Incoming traffic TO the selected pods. Controls what can connect to your application.
  • Egress: Outgoing traffic FROM the selected pods. Controls what your application can connect to.
  • Pod Selector: Determines which pods the policy applies to using label matching (app=backend, tier=database, etc.)
  • Policy Types: Ingress only, Egress only, or both. Specifying policyTypes: [Ingress] without any ingress rules = deny all ingress.
  • Namespace Scope: Network Policies are namespaced resources that affect only pods in the same namespace (unless using a namespaceSelector for cross-namespace rules).

CNI Plugin Support: Critical Prerequisite

Network Policies ONLY work if your CNI plugin supports them. This is a hard requirement without CNI support, Network Policy resources are accepted by the Kubernetes API but completely ignored (no enforcement), creating a dangerous false sense of security.

CNI Plugins WITH Network Policy Support:

  • Calico: Most popular choice specifically for Network Policy support. High-performance eBPF dataplane option. Supports advanced features like global network policies, egress gateways, and encryption. Used by the majority of production clusters requiring Network Policies.
  • Cilium: eBPF-based CNI with advanced observability via Hubble. Supports Layer 7 policies (HTTP method/path filtering), DNS-based policies, and service mesh integration. Best for teams wanting network visibility alongside enforcement.
  • Weave Net: Simple to deploy with automatic encryption. Good Network Policy support, but less performant than Calico/Cilium at very large scale. Note: the Weave Net project is archived and no longer maintained, so avoid it for new clusters.
  • Antrea: VMware-backed CNI with good Windows support. Solid Network Policy implementation.
  • kube-router: Lightweight CNI using standard Linux networking (iptables/ipvs). Basic but functional Network Policy support.

CNI Plugins WITHOUT Network Policy Support:

  • Flannel: Very popular for simplicity, but does NOT support Network Policies. If you need Network Policies, don't use Flannel alone (can combine with Calico for policy enforcement).
  • Host-local: Simple IPAM, no policy support
  • Bridge: Basic bridge networking, no policies

Verify Network Policy support in your cluster:

# The NetworkPolicy API exists on every cluster, whatever the CNI:
kubectl api-resources | grep networkpolicies
# ...so its presence does NOT prove policies are enforced.

# 1. Identify the CNI
kubectl get pods -n kube-system | grep -Ei 'calico|cilium|antrea|flannel|aws-node'

# 2. Test enforcement with a deny-all policy in a scratch namespace
kubectl create namespace netpol-test
kubectl run web --image=nginx --labels=app=web --port=80 -n netpol-test
kubectl expose pod web --port=80 -n netpol-test
kubectl apply -n netpol-test -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-ingress
spec:
  podSelector: {}
  policyTypes:
  - Ingress
EOF
kubectl run client --rm -it --restart=Never --image=busybox -n netpol-test -- wget -qO- -T 5 http://web

# A timeout means policies are enforced. The nginx welcome page means they are not:
# install or enable a policy-capable CNI (Calico, Cilium) before relying on them.
# Clean up the scratch namespace afterwards.

See what is exposed to the internet across AWS and EKS.

SquareOps Verity, in early access: read-only scans, a security posture score and findings ranked by real risk. Nothing changes in your accounts.

Request early access →

AWS EKS, Google GKE, Azure AKS support: All major managed Kubernetes services support Network Policies when configured with appropriate CNI (EKS with AWS VPC CNI + Calico, GKE with default CNI, AKS with Azure CNI or Calico).

Implementing Default Deny Network Policies: Security Foundation

Why Default Deny is Critical

The single most important security principle for Network Policies is default deny: start by blocking all traffic, then explicitly allow only legitimate communication patterns. This whitelist approach is vastly superior to the default allow with blacklist exceptions because you can't possibly enumerate all the bad traffic patterns to block attackers are creative and constantly evolving their techniques. But you CAN enumerate the legitimate traffic your applications need to function.

Default Deny: Deny All Ingress Traffic

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-ingress
  namespace: production
spec:
  podSelector: {}  # Empty selector = applies to ALL pods in namespace
  policyTypes:
  - Ingress
  # No ingress rules = deny all ingress

What this does: Blocks all incoming traffic to all pods in the production namespace. Pods cannot receive connections from other pods, from other namespaces, or from external sources. Services stop working until you add explicit allow policies.

Default Deny: Deny All Egress Traffic

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
  # No egress rules = deny all egress

What this does: Blocks all outgoing traffic from all pods. Pods cannot initiate connections to other pods, databases, external APIs, or even DNS. This will break everything until you add allow rules, which is exactly the point, it forces you to explicitly define what's allowed.

Critical Note: Applying these default deny policies will immediately break all services in the namespace. Only apply after you have allow policies ready for critical services, or apply in a test namespace first to validate your allow rules work correctly.

Common Network Policy Patterns for Production

Pattern 1: Allow Frontend to Backend Communication

Use case: Three-tier application: frontend web tier calls backend API tier, which calls database tier. Frontend should never directly access the database (violates tier separation).

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

What's allowed: Frontend pods (tier=frontend label) can connect to backend pods (tier=backend label) on TCP port 8080 only.

What's blocked: Frontend cannot connect to the database directly (tier=database pods not in allowed sources). Backend cannot connect to frontend (ingress-only, not bidirectional). Other namespaces cannot connect to the backend (no namespaceSelector). External traffic cannot reach the backend directly (must go through the ingress controller).

Pattern 2: Database Isolation (Backend Access Only)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-access-control
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
      tier: database
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          tier: backend  # Only backend tier can access database
          access-db: "true"  # Additional label for fine-grained control
    ports:
    - protocol: TCP
      port: 5432  # PostgreSQL port only

Security benefit: Even if the frontend is compromised, it cannot connect to the database and steal customer data. Defense-in-depth.

Pattern 3: Allow Cross-Namespace Monitoring

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-monitoring-scraping
  namespace: production
spec:
  podSelector:
    matchLabels:
      prometheus.io/scrape: "true"  # Pods exposing metrics
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: monitoring  # Prometheus pods in monitoring namespace
      podSelector:
        matchLabels:
          app: prometheus
    ports:
    - protocol: TCP
      port: 9090  # Metrics endpoint

Explanation: Allows Prometheus pods from the monitoring namespace to scrape metrics from production pods, while blocking all other cross-namespace traffic.

Pattern 4: Always Allow DNS (Critical!)

The #1 Network Policy Mistake: Blocking DNS and wondering why nothing works.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-access
  namespace: production
spec:
  podSelector: {}  # ALL pods in namespace
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          name: kube-system  # CoreDNS/kube-dns runs in kube-system
    ports:
    - protocol: UDP
      port: 53  # DNS port
    - protocol: TCP
      port: 53  # DNS over TCP (rare but can happen)

Why this matters: Service discovery, external API calls, database connections everything requires DNS resolution. Without this policy, all DNS queries fail and applications cannot resolve service names to IPs.

Pattern 5: Allow Traffic from Ingress Controller

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-controller
  namespace: production
spec:
  podSelector:
    matchLabels:
      tier: frontend  # Frontend pods receive external traffic
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx  # Ingress controller namespace
      podSelector:
        matchLabels:
          app.kubernetes.io/name: ingress-nginx
    ports:
    - protocol: TCP
      port: 80
    - protocol: TCP
      port: 443

Pattern 6: Egress to External APIs (IP-Based Filtering)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-external-payment-api
spec:
  podSelector:
    matchLabels:
      app: payment-service
  policyTypes:
  - Egress
  egress:
  # Allow internal cluster traffic
  - to:
    - podSelector: {}
  
  # Allow specific external payment gateway
  - to:
    - ipBlock:
        cidr: 52.32.123.45/32  # Stripe API IP (example)
    ports:
    - protocol: TCP
      port: 443  # HTTPS only
  
  # Allow DNS
  - to:
    - namespaceSelector:
        matchLabels:
          name: kube-system
    ports:
    - protocol: UDP
      port: 53

Security benefit: Payment service can only connect to internal cluster pods, DNS, and the specific payment gateway IP. If compromised, cannot exfiltrate data to attacker-controlled servers.

Understanding Selector Logic: AND vs OR Conditions

The Confusing Part of Network Policies

Network Policy selector logic is subtle and frequently misunderstood, leading to policies that don't work as intended. The key is understanding when selectors combine with AND logic versus OR logic.

AND Logic (Both conditions must match):

ingress:
- from:
  - namespaceSelector:
      matchLabels:
        env: production
    podSelector:
      matchLabels:
        app: frontend

# This means: Pods from (namespace with env=production) AND (pod with app=frontend)
# Both namespace AND pod selector must match same source

OR Logic (Either condition matches):

ingress:
- from:
  - namespaceSelector:
      matchLabels:
        env: production
  - podSelector:
      matchLabels:
        app: frontend

# This means: Pods from (namespace with env=production) OR (pod with app=frontend)
# Two separate list items = OR relationship

Common mistake example:

# Intended: allow app=frontend pods that run in env=production namespaces
# MISTAKE: the extra dash makes podSelector a second, separate peer (OR):
# any pod in an env=production namespace, OR any app=frontend pod in this namespace
- from:
  - namespaceSelector: {matchLabels: {env: production}}
  - podSelector: {matchLabels: {app: frontend}}

# CORRECT: one peer carrying both selectors (AND)
- from:
  - namespaceSelector: {matchLabels: {env: production}}
    podSelector: {matchLabels: {app: frontend}}

Advanced Network Policy Patterns

Pattern 7: Allow Specific External CIDR Ranges

egress:
- to:
  - ipBlock:
      cidr: 10.0.0.0/8  # Allow RFC 1918 private network...
      except:
      - 10.10.5.0/24    # ...except this DMZ subnet
  - ipBlock:
      cidr: 172.16.0.0/12
  - ipBlock:
      cidr: 192.168.0.0/16
# Peers are ORed, so an except only takes effect inside the ipBlock it is written in.
# A separate ipBlock with its own except cannot carve a hole in 10.0.0.0/8.

Use case: Allow internal corporate network access but block specific DMZ subnet.

Pattern 8: Multi-Port Access

ingress:
- from:
  - podSelector:
      matchLabels:
        app: backend
  ports:
  - protocol: TCP
    port: 5432  # PostgreSQL
  - protocol: TCP
    port: 6379  # Redis

Allows: Backend pods can connect on ports 5432 OR 6379 (not AND it's OR for ports list).

Pattern 9: Named Ports (Flexible and Maintainable)

# Service definition
apiVersion: v1
kind: Service
spec:
  ports:
  - name: http
    port: 80
    targetPort: 8080
  - name: metrics
    port: 9090
    targetPort: 9090

---
# Network Policy referencing named ports
ingress:
- from:
  - podSelector:
      matchLabels:
        app: frontend
  ports:
  - protocol: TCP
    port: http  # References named port, not number

Advantage: If you change port numbers in pod spec, Network Policy automatically adapts (references port name, not hardcoded number).

Testing Network Policies Before Production Enforcement

Pre-Deployment Validation Strategy

Step 1: Deploy in Test Namespace First

# Create test namespace with same labels as production
kubectl create namespace production-test
kubectl label namespace production-test name=production-test

# Deploy your application
kubectl apply -f app-deployment.yaml -n production-test

# Apply Network Policies
kubectl apply -f network-policies.yaml -n production-test

# Test functionality thoroughly

Step 2: Test Allowed Traffic

# Should succeed
kubectl exec -n production-test frontend-pod -- \
  wget -O- http://backend-service:8080/health
# Expected: 200 OK

# Should fail (blocked by policy)
kubectl exec -n production-test frontend-pod -- \
  wget -O- http://database-service:5432
# Expected: timeout or connection refused

Step 3: Monitor for Dropped Connections (Cilium example)

# If using Cilium CNI
cilium monitor --type policy-verdict

# Shows:
# -> DROPPED (policy denied)
# <- FORWARDED (policy allowed)

Common Testing Mistakes to Avoid

Mistake 1: Wrong Label Selectors

# Policy targets app=fronted (typo!)
podSelector:
  matchLabels:
    app: fronted  # Typo: should be "frontend"

# Verify labels on actual pods
kubectl get pods --show-labels -n production | grep frontend
# Check that labels match exactly

Mistake 2: Forgetting policyTypes

# Dangerous: no policyTypes specified
spec:
  podSelector: {}
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend

# What happens:
# Ingress: Explicitly allowed from frontend (correct)
# Egress: NOT controlled (default allow because policyTypes missing Egress)
# Pods can still exfiltrate data via egress!

# Correct: Always specify policyTypes
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress  # Now egress is also controlled

Troubleshooting Network Policy Issues

Issue: Traffic Blocked Unexpectedly

Debugging steps:

# 1. Verify Network Policies exist and target correct pods
kubectl get networkpolicies -n production
kubectl describe networkpolicy allow-frontend-to-backend

# 2. Check pod labels match policy selectors
kubectl get pods --show-labels | grep backend
# Verify app=backend label exists

# 3. Check namespace labels for cross-namespace policies
kubectl get namespace production --show-labels
# Verify name=production label if policies use namespaceSelector

# 4. Test connectivity
kubectl exec frontend-pod -- wget -T 5 -O- http://backend:8080
# -T 5 = 5 second timeout (don't wait forever)

# 5. Check CNI plugin logs
kubectl logs -n kube-system -l k8s-app=calico-node
# Look for "Policy denied" messages

Issue: Policies Not Enforcing (Silent Failure)

Most common cause: CNI doesn't support Network Policies

# Verify CNI supports policies
kubectl api-resources | grep networkpolicies

# Check which CNI is installed
kubectl get pods -n kube-system | grep -E "calico|cilium|weave|flannel"

# If Flannel: Need to add Calico for policy enforcement
# Install Calico policy-only mode (works with Flannel for networking)

Network Policy Best Practices

1. Start with Default Deny

Always implement deny-all first, then add explicit allows

2. Test in Staging First

Never apply untested policies to production

3. Always Allow DNS

UDP:53 to kube-system namespace — required for everything

4. Document Why Each Policy Exists

Add annotations explaining purpose

metadata:
  annotations:
    description: "Allows frontend to access backend API only on port 8080"
    owner: "platform-team"
    reviewed: "2025-01-15"

5. Use Namespace Labels Consistently

# Label all namespaces
kubectl label namespace production name=production env=prod
kubectl label namespace staging name=staging env=staging
kubectl label namespace monitoring name=monitoring
kubectl label namespace kube-system name=kube-system

# Makes namespaceSelector policies work reliably

6. Combine with Service Mesh for Layer 7 Policies

Network Policies operate at Layer 3/4 (IP, port). For Layer 7 (HTTP method, path, headers), use service mesh:

  • Istio: AuthorizationPolicy for HTTP-level rules (allow POST to /api/users, block DELETE from untrusted sources)
  • Linkerd: Server and ServerAuthorization for HTTP policy
  • Cilium: Native Layer 7 Network Policy support

7. Monitor Denied Connections

Set up logging/alerting for policy denials to detect:

  • Legitimate traffic blocked by misconfigured policy (fix policy)
  • Attack attempts or compromised pods (security incident)
  • New application features requiring policy updates

8. Version Control Network Policies

Store in Git with application code
Review changes via pull requests
Deploy via GitOps (ArgoCD, Flux)

Conclusion: Zero-Trust Networking for Kubernetes

Kubernetes Network Policies enable zero-trust networking: verify every connection, allow nothing by default, and explicitly permit only required traffic. This dramatically reduces the attack surface and limits the impact of breaches.

Critical Implementation Steps:

  1. Verify your CNI enforces Network Policies (Calico, Cilium, Antrea) by testing a deny-all policy
  2. Label all namespaces and pods consistently
  3. Implement default deny-all in production namespaces
  4. Create allow policies for legitimate traffic
  5. Always allow DNS (UDP:53 to kube-system)
  6. Test thoroughly in staging before production
  7. Base allow rules on observed traffic (for example Cilium Hubble flow logs), not guesswork
  8. Monitor and iterate as applications evolve

Network Policies segment traffic inside the cluster; the cloud network around it needs the same discipline. If you run on AWS, request early access to SquareOps Verity for a ranked view of what is exposed — from security groups open to the internet to public EKS API endpoints — with each fix tracked until a scan verifies it.