If you've ever wondered, "How do I stop my pods from randomly talking to everything in my cluster?" – this is the post for you.
Today, I dove into Kubernetes Network Policies – the way you control who can talk to whom inside your cluster. I started with weavenet, then switched to Calico, and I'm excited to walk you through how it all came together.
What Even Is a Network Policy?
A NetworkPolicy in Kubernetes is like a firewall rule for your pods. It lets you:
- Control inbound traffic (who can talk to a pod)
- Control outbound traffic (where a pod can talk to)
For example:
- You can create a deny-all policy that blocks all incoming traffic to a pod.
- Or an allow-only policy that says: "Only the
backend pods can talk to the mysql pod on port 3306."
Without network policies, by default, everything can talk to everything in your cluster. That's fine for demos, terrible for real security.
Why I Needed a Different CNI
Kubernetes itself doesn't implement network policies. It just defines them. The actual work is done by your CNI plugin (Container Network Interface).
I started with weavenet, which supports basic network policies. But as I dug deeper, I realized I wanted something more powerful and production-friendly. That's when I switched to Calico, which is:
- Fast
- Scalable
- Great for complex network policies
- Works well with
kind clusters (as long as you configure it correctly)
If you're just experimenting, weavenet is fine. If you want to level up your cluster networking, Calico is a great next step.
My Test Cluster: A Kind Setup with Calico
To test network policies, I created a simple kind cluster with:
- 1 control-plane node
- 2 worker nodes
- Disabled default CNI (
disableDefaultCNI: true)
- Custom pod subnet:
192.168.0.0/16
kind Cluster YAML
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30001
hostPort: 30001
- role: worker
- role: worker
networking:
disableDefaultCNI: true
podSubnet: "192.168.0.0/16"
Creating the Cluster
With the YAML saved as kind-config.yaml, I created the cluster like this:
kind create cluster --name day26 --config kind-config.yaml
Then I checked the nodes:
kubectl get nodes
And here's the part that was a bit confusing at first:
Until I installed a CNI (like Calico), the worker nodes stayed in a NotReady state.
That's because Kubernetes needs a CNI to:
- Assign IPs to pods
- Set up networking between nodes
- Enable traffic to flow across the cluster
Without a CNI, the control-plane node can be Ready, but the worker nodes will remain NotReady until networking is configured.
Only after installing Calico did all nodes show as Ready:
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# day26-control-plane Ready control-plane ...
# day26-worker Ready worker ...
# day26-worker2 Ready worker ...
Once Calico was running, my cluster was ready for real network policy experiments.
The App I Built: Frontend → Backend → MySQL
To make network policies meaningful, I created a tiny 3-tier app:
frontend pod + service
backend pod + service
mysql pod + service (db service)
Pods & Services
apiVersion: v1
kind: Pod
metadata:
name: frontend
labels:
role: frontend
spec:
containers:
- name: nginx
image: nginx
ports:
- name: http
containerPort: 80
protocol: TCP
***
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
role: frontend
spec:
selector:
role: frontend
ports:
- protocol: TCP
port: 80
targetPort: 80
***
apiVersion: v1
kind: Pod
metadata:
name: backend
labels:
role: backend
spec:
containers:
- name: nginx
image: nginx
ports:
- name: http
containerPort: 80
protocol: TCP
***
apiVersion: v1
kind: Service
metadata:
name: backend
labels:
role: backend
spec:
selector:
role: backend
ports:
- protocol: TCP
port: 80
targetPort: 80
***
apiVersion: v1
kind: Service
metadata:
name: db
labels:
name: mysql
spec:
selector:
name: mysql
ports:
- protocol: TCP
port: 3306
targetPort: 3306
***
apiVersion: v1
kind: Pod
metadata:
name: mysql
labels:
name: mysql
spec:
containers:
- name: mysql
image: mysql:latest
env:
- name: "MYSQL_USER"
value: "mysql"
- name: "MYSQL_PASSWORD"
value: "mysql"
- name: "MYSQL_DATABASE"
value: "testdb"
- name: "MYSQL_ROOT_PASSWORD"
value: "verysecure"
ports:
- name: http
containerPort: 3306
protocol: TCP
At this point, everything could talk to everything:
frontend could reach backend
backend could reach mysql
frontend could also reach mysql directly (which we probably don't want)
Now it was time to lock it down.
My First Real Network Policy: Only Backend → MySQL
The goal:
mysql should only accept traffic from pods with label role: backend
- On port 3306
- No one else (including
frontend) should touch the database
NetworkPolicy Manifest
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-test
spec:
podSelector:
matchLabels:
name: mysql
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: backend
ports:
- port: 3306
Key parts:
podSelector: targets the mysql pod (name: mysql)
policyTypes: [Ingress]: we're controlling inbound traffic
ingress.from.podSelector: only pods with role: backend are allowed
ports: only port 3306
After applying this:
backend → mysql works
frontend → mysql blocked
mysql → anywhere (outbound) still allowed (we didn't restrict Egress)
How I Tested It
I used a simple approach:
- Run a temporary pod with
curl or mysql client.
- Try to reach services from different pods.
- Observe which connections succeed or fail.
Example:
# From frontend pod – should fail
kubectl exec -it frontend -- curl -s http://db:3306
# From backend pod – should succeed
kubectl exec -it backend -- curl -s http://db:3306
Seeing the first one fail and the second one succeed was the "aha!" moment: the policy is actually working.
Wrapping Up
Network Policies are one of those things that feel abstract until you actually apply them and see connections blocked in real time. Once that happens, you suddenly understand:
- Why security matters
- Why "everything can talk to everything" is not a strategy
- How Kubernetes gives you granular control over your cluster's traffic