DEV Community

Cover image for Kubernetes Networking [Level-6: Networking Policy]
ADITYA RAJ
ADITYA RAJ

Posted on

Kubernetes Networking [Level-6: Networking Policy]

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
Enter fullscreen mode Exit fullscreen mode

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

  1. The Problem: Too Much Trust
  2. The Solution: NetworkPolicy
  3. Two Directions: Ingress and Egress
  4. An Easy Way to Remember Ingress vs Egress
  5. A Simple Scenario
  6. Your First NetworkPolicy
  7. Reading the Policy Step by Step
  8. The Policy Selects the Destination
  9. What About the Database?
  10. NetworkPolicy Doesn't Block Everything by Default
  11. Default Deny
  12. Why Default Deny Is Useful
  13. Controlling Egress Traffic
  14. An Egress Policy Example
  15. Namespaces Matter More Than You Think
  16. namespaceSelector
  17. Combining podSelector and namespaceSelector
  18. Three Key Selectors: podSelector, namespaceSelector, ipBlock
  19. Restricting Ports
  20. A Complete Multi-Tier Example
  21. NetworkPolicy Is Not an Application Firewall
  22. NetworkPolicy Doesn't Replace Authentication
  23. Who Actually Enforces NetworkPolicy?
  24. Two Common Misconceptions
  25. Hands-On: Inspecting NetworkPolicies
  26. Hands-On: Creating an Allow Policy
  27. Hands-On: The Default-Deny Lab
  28. A Production Mental Model
  29. Troubleshooting NetworkPolicy
  30. The Mental Model to Memorize
  31. Level 6 Checkpoint
  32. What's Next: CNI

The Problem: Too Much Trust

Imagine a typical three-tier application:

Internet → Frontend → Backend → Database
Enter fullscreen mode Exit fullscreen mode

Ideally, we want a strict set of allowed paths:

Frontend → Backend       ✅
Backend  → Database      ✅
Frontend → Database      ❌
Database ← Internet      ❌
Enter fullscreen mode Exit fullscreen mode

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       ✅
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Egress — traffic leaving a Pod:

Backend Pod
    | traffic
    ↓
Database
   ↑
egress from Backend
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We want:

frontend → backend     ✅
backend  → database    ✅
frontend → database    ❌
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What direction is being controlled?

policyTypes:
  - Ingress
Enter fullscreen mode Exit fullscreen mode

This means: we're controlling traffic coming into the selected backend Pods.

Who is allowed in?

ingress:
  - from:
      - podSelector:
          matchLabels:
            app: frontend
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The from block inside ingress defines who is allowed to reach it — the source:

from:
  - podSelector:
      matchLabels:
        app: frontend
Enter fullscreen mode Exit fullscreen mode

Visually:

        NetworkPolicy
              |
              ↓
       ┌─────────────┐
       │   backend   │
       └──────▲──────┘
              |
           allowed
              |
       ┌──────┴──────┐
       │  frontend   │
       └─────────────┘
Enter fullscreen mode Exit fullscreen mode

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    ❌
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

...you start from DENY, and explicitly allow only what's actually required:

frontend → backend       ✅
backend  → database      ✅
backend  → cache         ✅
everything else          ❌
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You express this using:

policyTypes:
  - Egress
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Read it as: "Select backend Pods, and allow their outgoing traffic to database Pods."

backend
   | egress allowed
   ↓
database
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

...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
Enter fullscreen mode Exit fullscreen mode
frontend namespace
        | allowed
        ↓
backend namespace
Enter fullscreen mode Exit fullscreen mode

Combining podSelector and namespaceSelector

You can combine both for much more precise targeting:

from:
  - namespaceSelector:
      matchLabels:
        name: frontend
    podSelector:
      matchLabels:
        app: frontend
Enter fullscreen mode Exit fullscreen mode

This means: "Allow traffic from Pods labeled app: frontend, but only inside namespaces labeled name: frontend."

namespace
    +
pod label
    ↓
specific source Pods
Enter fullscreen mode Exit fullscreen mode

Three Key Selectors: podSelector, namespaceSelector, ipBlock

podSelector — selects Pods based on labels:

podSelector:
  matchLabels:
    app: backend
Enter fullscreen mode Exit fullscreen mode

namespaceSelector — selects namespaces based on labels:

namespaceSelector:
  matchLabels:
    team: payments
Enter fullscreen mode Exit fullscreen mode

ipBlock — selects IP ranges:

ipBlock:
  cidr: 10.0.0.0/8
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Restricting Ports

NetworkPolicy can also restrict specific ports. Suppose backend only listens on TCP 8080:

ports:
  - protocol: TCP
    port: 8080
Enter fullscreen mode Exit fullscreen mode

With this in place (assuming the relevant isolation applies):

frontend → backend:8080     ✅
frontend → backend:22       ❌
Enter fullscreen mode Exit fullscreen mode

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    ❌
Enter fullscreen mode Exit fullscreen mode
                 ┌───────────────┐
                 │   frontend    │
                 └───────┬───────┘
                         │
                       8080
                         ↓
                 ┌───────────────┐
                 │    backend    │
                 └───────┬───────┘
                         │
                       5432
                         ↓
                 ┌───────────────┐
                 │   database    │
                 └───────────────┘

frontend ────────────────X────────→ database
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

But it generally has no concept of things like:

HTTP: GET /users
HTTP: POST /delete-account
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Inspect one in detail:

kubectl describe networkpolicy <name>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Verify their labels:

kubectl get pods --show-labels
Enter fullscreen mode Exit fullscreen mode
frontend   app=frontend
backend    app=backend
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
kubectl apply -f backend-policy.yaml
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
kubectl apply -f default-deny.yaml
Enter fullscreen mode Exit fullscreen mode

Now every selected Pod is denied ingress traffic by default:

All selected Pods
      ↑
      |
   DENY by default
Enter fullscreen mode Exit fullscreen mode

From here, you layer in specific allow policies as needed — the classic pattern:

DEFAULT DENY → ALLOW ONLY WHAT IS REQUIRED
Enter fullscreen mode Exit fullscreen mode

A Production Mental Model

                  Internet
                     |
                     ↓
                 Ingress
                     |
                     ↓
                 Frontend
                     |
                     ↓
                  Backend
                     |
                     ↓
                 Database
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

But your actual Pod is labeled:

app=back-end
Enter fullscreen mode Exit fullscreen mode
Policy selector: app=backend
                     ↓
                     X
Pod label:       app=back-end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or, compressed even further:

WHO → WHERE → DIRECTION → PORT → ALLOW
Enter fullscreen mode Exit fullscreen mode

A useful production security model to keep in your head:

              Internet
                  |
                  ↓
              Ingress
                  |
                  ↓
             Frontend
                  |
             NetworkPolicy
                  |
                  ↓
              Backend
                  |
             NetworkPolicy
                  |
                  ↓
             Database
Enter fullscreen mode Exit fullscreen mode

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)