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
backendpods can talk to themysqlpod 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
kindclusters (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
NotReadystate.
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:
-
frontendpod + service -
backendpod + service -
mysqlpod + service (dbservice)
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:
-
frontendcould reachbackend -
backendcould reachmysql -
frontendcould also reachmysqldirectly (which we probably don't want)
Now it was time to lock it down.
My First Real Network Policy: Only Backend → MySQL
The goal:
-
mysqlshould only accept traffic from pods with labelrole: 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 themysqlpod (name: mysql) -
policyTypes: [Ingress]: we're controlling inbound traffic -
ingress.from.podSelector: only pods withrole: backendare allowed -
ports: only port 3306
After applying this:
-
backend→mysqlworks -
frontend→mysqlblocked -
mysql→ anywhere (outbound) still allowed (we didn't restrict Egress)
How I Tested It
I used a simple approach:
- Run a temporary pod with
curlormysqlclient. - 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
Top comments (0)