1. The Problem: The “Flat Network” Danger
Imagine you work in a high-security corporate office. You scan your badge at the front desk and walk into the lobby. Once inside, you realize something terrifying: none of the internal doors are locked. You can walk into the HR filing room, the CEO’s office, or the server room without anyone stopping you.
This is exactly how a default Kubernetes cluster operates. By design, Kubernetes uses a “flat network” model. Any Pod can talk to any other Pod, regardless of which Namespace they are in.
While this default behavior makes initial development incredibly frictionless, it is a nightmare for security. In the real world, if a threat actor exploits a vulnerability in your public-facing frontend Pod (e.g., via a Log4j or SQL injection exploit), they gain a foothold in your cluster. From there, they can freely ping, scan, and laterally move to your internal billing services, databases, or Redis caches because nothing is stopping them.
2. The Solution: Kubernetes Network Policies
To prevent lateral movement, we need to introduce Zero Trust Security inside our cluster. In Kubernetes, this is achieved using a NetworkPolicy.
Think of a NetworkPolicy as an internal firewall (operating at OSI Layers 3 and 4) that dictates exactly which Pods are allowed to talk to each other. Instead of locking down the perimeter and leaving the inside open, Network Policies enforce “least privilege” access between individual microservices.
A NetworkPolicy evaluates traffic based on three primary pillars:
- Pod Selectors: Allow traffic only from Pods bearing specific labels (e.g.,
app: frontend). - Namespace Selectors: Allow traffic only from Pods residing in specific namespaces (e.g., the
productionnamespace). - IP Blocks (CIDR): Allow or Deny traffic to/from external IP ranges outside the cluster.
[!WARNING] The CNI Reality Check: Kubernetes itself does not enforce Network Policies; it merely stores the YAML configuration in
etcd. The actual enforcement is done by your Container Network Interface (CNI) plugin.If you are using a basic CNI like Flannel, creating a NetworkPolicy will silently do nothing. To enforce policies, you must use a CNI that supports them, such as Calico (which compiles policies into Linux
iptables), Cilium (which uses high-performance eBPF), or Antrea.
3. Interactive: Zero Trust Firewall Visualizer
Let’s test this concept. Below is a simulation of a Zero Trust architecture. We have a policy in place that explicitly allows traffic only from the Frontend Pod.
Click the Pods to simulate network requests and observe how the firewall handles the traffic.
Waiting for traffic...
Current Policy: Allow Ingress from app: frontend
4. Anatomy of a Network Policy
Let’s dissect a real-world NetworkPolicy to understand how the rules are evaluated.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow-db
namespace: production
spec:
# 1. Target Pods: Which pods does this policy apply TO?
podSelector:
matchLabels:
app: database
# 2. Policy Types: Are we filtering incoming (Ingress) or outgoing (Egress) traffic?
policyTypes:
- Ingress
# 3. Rules: What traffic is explicitly allowed?
ingress:
- from:
# Allow traffic from pods with label 'app: api' in ANY namespace
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 5432
The Three Targeting Mechanisms
When defining ingress.from or egress.to blocks, you can mix and match three selectors:
podSelector: Matches Pods within the same namespace (unless combined withnamespaceSelector).namespaceSelector: Matches all Pods within a specific namespace (e.g., allowing monitoring tools from theobservabilitynamespace).ipBlock: Used strictly for routing IP traffic outside the cluster, such as allowing Egress out to a third-party managed database or restricting Ingress from a specific corporate VPN CIDR.
5. Step-by-Step Practical Examples
The golden rule of Zero Trust is to start by locking all doors, and then explicitly handing out keys.
Step 1: The Default Deny (Locking the Doors)
By default, traffic is allowed. However, the moment a NetworkPolicy selects a Pod, that Pod becomes isolated. Any traffic not explicitly allowed by the policy is dropped.
To secure a namespace, we deploy a “Default Deny” policy. This selects all Pods ({}) and defines empty Ingress/Egress rules, effectively dropping everything.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: secure-ns
spec:
podSelector: {} # Selects all pods in this namespace
policyTypes:
- Ingress
- Egress
Step 2: Explicitly Allow DNS (Egress)
If you apply a default deny to Egress, your Pods won’t be able to resolve domain names because traffic to CoreDNS (port 53) will be blocked. Let’s open a hole just for DNS.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: secure-ns
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
Step 3: Explicitly Allow Web Traffic (Ingress)
Now we allow our web frontend to receive internet traffic, typically routed via an Ingress Controller.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-web-ingress
namespace: secure-ns
spec:
podSelector:
matchLabels:
app: web-frontend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx # Allow traffic from the NGINX ingress controller namespace
ports:
- protocol: TCP
port: 80
By systematically layering these additive policies, we transform our cluster from an open “flat network” into a heavily fortified, Zero Trust environment.