Understanding Kubernetes: Network Policies
In the previous articles we focused on how connectivity works within Kubernetes: Pods receive IP addresses, Services provide stable identities, and Ingresses allow traffic to enter the cluster. But there is an important question we haven’t asked yet: Just because workloads can communicate, should they?
By default, Kubernetes is surprisingly permissive. Unless configured otherwise, Pods can generally communicate with any other Pod in the cluster. This is where Network Policies come in.
The default behavior: allow everything
One of the most common misconceptions is that Kubernetes provides network isolation out of the box. It doesn’t.
Without Network Policies, a Pod can typically:
- connect to other Pods
- receive traffic from other Pods
- communicate across namespaces
- access external systems
So everything can generally talk to everything else. This keeps networking simple, but is rarely what we want from a security perspective.
Network Policies define allowed traffic
A NetworkPolicy allows us to define which traffic should be permitted. Instead of configuring networking on nodes or IP addresses, Kubernetes uses labels to define communication boundaries.
A minimal example could look like this:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend
spec:
podSelector:
matchLabels:
app: backend
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
This policy says: “Pods labeled app=backend only accept incoming traffic from Pods labeled app=frontend”.
Ingress and egress
Network Policies distinguish between two traffic directions:
- Ingress: Traffic entering a Pod
- Egress: Traffic leaving a Pod.
Policies can control either direction independently using policyTypes, which explicitly enables enforcement for Ingress, Egress, or both. Here is a comprehensive example the Kubernetes Docs
provide:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: my-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- ipBlock:
cidr: 172.17.0.0/16
except:
- 172.17.1.0/24
- namespaceSelector:
matchLabels:
project: myproject
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 6379
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 5978
The important rule: policies are additive
Network Policies do not work like traditional firewall rules - there is no ordering or priority between rules. Instead, policies are additive, which also means that multiple policies can apply to the same Pod simultaneously. Kubernetes then evaluates all Network Policies that select a Pod and rraffic is allowed if it’s permitted by at least one matching policy.
Namespace isolation
Policies can also restrict communication between namespaces. Imagine two namespaces, frontend and backend. Without policies, Pods in both namespaces can communicate.
We could use a NetworkPolicy to allow traffic only from the frontend namespace:
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: frontend
This creates a simple isolation boundary between workloads.
One important caveat
Network Policies are part of the Kubernetes API. However, enforcement is handled by the CNI. So, a cluster only enforces Network Policies if the installed CNI supports them. Luckily, most modern CNIs such as Calico or Cilium do.
Demo time
Let’s see NetworkPolicies in action. Please ensure that your cluster uses a CNI supporting Networking Policies. If you are trying with a fresh demo cluster, I would suggest using kind and install cilium on it. This is pretty straight forward:
# 1) Install kind
# check https://kind.sigs.k8s.io/docs/user/quick-start/#installation for reference
# 2) Install cilium-cli
# check https://github.com/cilium/cilium-cli for reference
# 3) Create a kind cluster
kind create cluster
# 4) Install cilium on the cluster
cilium install
So, we are all good and have a CNI in our cluster? Great! Then first, create two Pods
kubectl run frontend --image=busybox:1.36 --labels="app=frontend" -- sleep 3600
kubectl run backend --image=nginx --labels="app=backend"
and expose our backend pod via
kubectl expose pod backend \
--port=80 \
--target-port=80
Let’s now verify connectivity with
kubectl exec frontend -- wget -qO- http://backend
And there we have our “Welcome to nginx!” message - so traffic flows.
Now create a minimal NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress-to-backend
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
and apply it via:
kubectl apply -f policy.yaml
Try the request again. Connectivity breaks? Great, our Network Policy denying all ingress traffic is taking effect.
Allowing specific traffic again
Thing is - we now denied all ingress traffic - but our frontend still should be able to talk to the backend, right? So, we need to add another Network Policy:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-ingress-to-backend
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
Apply it and test connectivity again - the request now succeeds because the frontend Pod is explicitly allowed by the second policy. So we now have moved from “Everyone can talk to the backend” to “Only frontend can talk to backend”. 🎉
Summing up
By default, Kubernetes networking is largely unrestricted: Pods can generally communicate freely across the cluster. Network Policies allow us to define communication boundaries using labels and namespaces instead of IP addresses - and are an important security control. But NetworkPolicies are only one part of Kubernetes security - and that’s what we’ll explore in the next articles. So, stay tuned :)
header image created by buddy ChatGPT
