Implement zero-trust networking in Kubernetes with network policies that control pod-to-pod traffic, namespace isolation, and egress filtering.
The Default-Allow Problem
By default, every pod in a Kubernetes cluster can communicate with every other pod across all namespaces. This flat network model simplifies initial development but creates a significant security risk in production. A compromised pod can reach databases, internal APIs, and services in other namespaces without restriction.
Network Policies implement zero-trust networking at the pod level, defining explicit ingress and egress rules based on labels, namespaces, and CIDR blocks. This guide covers practical policy patterns for microservices architectures, drawing on our security and compliance practice.
Prerequisites
Network Policies require a CNI plugin that supports them. Not all CNI plugins enforce policies:
- Calico - Full NetworkPolicy support plus extended Calico-specific policies
- Cilium - eBPF-based enforcement with L7 visibility
- Weave Net - Basic NetworkPolicy support
- Flannel - Does NOT support NetworkPolicy (common mistake)
Verify your CNI supports policies before relying on them for security.
Default Deny All Traffic
Start by denying all traffic in a namespace, then selectively allow what is needed:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
An empty podSelector matches all pods in the namespace. With no ingress or egress rules defined, all traffic is blocked. This is the foundation of zero-trust networking.
Allow DNS Resolution
After applying default-deny, pods cannot resolve DNS. Allow egress to the kube-dns service:
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
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Service-to-Service Policies
Define explicit communication paths between microservices:
API Gateway to Backend Services
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-gateway-to-api
namespace: production
spec:
podSelector:
matchLabels:
app: user-api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api-gateway
ports:
- protocol: TCP
port: 8080
This policy allows only pods labeled app: api-gateway to reach app: user-api on port 8080. All other ingress to user-api is denied.
Database Access Restriction
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: database-access
namespace: production
spec:
podSelector:
matchLabels:
app: postgresql
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: backend
ports:
- protocol: TCP
port: 5432
Only pods with the tier: backend label can connect to PostgreSQL. Frontend pods, job runners without the label, and any compromised pods cannot reach the database directly.
Namespace Isolation
In multi-tenant clusters, isolate namespaces from each other while allowing specific cross-namespace communication:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-monitoring-scrape
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
purpose: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 9090
- protocol: TCP
port: 9091
This allows Prometheus in the monitoring namespace to scrape metrics from all pods in production, without opening access for other pods in the monitoring namespace.
Egress Filtering
Control what external resources pods can access:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-egress
namespace: production
spec:
podSelector:
matchLabels:
app: payment-service
policyTypes:
- Egress
egress:
# Allow access to internal database
- to:
- podSelector:
matchLabels:
app: postgresql
ports:
- protocol: TCP
port: 5432
# Allow access to Stripe API
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 443
# Allow DNS
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
The payment service can reach its database and external HTTPS endpoints (for Stripe API calls) but nothing else. In a production environment, you would further restrict the CIDR to Stripe's published IP ranges.
Policy Visualization
Understanding the cumulative effect of multiple policies is challenging. Use tools to visualize the network topology:
# Install kubectl network policy viewer
kubectl get networkpolicies -n production -o yaml | npx @kubevious/cli network-policy-viewer
# Or use Cilium's Hubble for real-time flow visualization
hubble observe --namespace production --verdict DROPPED
Testing Network Policies
Validate policies before deploying to production:
# Deploy a test pod
kubectl run test-pod --rm -it --image=busybox --namespace=production -- sh
# Test connectivity from inside the pod
wget -qO- --timeout=3 http://user-api:8080/health
# Should succeed if policy allows it
wget -qO- --timeout=3 http://postgresql:5432
# Should timeout/fail if policy blocks it
# Test cross-namespace access
wget -qO- --timeout=3 http://user-api.production.svc:8080/health
# From a pod in a different namespace - should be blocked
Common Pitfalls
Policy Ordering
Network Policies are additive - if any policy allows traffic, it is allowed. There is no explicit deny rule. The deny-all baseline combined with specific allow policies creates the desired restriction.
Health Check Traffic
Kubelet health checks originate from the node, not from a pod. Some CNI plugins require explicit allowances for node-to-pod health check traffic:
ingress:
- from:
- ipBlock:
cidr: 10.0.0.0/8 # Node CIDR range
ports:
- protocol: TCP
port: 8080
Stateful Connections
Network Policies are stateful - if egress to an external service is allowed, the response traffic is automatically permitted. You do not need separate ingress rules for return traffic.
Best Practices
- Start with default-deny in every namespace
- Label pods consistently - network policies depend entirely on label selectors
- Document every policy with annotations explaining the business reason
- Test policies in staging with real traffic before production deployment
- Monitor dropped packets using your CNI's flow logging capabilities
- Review policies quarterly as service dependencies evolve
- Integrate network policy manifests into your infrastructure as code repository
Network Policies are a fundamental building block of Kubernetes security. Combined with RBAC, pod security standards, and secrets management, they create defense-in-depth that limits the blast radius of any security incident.
Frequently asked questions
Does Flannel support Kubernetes NetworkPolicy?
No. Flannel does not enforce NetworkPolicy, and assuming it does is a common mistake. Calico offers full NetworkPolicy support plus its own extended policies, Cilium enforces them with eBPF and adds L7 visibility, and Weave Net gives basic support. Confirm what the cluster's CNI plugin enforces before relying on network policies for security.
Can pods in different Kubernetes namespaces talk to each other by default?
Yes, every pod in a Kubernetes cluster can reach every other pod across all namespaces by default. The flat network makes early development simpler, but a compromised pod can then reach databases, internal APIs and services in other namespaces without restriction. Network policies close that gap with explicit ingress and egress rules based on labels, namespaces and CIDR blocks.
How do I create a default deny NetworkPolicy in Kubernetes?
Apply a NetworkPolicy with an empty podSelector, policyTypes set to Ingress and Egress, and no ingress or egress rules. The empty selector matches every pod in the namespace, so all traffic is blocked. Pods then cannot resolve DNS, so add a second policy allowing egress on UDP and TCP port 53 to the kube-dns pods in kube-system. Allow policies for each real communication path go on top of that baseline.
How do I restrict access to a database pod with a Kubernetes NetworkPolicy?
Select the database pods, for example by the label app: postgresql, and allow ingress only from pods labeled tier: backend on TCP port 5432. Frontend pods, job runners without the label and any compromised pod can then no longer reach PostgreSQL directly. The same pattern lets only pods labeled app: api-gateway reach a user-api service on port 8080, with all other ingress to user-api denied.
How do I let Prometheus scrape pods in another namespace when network policies are enabled?
Create an ingress policy in the target namespace with an empty podSelector and one from entry that pairs a namespaceSelector for purpose: monitoring with a podSelector for app: prometheus, opening TCP ports 9090 and 9091. Prometheus in the monitoring namespace can then scrape every pod in production. Other pods in the monitoring namespace still get no access.
How do I limit egress traffic from a pod in Kubernetes?
Give the pod an egress policy that lists only the destinations it needs. A payment-service example allows TCP 5432 to the PostgreSQL pods, TCP 443 to ipBlock 0.0.0.0/0 for Stripe API calls and UDP 53 to kube-system for DNS, and nothing else. In production that CIDR should be narrowed to Stripe's published IP ranges. Responses come back without separate ingress rules, because network policies are stateful.
Why do health checks fail after adding Kubernetes network policies?
Kubelet health checks come from the node, and some CNI plugins need an explicit rule before node-to-pod traffic gets through. Add an ingress entry with an ipBlock for the node CIDR range, for example 10.0.0.0/8, on the health check port such as TCP 8080. Network policies are additive, so traffic allowed by any one policy gets through, and there is no explicit deny rule.
How do I test Kubernetes network policies before production?
Start a throwaway pod with kubectl run test-pod --rm -it --image=busybox --namespace=production -- sh and try connections from inside it with wget -qO- --timeout=3. A request on an allowed path, such as user-api on port 8080, should succeed, and one a policy blocks, such as postgresql on 5432, should time out. Repeat from a pod in another namespace to confirm cross-namespace access is blocked. On Cilium, hubble observe --namespace production --verdict DROPPED shows dropped flows.
Read how we handle it in Cybersecurity.
Related Articles
How to Kill Password SSH Across a Fleet, Once
Password authentication on SSH is the single setting that turns a leaked or guessed credential into a shell. Turning it off is five lines of config; doing it across a fleet without locking yourself out is the part that needs an order. This is that order, including the check that proves keys work before you disable the fallback, why AuthenticationMethods beats PasswordAuthentication alone, and how to leave one deliberate way back in.
SecurityHow to Automate Certificates for Hundreds of Client Domains
Maximum certificate lifetimes are shrinking on a published schedule, so the number of renewals an agency or host runs each year is about to multiply twice over. This guide covers the pattern that survives it: DNS-01 validation with CNAME delegation, so a customer makes one record once and you never hold their zone credentials. It also covers which Let's Encrypt rate limit you meet first depending on how your names are shaped, why the authorization failure limit turns a small mistake into an outage, and how ACME Renewal Information takes the renewal schedule out of your hands entirely.
SecurityThe Brotli Flaw Behind Node2Shell and How to Patch Node.js
A Brotli decoder flaw fixed on October 21 can let crafted data run code in Node.js apps that fetch remote content or inflate request bodies. How to patch.