Kubernetes doesn't restrict pod-to-pod traffic by default. Out of the box, any pod in your cluster can talk to any other pod, across namespaces, without restriction — which means a single compromised container can pivot freely across your workloads. This is one of the most common gaps auditors and pentesters flag in Kubernetes environments, and it's entirely preventable.
Kubernetes network policies let you define exactly which pods can communicate with which, over which ports and protocols, turning your flat cluster network into a segmented one. In this guide, you'll learn how to plan, write, apply, and verify kubernetes network policies step by step — from enabling a compatible CNI plugin to building a default-deny baseline and allow-listing only the traffic your applications actually need. By the end, you'll have a working example you can adapt to your own namespaces, plus a troubleshooting checklist for when policies don't behave as expected.
Step 1: Understand How Kubernetes Network Policies Work
Before writing any YAML, it helps to understand the model. A NetworkPolicy resource is scoped to a namespace and selects pods using label selectors. It then defines ingress and/or egress rules that whitelist traffic from or to other pods, namespaces, or IP blocks.
Two things trip people up early on:
- Policies are additive, not exclusive by default. If no policy selects a pod, all traffic to and from it is allowed. Once at least one policy selects a pod for a given direction (ingress or egress), only the traffic explicitly allowed by that policy (or any other policy selecting it) is permitted — everything else is dropped.
- Kubernetes network policies require a compliant CNI plugin. The API object is just a spec; enforcement is handled by the network plugin. Without one,
kubectl applywill succeed, but nothing will actually be blocked.
Step 2: Choose and Enable a CNI Plugin That Enforces Network Policies
The default kubenet plugin and some cloud-managed networking layers don't enforce NetworkPolicy objects at all. You need a CNI plugin that does — Calico, Cilium, and Weave Net are the most common choices.
For this calico network policy tutorial, we'll use Calico since it's widely supported, works well on managed clusters (EKS, AKS, on-prem kubeadm), and also supports its own extended GlobalNetworkPolicy CRDs for cluster-wide rules.
Install Calico on a kubeadm cluster:
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml
If you're on a managed service like EKS or GKE, check whether network policy enforcement is a togglable cluster feature first — for example:
# EKS: install the Calico add-on via eksctl
eksctl utils enable-network-policy-support --cluster=<cluster-name>
Verify the plugin is running before moving on:
kubectl get pods -n kube-system -l k8s-app=calico-node
Step 3: Establish a Default-Deny Baseline
The single highest-leverage step to restrict pod traffic in Kubernetes is a default-deny policy. Without it, every subsequent "allow" rule you write is layered on top of an already-open network, and it's easy to miss a gap.
Apply a default-deny for both ingress and egress in a namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
An empty podSelector: {} matches every pod in the namespace. With no ingress or egress fields defined, this drops all traffic in both directions for every pod in payments — a clean slate you can now open up deliberately.
Repeat this for every namespace that holds workloads you care about protecting. Leave system namespaces like kube-system alone unless you've mapped their traffic carefully; breaking DNS or the API server's control-plane traffic is a fast way to take down a cluster.
Step 4: Write a Kubernetes Network Policy Example for Allowed Traffic
Now allow only the traffic your application actually needs. Here's a concrete k8s network policy example for a typical three-tier setup where a frontend deployment should be allowed to reach a backend deployment on port 8080, and nothing else should:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: payments
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
Pair it with an egress rule so backend can still reach the database and DNS:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-egress
namespace: payments
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Apply both with kubectl apply -f backend-policy.yaml and confirm they exist:
kubectl get networkpolicy -n payments
Step 5: Isolate Namespaces and Extend with Calico Global Policies
Pod-level rules handle intra-namespace traffic, but you often also want to restrict pod traffic in Kubernetes across namespace boundaries — for example, ensuring only the ingress-nginx namespace can reach your app namespaces, and that staging can never talk to production.
A namespace-scoped ingress rule using labels:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-ingress-controller
namespace: payments
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
If you installed Calico, you can go further with a GlobalNetworkPolicy that applies cluster-wide without being tied to a single namespace — useful for org-wide rules like "deny all egress to the public internet except through an approved egress gateway":
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
name: deny-egress-to-internet
spec:
order: 100
selector: all()
types:
- Egress
egress:
- action: Allow
destination:
selector: role == 'egress-gateway'
- action: Deny
This is the part of a calico network policy tutorial that's easy to skip but genuinely valuable: standard Kubernetes NetworkPolicy objects can't reference IP ranges outside the cluster as cleanly, or apply a single rule across every namespace at once.
Step 6: Test and Verify Enforcement
Never assume a policy works because kubectl apply returned no errors. Test both the allowed and denied paths explicitly.
Spin up a temporary pod and try to reach something that should now be blocked:
kubectl run test-pod --rm -it --image=busybox -n payments -- sh
# inside the pod:
wget -qO- --timeout=3 http://database:5432
That request should time out. Then confirm the allowed path still works:
kubectl exec -n payments deploy/frontend -- curl -s -m 3 http://backend:8080/healthz
For a faster feedback loop across many rules at once, use a purpose-built tool like kube-network-policy-viewer or Cilium's cilium connectivity test (if you're on Cilium) to visualize which paths are open.
Troubleshooting Kubernetes Network Policies
- Policy applied, but nothing is blocked. Almost always means the CNI plugin doesn't enforce
NetworkPolicyobjects. Re-check Step 2 —kubectl get pods -n kube-systemfor your CNI's controller pods, and check its logs for policy-sync errors. - DNS resolution breaks after adding a default-deny. You forgot to allow egress to
kube-dns/corednson UDP/TCP 53. Add an explicit egress rule tokube-systemon port 53, as shown in Step 4. - Traffic from the ingress controller is dropped. Check that your namespace selector label matches exactly —
kubernetes.io/metadata.nameis auto-populated by Kubernetes 1.21+, but custom labels need to be applied manually withkubectl label namespace. - Rules conflict or seem to be ignored. Multiple policies selecting the same pod are additive (union of allows), not evaluated in order — except for Calico's own CRDs, which do use
orderand canDenyexplicitly. Don't mix mental models between the two APIs. - Cross-namespace traffic still isn't isolated. Confirm you're using
namespaceSelectorcorrectly — a missing selector combined withpodSelector: {}matches pods in the same namespace only, which is a common mistake when trying to restrict pod traffic in Kubernetes across namespace boundaries.
How Safeguard Helps
Writing correct network policies is only half the job — the harder problem is knowing whether the policies you shipped actually match your intended segmentation, and catching drift before it becomes an incident. Safeguard continuously scans your Kubernetes manifests and live cluster state as part of your software supply chain security posture, flagging namespaces with no default-deny baseline, overly permissive podSelector: {} allow rules, and workloads that can reach the public internet without an explicit egress policy.
Because Safeguard ties this analysis back to the CI/CD pipeline and IaC that produced the manifests, teams get policy-gap findings at pull-request time — before a misconfigured or missing network policy ever reaches production — alongside the rest of your supply chain risk signals like vulnerable base images, unsigned artifacts, and dependency provenance. That means network segmentation stops being a one-time hardening exercise and becomes a continuously verified property of your cluster.