This is Level 6 of our Kubernetes networking series. So far, we've learned that Kubernetes networking is, by default, remarkably open — any Pod can talk to any other Pod:
Pod A ─────────→ Pod B
Pod C ─────────→ Pod B
Pod D ─────────→ Pod B
That's great for connectivity, but terrible for security. In a real application, you almost never want everything talking to everything. This article covers the Kubernetes object designed specifically to fix that: NetworkPolicy.
Table of Contents
- The Problem: Too Much Trust
- The Solution: NetworkPolicy
- Two Directions: Ingress and Egress
- An Easy Way to Remember Ingress vs Egress
- A Simple Scenario
- Your First NetworkPolicy
- Reading the Policy Step by Step
- The Policy Selects the Destination
- What About the Database?
- NetworkPolicy Doesn't Block Everything by Default
- Default Deny
- Why Default Deny Is Useful
- Controlling Egress Traffic
- An Egress Policy Example
- Namespaces Matter More Than You Think
- namespaceSelector
- Combining podSelector and namespaceSelector
- Three Key Selectors: podSelector, namespaceSelector, ipBlock
- Restricting Ports
- A Complete Multi-Tier Example
- NetworkPolicy Is Not an Application Firewall
- NetworkPolicy Doesn't Replace Authentication
- Who Actually Enforces NetworkPolicy?
- Two Common Misconceptions
- Hands-On: Inspecting NetworkPolicies
- Hands-On: Creating an Allow Policy
- Hands-On: The Default-Deny Lab
- A Production Mental Model
- Troubleshooting NetworkPolicy
- The Mental Model to Memorize
- Level 6 Checkpoint
- What's Next: CNI
The Problem: Too Much Trust
Imagine a typical three-tier application:
Internet → Frontend → Backend → Database
Ideally, we want a strict set of allowed paths:
Frontend → Backend ✅
Backend → Database ✅
Frontend → Database ❌
Database ← Internet ❌
But by default, Kubernetes doesn't enforce any of this. Without restrictions, every one of these connections is technically possible:
frontend → backend ✅
frontend → database ✅
backend → database ✅
backend → frontend ✅
database → frontend ✅
database → backend ✅
That's usually far too permissive. A compromised frontend Pod could, in principle, connect directly to the database — bypassing the backend entirely. We need a way to explicitly say: this path is allowed, that one isn't.
The Solution: NetworkPolicy
A NetworkPolicy is a Kubernetes object that defines rules controlling network traffic to and from selected Pods. In one sentence:
NetworkPolicy = firewall-like rules for Pod traffic.
The mental model:
NetworkPolicy
↓
Which Pods?
↓
What traffic?
↓
Allow / restrict
Two Directions: Ingress and Egress
NetworkPolicy operates along two possible directions — and getting these terms straight is essential.
Ingress — traffic coming into a Pod:
Frontend
| traffic
↓
Backend Pod
↑
ingress
Egress — traffic leaving a Pod:
Backend Pod
| traffic
↓
Database
↑
egress from Backend
An Easy Way to Remember Ingress vs Egress
Picture yourself standing inside a room:
- INGRESS = people coming IN
- EGRESS = people going OUT
For a Pod:
Traffic → Pod = Ingress
Pod → Traffic = Egress
A Simple Scenario
Suppose we have three Pods: frontend, backend, and database, each with a matching label:
frontend: app=frontend
backend: app=backend
database: app=database
We want:
frontend → backend ✅
backend → database ✅
frontend → database ❌
A NetworkPolicy can express this as: "Backend Pods may receive traffic from Frontend Pods."
Your First NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
Don't worry about memorizing this YAML yet — let's walk through exactly what it means.
Reading the Policy Step by Step
Which Pods does the policy apply to?
podSelector:
matchLabels:
app: backend
This means: apply this policy to Pods labeled app: backend.
backend Pod 1 ← selected
backend Pod 2 ← selected
frontend Pod ← not selected
database Pod ← not selected
What direction is being controlled?
policyTypes:
- Ingress
This means: we're controlling traffic coming into the selected backend Pods.
Who is allowed in?
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
This means: backend Pods can receive traffic from Pods labeled app: frontend.
So frontend → backend is allowed, but anything not explicitly listed (like database → backend) is not covered by this rule.
The Policy Selects the Destination
This point confuses a lot of beginners, so let's be explicit. The top-level podSelector defines where the policy applies — the destination:
podSelector:
matchLabels:
app: backend
The from block inside ingress defines who is allowed to reach it — the source:
from:
- podSelector:
matchLabels:
app: frontend
Visually:
NetworkPolicy
|
↓
┌─────────────┐
│ backend │
└──────▲──────┘
|
allowed
|
┌──────┴──────┐
│ frontend │
└─────────────┘
What About the Database?
Suppose we never wrote an explicit allow rule for frontend → database. Then, once the database Pods become subject to a NetworkPolicy, that path stays blocked:
frontend → backend ✅
frontend → database ❌
This is the beginning of micro-segmentation: instead of trusting the entire cluster ("everything can talk to everything"), you deliberately define a small, explicit set of allowed paths — Frontend → Backend, Backend → Database, and nothing else.
NetworkPolicy Doesn't Block Everything by Default
This is a critical concept: creating one NetworkPolicy does not mean "everything else in the cluster is now blocked." Whether a Pod becomes restricted depends on whether it's selected by a policy for the relevant direction:
Policy selects backend
+
Ingress policy exists
↓
backend ingress becomes restricted
Once a Pod becomes "isolated" for a direction (ingress or egress), only the explicitly allowed traffic for that direction gets through. This naturally leads to the concept of default deny.
Default Deny
Suppose you want: "By default, nobody can send traffic to these Pods." You can express that with:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingress
Here, podSelector: {} (an empty selector) means: select all Pods in this namespace. Combined with policyTypes: [Ingress] and no ingress rules at all, the effect is:
Any Pod
|
X
↓
Selected Pods
Nothing is allowed in, for any Pod in the namespace, until you add explicit allow rules.
Why Default Deny Is Useful
Imagine a production namespace with frontend, backend, database, and cache Pods. Instead of assuming everything is safe by default:
frontend → everything
backend → everything
database → everything
...you start from DENY, and explicitly allow only what's actually required:
frontend → backend ✅
backend → database ✅
backend → cache ✅
everything else ❌
This "deny by default, allow by exception" approach is a widely used, well-established security strategy — and NetworkPolicy is how you implement it in Kubernetes.
Controlling Egress Traffic
So far we've focused on incoming traffic, but you can restrict outgoing traffic too. For example:
Backend → Database ✅ allowed
Backend → Random external destination ❌ blocked
You express this using:
policyTypes:
- Egress
An Egress Policy Example
Suppose backend Pods should only be allowed to talk to database Pods:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-to-database
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: database
Read it as: "Select backend Pods, and allow their outgoing traffic to database Pods."
backend
| egress allowed
↓
database
Namespaces Matter More Than You Think
This is one of the most important — and most frequently misunderstood — NetworkPolicy concepts.
Suppose you have a frontend Pod (labeled app=frontend) in the frontend namespace, and a backend Pod (labeled app=backend) in the backend namespace. If you write:
podSelector:
matchLabels:
app: frontend
...does this mean "any frontend Pod in the entire cluster"? No. A plain podSelector in a NetworkPolicy only selects Pods within the same namespace as the policy itself. This is exactly why namespaceSelector exists.
namespaceSelector
Suppose you want: "Allow traffic from Pods in the frontend namespace." You'd write:
from:
- namespaceSelector:
matchLabels:
name: frontend
frontend namespace
| allowed
↓
backend namespace
Combining podSelector and namespaceSelector
You can combine both for much more precise targeting:
from:
- namespaceSelector:
matchLabels:
name: frontend
podSelector:
matchLabels:
app: frontend
This means: "Allow traffic from Pods labeled app: frontend, but only inside namespaces labeled name: frontend."
namespace
+
pod label
↓
specific source Pods
Three Key Selectors: podSelector, namespaceSelector, ipBlock
podSelector — selects Pods based on labels:
podSelector:
matchLabels:
app: backend
namespaceSelector — selects namespaces based on labels:
namespaceSelector:
matchLabels:
team: payments
ipBlock — selects IP ranges:
ipBlock:
cidr: 10.0.0.0/8
This is especially useful when communicating with systems outside Kubernetes entirely — for example, an external database at a fixed IP or CIDR range:
Pod
| allowed
↓
10.20.0.0/16
Restricting Ports
NetworkPolicy can also restrict specific ports. Suppose backend only listens on TCP 8080:
ports:
- protocol: TCP
port: 8080
With this in place (assuming the relevant isolation applies):
frontend → backend:8080 ✅
frontend → backend:22 ❌
This is powerful because you're simultaneously controlling who can connect and on which port — a much finer-grained model than simple Pod-to-Pod allow/deny.
A Complete Multi-Tier Example
Suppose we have frontend, backend, and database Pods, each in their own namespace, and we want:
frontend → backend:8080 ✅
backend → database:5432 ✅
frontend → database:5432 ❌
┌───────────────┐
│ frontend │
└───────┬───────┘
│
8080
↓
┌───────────────┐
│ backend │
└───────┬───────┘
│
5432
↓
┌───────────────┐
│ database │
└───────────────┘
frontend ────────────────X────────→ database
This is exactly the kind of layered security architecture NetworkPolicy is designed to express.
NetworkPolicy Is Not an Application Firewall
An important distinction: NetworkPolicy operates primarily at the network traffic level, not the application layer. It can say:
Allow frontend → backend:8080
But it generally has no concept of things like:
HTTP: GET /users
HTTP: POST /delete-account
For that kind of application-layer control, you'd need additional mechanisms like an Ingress/Gateway, an API gateway, a service mesh, or application-level authorization. In short: NetworkPolicy = network-level access control, not HTTP-aware access control.
NetworkPolicy Doesn't Replace Authentication
If frontend → backend is allowed by a NetworkPolicy, that does not mean "the frontend is authenticated." These are entirely separate security layers:
- NetworkPolicy answers: can network traffic flow at all?
- Authentication answers: who are you?
- Authorization answers: are you allowed to perform this specific action?
Don't confuse network-level permission with identity or permission checks — you typically need all three layered together for real security.
Who Actually Enforces NetworkPolicy?
Here's a subtle but critical concept: a NetworkPolicy object only describes the desired rules — the actual networking implementation has to enforce them.
NetworkPolicy
↓
rules
↓
CNI/network dataplane
↓
enforcement
If your CNI plugin doesn't support NetworkPolicy enforcement, creating a NetworkPolicy object won't magically create a firewall — it'll just sit there, unenforced. This is one of many reasons the choice of CNI plugin matters, which we'll explore properly in Level 7.
For now, keep the division of responsibility simple:
NetworkPolicy → "What traffic should be allowed?"
CNI → "How do I actually enforce that?"
Don't worry yet about whether enforcement happens via iptables, eBPF, or some other mechanism — those details come later.
Two Common Misconceptions
Misconception 1: "NetworkPolicy protects my whole cluster."
Not exactly — policies are namespace-scoped objects that select specific Pods. You need to design policies deliberately across every namespace and workload in your architecture; there's no single switch that secures everything at once.
Misconception 2: podSelector: {} means "no Pods."
Actually, the opposite is true — podSelector: {} means select all Pods in the policy's namespace. This is a very easy mistake to make, and it has real security implications if misunderstood.
Hands-On: Inspecting NetworkPolicies
Check what NetworkPolicies exist in your cluster:
kubectl get networkpolicy
# or the shorter alias:
kubectl get netpol
Inspect one in detail:
kubectl describe networkpolicy <name>
This shows you the Pod selector, policy types, ingress/egress rules, ports, and sources/destinations all in one place.
Hands-On: Creating an Allow Policy
Create two labeled Pods:
kubectl run frontend --image=nginx --labels=app=frontend
kubectl run backend --image=nginx --labels=app=backend
Verify their labels:
kubectl get pods --show-labels
frontend app=frontend
backend app=backend
Now apply a policy selecting backend:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-policy
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
kubectl apply -f backend-policy.yaml
The conceptual result: frontend → backend is allowed, while other sources attempting to reach backend may be blocked, depending on your cluster's NetworkPolicy implementation and any other policies in effect.
Hands-On: The Default-Deny Lab
A great learning exercise is applying a namespace-wide default deny:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes:
- Ingress
kubectl apply -f default-deny.yaml
Now every selected Pod is denied ingress traffic by default:
All selected Pods
↑
|
DENY by default
From here, you layer in specific allow policies as needed — the classic pattern:
DEFAULT DENY → ALLOW ONLY WHAT IS REQUIRED
A Production Mental Model
Internet
|
↓
Ingress
|
↓
Frontend
|
↓
Backend
|
↓
Database
A realistic NetworkPolicy setup might enforce:
Internet → Frontend allowed at edge
Frontend → Backend:8080 allowed
Backend → Database:5432 allowed
Frontend → Database blocked
Database → Frontend blocked
Random Pod → Database blocked
That's genuine network segmentation, built entirely out of Kubernetes-native objects.
Troubleshooting NetworkPolicy
Suppose your application suddenly can't connect to something it used to reach fine. Don't immediately assume the Service is broken — walk the chain methodically:
Application → DNS → Service → Pod → NetworkPolicy
If the Pod was reachable directly before a policy was applied, but not afterward, the NetworkPolicy is the likely suspect. Useful commands:
kubectl get networkpolicy
kubectl describe networkpolicy <name>
kubectl get pods --show-labels
kubectl get namespaces --show-labels
The single biggest troubleshooting mistake: the selector doesn't match the Pods you think it matches.
Selector Debugging
Suppose your policy says:
matchLabels:
app: backend
But your actual Pod is labeled:
app=back-end
Policy selector: app=backend
↓
X
Pod label: app=back-end
No match — the policy simply isn't selecting the Pod you expected. Always verify with kubectl get pods --show-labels before assuming something deeper is wrong.
The Namespace Mistake
Suppose you have a frontend Pod in the frontend namespace and a backend Pod in the backend namespace, and you write only:
podSelector:
matchLabels:
app: frontend
Remember: a plain podSelector in a NetworkPolicy is always namespace-local. If you need to allow traffic across namespaces, you need namespaceSelector combined with podSelector.
The Mental Model to Memorize
NetworkPolicy
|
↓
Which Pods?
|
↓
Ingress / Egress?
|
↓
From where / To where?
|
↓
Which ports?
|
↓
Allow traffic
Or, compressed even further:
WHO → WHERE → DIRECTION → PORT → ALLOW
A useful production security model to keep in your head:
Internet
|
↓
Ingress
|
↓
Frontend
|
NetworkPolicy
|
↓
Backend
|
NetworkPolicy
|
↓
Database
Each boundary in your architecture can — and often should — have its own explicit rules.
Level 6 Checkpoint
1. What is NetworkPolicy?
A Kubernetes API for defining network traffic rules for selected Pods.
2. What is ingress?
Traffic coming into a Pod — A → Pod.
3. What is egress?
Traffic going out of a Pod — Pod → B.
4. What does podSelector do?
Selects Pods by labels, within the policy's own namespace.
5. What does namespaceSelector do?
Selects namespaces by labels.
6. What does ipBlock do?
Selects an IP/CIDR range, typically for traffic to/from outside the cluster.
7. What is default deny?
Start by denying all traffic, then explicitly allow only what's actually required.
8. Does NetworkPolicy itself implement packet filtering?
No — the underlying network implementation (CNI dataplane) must support and enforce it.
9. Does NetworkPolicy provide application authentication?
No. NetworkPolicy → network access, Authentication → identity, Authorization → permissions — three separate layers.
10. Does NetworkPolicy understand HTTP URLs?
Generally no — it's primarily network-level control, not application-aware.
What's Next: CNI
In Level 7, we'll finally answer a question we've deliberately postponed since Level 1:
When Kubernetes creates a Pod, who actually creates its
eth0, its veth pair, its IP address, its routes, and its overall network connectivity?
We'll walk through CNI, CNI plugins, IPAM, veth pairs, bridges/routes/overlays, and popular real-world CNI implementations like Calico, Cilium, and Flannel, along with cloud-native CNIs — while still saving the deepest dataplane internals for Level 8.
Conclusion
NetworkPolicy is what turns Kubernetes networking from "everything can talk to everything" into a deliberately designed, segmented system. The core ideas are simple once they click: a policy selects a group of Pods, declares a direction (ingress and/or egress), and defines exactly who is allowed to communicate with them and on which ports. Layer in a default-deny policy per namespace, and you've built the foundation of real network security in Kubernetes.
Just remember the two biggest gotchas: podSelector is namespace-local unless combined with namespaceSelector, and NetworkPolicy is only as effective as the CNI plugin enforcing it. Get those two details right, and you'll avoid the vast majority of real-world NetworkPolicy debugging headaches.
Found this useful? Follow along for Level 7, where we finally dig into CNI itself — the component quietly responsible for everything we've built on top of it since Level 1. Drop your questions in the comments, and share this with a teammate who's still running a completely flat, unsegmented cluster network. Let's keep building this Kubernetes networking roadmap together. 🚀
Top comments (0)