
Here's a fact that should bother you more than it does. In a fresh Kubernetes cluster, every single pod can talk to every other pod. Your public-facing web pod can open a direct connection to your database pod, no permission needed. So can a pod an attacker just popped. Kubernetes ships wide open on purpose, and it's your job to close it. The tool for that is the network policy, and it's the closest thing Kubernetes has to an internal firewall.
This one's about locking pod-to-pod traffic down to only what should exist. I'll build up a normal three-tier app, show the default free-for-all, then fence it in step by step. Usual shape: what, how, why, takeaway.
A normal app, and how traffic moves through it
Let me set the scene with a classic setup, three tiers. A web pod serves the site, an API pod handles the logic, and a database pod stores the data. A request walks through them in order and the answer walks back out:
request hits the web pod on port 80
web calls the API pod on port 5000
API queries the database on port 3306
DB → API → Web → user
Ingress vs egress (the only two words to get right)
Two terms run through everything here, so let me pin them down. Ingress is traffic coming into a pod. Egress is traffic leaving a pod. That's it. The trick is that you classify by where the connection originates, not which way data physically flows. A user hitting the web pod is ingress to the web pod. The web pod then calling the API is egress from the web pod and ingress to the API pod.
If you write out the rules each tier actually needs, it's short and obvious:
| Pod | Ingress (allow in) | Egress (allow out) |
|---|---|---|
| Web | port 80 from users | port 5000 to API |
| API | port 5000 from web | port 3306 to database |
| Database | port 3306 from API | nothing |
Why the default is a problem
Now the uncomfortable default. Kubernetes gives every pod its own IP and joins them all onto one big flat virtual network, so any pod can reach any other by IP, by pod name (via DNS), or by service name. Out of the box there's an implied "allow all", nothing is blocked. In our three-tier app that means this is all permitted:
From my chair, the interesting bit is that the web pod is the one exposed to the internet, so it's the most likely to get compromised. In a default cluster, an attacker who lands there can reach straight into the database. Network policies are how you cut those extra lines, so a foothold in the web tier can't just walk to the crown jewels. This is the same lateral-movement problem I go on about with the kubelet, just pod-to-pod.
What a network policy actually is
A network policy is a Kubernetes object that says which pods are allowed to talk to which. It works using two things you'll use constantly:
- A label is just a key-value tag you stick on a pod, like
app: database. It's how you name a group of pods. - A selector is how a policy picks which pods it's talking about, by matching labels. A
podSelectormatchingapp: databasemeans "this rule is about the database pods".
So the pattern is always: use a selector to point the policy at some pods, then describe the traffic you'll allow to or from them. Let me lock down the database.
Locking the database with an ingress policy
Goal: the database should accept connections only from the API pod, on port 3306, and nothing else. Here's the whole policy, then I'll read it line by line:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-policy
spec:
podSelector:
matchLabels:
role: db # this policy applies to pods labelled role: db
policyTypes:
- Ingress # we're only controlling incoming traffic
ingress:
- from:
- podSelector:
matchLabels:
name: api-pod # allow connections from pods labelled name: api-pod
ports:
- protocol: TCP
port: 3306 # only on TCP 3306
Reading it in plain words: this policy targets pods labelled role: db (the podSelector at the top). It sets policyTypes: Ingress, so we're only governing what comes in. The ingress block then allows traffic from pods labelled name: api-pod, and only on TCP 3306. Everything else in is denied. And because responses are automatic, the database can still reply to the API without any egress rule. You'd only need egress if the database itself started outbound connections (say, pushing a backup somewhere).
Getting more specific: combining selectors
Real clusters need finer control. Say your API pods exist in three namespaces, dev, test and prod, and only the prod API should reach the database. You combine a podSelector with a namespaceSelector so both must be true:
ingress:
- from:
- podSelector:
matchLabels:
name: api-pod
namespaceSelector:
matchLabels:
name: prod # AND: must ALSO be in the prod namespace
Now traffic is allowed only if the pod is labelled api-pod and it lives in the prod namespace. Both conditions, together.
podSelector and namespaceSelector sit under the same list item (one dash), they're combined with AND, both must match. If you give each its own dash, they become separate entries and are combined with OR, either can match. One stray dash quietly changes "prod api pods only" into "any api pod, OR anything in prod", which can let in traffic you never meant to. Read your indentation twice.Allowing something outside the cluster: ipBlock
Selectors only work on things Kubernetes knows about, pods and namespaces. But sometimes you need to allow a machine that lives outside the cluster, like an external backup server. For that you use an ipBlock, which allows a specific IP address or range (written in CIDR notation, where /32 means one exact IP):
ingress:
- from:
- ipBlock:
cidr: 192.168.5.10/32 # allow just this one external IP
You can mix all of these in one rule. Here the database accepts traffic if it's from the prod API pod (the combined AND selector) or from that specific backup IP, on port 3306:
ingress:
- from:
- podSelector:
matchLabels:
name: api-pod
namespaceSelector:
matchLabels:
name: prod
- ipBlock:
cidr: 192.168.5.10/32
ports:
- protocol: TCP
port: 3306
See the two dashes under from? That's deliberate OR this time: "the prod API pod" or "the backup IP". Exactly the structure the callout above warned about, used on purpose.
Adding an egress rule
Everything so far controlled traffic coming in. You need egress only when the pod itself opens outbound connections. Classic example: the database pushing a nightly backup to that external server. Because the database is the one starting that connection, it's egress. You add Egress to policyTypes and describe where it's allowed to go:
policyTypes:
- Ingress
- Egress
# ...ingress block as above...
egress:
- to:
- ipBlock:
cidr: 192.168.5.10/32 # backup server
ports:
- protocol: TCP
port: 80 # allowed out on port 80 only
Now the full policy does two jobs: it only lets the prod API pod (or the backup IP) reach the database on 3306, and it only lets the database reach the backup server on port 80. Tight both ways. Ingress guards what can reach a pod; egress guards where a pod can reach; and you only need egress rules when the pod is the one initiating the connection.
Where hardening starts to feel real
This is the topic where Kubernetes finally starts to feel like something I can actually harden. Everything before was about who you are and what you can ask the API; this is about stopping a compromised pod from strolling across the network to the database. The mental model that stuck: default is allow-all, a policy is a targeted allow-list, and the direction the connection starts is the only thing that decides ingress or egress. The AND/OR dash thing genuinely unsettles me, it's exactly the kind of silent misconfiguration I'd hunt for on an engagement, so I want to build a lab and prove to myself I can spot it from the traffic. And I still need to properly test the Flannel "accepted but ignored" trap with my own eyes. Next I think I'll look at default-deny policies, starting from "block everything" and opening only what's needed, which is the way you'd actually run this in production.
If this made network policies click, come say hi on LinkedIn or the contact page, and tell me what to break down next. More in the Kubernetes Journey.
Further reading
- Kubernetes docs: network policies
- Kubernetes docs: the NetworkPolicy resource and selectors
- Calico: network policy documentation
FAQ
What is a Kubernetes network policy?
A network policy is a Kubernetes object that controls which pods can talk to which. By default every pod can reach every other pod. A policy uses labels and selectors to allow only specific traffic, for example letting the database pod accept connections only from the API pod on port 3306.
What is the difference between ingress and egress in Kubernetes?
Ingress is traffic coming into a pod; egress is traffic leaving a pod. You classify by where the connection starts. Response traffic is allowed automatically, so if you permit an incoming request, the reply back out does not need its own rule.
Why is my Kubernetes network policy not being enforced?
Network policies only work if your cluster's network plugin supports them. Calico, Cilium, Weave Net and Kube-router enforce them; Flannel does not. With an unsupported plugin the policy is accepted without error but silently ignored, which is a dangerous false sense of security.
Related reading
- Topic 13: Verify binaries & upgrade safely (keeping the cluster patched)
- Topic 11: Kubelet security (another lateral-movement path)
- Topic 9: Authorization & RBAC (controlling API access, not network)
- Browse the whole Kubernetes Journey