Our kubeadm cluster guide built a working cluster — by default, every pod can reach every other pod, across every namespace, with no restriction at all. That’s convenient for getting started and a real problem once anything sensitive (a database, an internal API) runs alongside less trusted workloads. NetworkPolicy resources let you restrict this down to exactly the traffic that should be allowed.
Step 1: Why the Default Is a Risk
A compromised pod — through a vulnerable dependency, a misconfigured service — can, by default, reach any other pod’s network endpoints in the entire cluster, including a database or internal admin API that was never meant to be reachable from a public-facing frontend pod. NetworkPolicy is the Kubernetes-native way to close this off, conceptually similar to security groups in cloud networking, but expressed as Kubernetes resources tied to pod labels rather than IP ranges.

Step 2: Confirm Your CNI Supports NetworkPolicy
NetworkPolicy is a Kubernetes API object, but enforcement is done by the CNI plugin — not every one supports it. Our kubeadm guide installed Calico, which does; Flannel alone does not enforce NetworkPolicy without an add-on. Confirm before writing policies that assume enforcement is actually happening — an unenforced policy silently does nothing, which is worse than an obvious error.

Step 3: Apply a Default-Deny Policy
Start from zero trust: a policy selecting all pods in a namespace with an empty podSelector: {} and policyTypes: [Ingress] denies all incoming traffic to every pod in that namespace unless another policy explicitly allows it. This is the safe default posture — build up specific allow rules from here rather than starting open and trying to lock down piecemeal.

Step 4: Allow Specific Traffic by Label Selector
Add a policy allowing exactly what’s needed: an ingress rule on the api pod’s policy with from: - podSelector: { matchLabels: { app: frontend } } permits only pods labeled app: frontend to reach it — every other pod remains blocked by the default-deny policy from Step 3.

Step 5: Isolate by Namespace
For broader isolation than per-pod rules, use namespaceSelector instead of (or combined with) podSelector — allowing all traffic from pods in a specific namespace (e.g. a shared monitoring namespace scraping metrics across the cluster) without needing to label every individual pod within it.

Step 6: Explicitly Allow Ingress Controller Traffic
Once default-deny is in place, don’t forget the traffic path coming from outside the cluster through your ingress controller (or Traefik, if used inside Kubernetes) — that traffic also needs an explicit allow rule matching the ingress controller’s namespace/labels, or external users will get connection failures that look like a broken app rather than a network policy blocking them.

Step 7: Test That Policies Actually Enforce
Don’t trust a policy without testing it: run a throwaway pod with a specific label and confirm it can reach the intended service (kubectl run test --rm -it --image=busybox --labels=app=frontend -- wget -qO- api:8080), then run one without that label and confirm it’s blocked. Testing both the allow and deny cases catches a policy that looks right on paper but has a labeling typo making it a no-op.

Step 8: Avoid Common Pitfalls
Forgetting egress: a policy with only Ingress in policyTypes doesn’t restrict outbound traffic at all — add Egress rules too if outbound restriction matters for your threat model. Breaking DNS: a default-deny egress policy commonly blocks pods from reaching the cluster’s internal DNS service (kube-dns/CoreDNS) unless you explicitly allow egress to it — a very common “why can’t pods resolve any hostnames anymore” support issue after tightening egress policy.

Example: default-deny + allow frontend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: default
spec:
podSelector: {}
policyTypes: [Ingress]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: default
spec:
podSelector:
matchLabels: { app: api }
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels: { app: frontend }
Related tutorials
Terminal screenshots are original illustrations created for Gnome IT Solutions (blog.gnomeitsolutions.com).