Lesson 56 of 66 · 17 min read · Updated
Network Policies — Controlling Pod-to-Pod Traffic
On this page
The previous two lessons limited what a Pod may do to the API server and to its node. This lesson covers the third direction: what a Pod may do over the network.
By default, Kubernetes networking is completely flat. Every Pod can open a connection to every other Pod, in every namespace, on every port. The Services lessons relied on that: a ClusterIP Service just works, from anywhere. For security it is the worst possible starting point. If an attacker compromises your public-facing frontend, nothing stops the next request from going straight to your payments service, your database, or the metrics endpoint of every other team.
A NetworkPolicy is a firewall rule written in terms of labels instead of IP addresses. This lesson builds a small shop, closes every path, and opens exactly the ones the architecture needs.
We will cover the following topics:
- How NetworkPolicies select Pods and why they are additive allow-lists
- Who enforces them: the CNI plugin, and how to check that yours does
- Default-deny policies for ingress and egress
- Allowing traffic by
podSelector,namespaceSelector, and both together - The AND versus OR trap in the
fromandtolists - The DNS exception every egress policy needs
- Why egress rules use the Pod's port, not the Service's port
- Allowing internet access with
ipBlockwhile blocking private ranges and the node
Technical Requirements
- A Kubernetes cluster whose network plugin enforces NetworkPolicies. The outputs here come from Docker Desktop's Kubernetes with the kind provisioner (one control plane and three workers, Kubernetes v1.36). Its network plugin, kindnet, enforces NetworkPolicies, as Step 3 proves.
kubectl. No application code is needed: the servers use the smalltraefik/whoamiimage and the clients usecurlimages/curl.- The Services lessons, for Service DNS names and ports.
Core Concepts
The Rules of the Game
A NetworkPolicy has three parts: which Pods it applies to, which directions it covers, and what it allows.
Four rules explain all NetworkPolicy behaviour:
- A Pod no policy selects is wide open. Policies only affect the Pods matched by their
podSelector. An empty selector,podSelector: {}, matches every Pod in the namespace. - Once any policy selects a Pod for a direction, that direction becomes deny-by-default for the Pod. Only traffic some policy allows gets through.
- Policies are additive. There are no deny rules. If any policy allows a connection, it is allowed. Adding a policy can only open more paths, never close one.
- Return traffic is automatic. Policies are stateful. If a request is allowed in, its response is allowed out, and the other way around.
Rule 3 surprises people who are used to firewalls with ordered allow and deny rules. With NetworkPolicies you build an allow-list and let the default deny everything else.
Ingress and Egress
| Direction | Controls | Typical use |
|---|---|---|
| Ingress | Connections into the selected Pods | Only the frontend may call the orders API |
| Egress | Connections out of the selected Pods | The orders API may call only the payments API and the internet on port 443 |
A connection from Pod A to Pod B must pass both A's egress rules and B's ingress rules. With policies on both sides you have to open the path twice.
Selecting Peers
Inside from (for ingress) and to (for egress) you list peers. There are three kinds:
| Peer | Matches |
|---|---|
podSelector | Pods with these labels in the policy's own namespace |
namespaceSelector | All Pods in namespaces with these labels |
namespaceSelector + podSelector in the same list item | Pods with these labels in namespaces with these labels |
ipBlock | IP ranges, with optional exceptions. Meant for traffic leaving or entering the cluster |
Every namespace carries the automatic label kubernetes.io/metadata.name: <name>, so you can select a namespace by name without labelling it yourself.
The AND versus OR Trap
The difference between these two from blocks is a single dash, and it changes the meaning completely:
In the first form there is one list item with two selectors, and both must match. In the second there are two list items, and either may match. The second form is almost always a mistake, and it fails open. Step 6 demonstrates it.
Who Enforces NetworkPolicies
The API server only stores NetworkPolicy objects. The network plugin (CNI) enforces them, and not every plugin does. On a plugin without support, your policies are accepted, show up in kubectl get networkpolicy, and do nothing at all.
| Network plugin | Enforces NetworkPolicy |
|---|---|
| Calico, Cilium, Antrea | Yes, with extra features through their own policy CRDs |
| kindnet in current kind and Docker Desktop releases | Yes |
| Flannel on its own | No, it is usually paired with Calico (the "Canal" setup) for policies |
| Managed clouds | Depends on the options you choose: Azure CNI with Cilium or Calico on AKS, network policy enforcement on EKS and GKE |
Never assume. Apply a default-deny policy in a test namespace and check that traffic really stops, as this lesson does.
Ports Are Evaluated After the Service
NetworkPolicies see Pod-to-Pod traffic after the Service has translated the Service IP and port into a Pod IP and targetPort. A Service on port 80 that forwards to a container on port 8080 must be allowed on 8080 in both ingress and egress rules. Writing the Service port is a common mistake, and the result is a silent timeout.
Hands-On: Kubernetes Commands
List the policies in a namespace
The POD-SELECTOR column shows which Pods each policy protects; <none> means all of them.
Read a policy in plain language
describe turns the YAML into allow statements, which makes AND/OR mistakes easier to spot.
Test a connection from a specific Pod
-m 3 sets a three-second limit, so a blocked connection fails fast. Exit code 28 is curl's timeout.
Test DNS separately from HTTP
When egress is restricted, DNS is often what breaks first.
Find the labels a policy can match
Policies match labels, so check them on both the Pods and the namespaces.
Find the DNS Pods and their labels
The egress DNS rule must match these labels in kube-system.
Step-by-Step Example
The Scenario
A small shop runs in the sec-shop namespace:
- frontend is the public-facing tier. It calls the orders API.
- orders-api calls the payments API, and it needs the internet on port 443 to reach an external service.
- payments-api must only be reachable from orders-api.
A monitoring team runs metrics-scraper in the sec-monitoring namespace, and it must reach both APIs.
The target architecture, as a list of allowed paths:
| From | To | Port |
|---|---|---|
| frontend | orders-api | 8080 |
| orders-api | payments-api | 8080 |
| sec-monitoring / metrics-scraper | any Pod in sec-shop | 8080 |
| orders-api | the internet, but not private networks | 443 |
| every Pod in sec-shop | cluster DNS | 53 |
Everything else must be blocked.
Step 1 — Create the Namespaces and Workloads
The two APIs run traefik/whoami, a tiny HTTP server, on port 8080, each behind a Service on port 80. The clients run curl in a container that just sleeps. The orders-api Pod has a second toolbox container with curl, so you can test calls that originate from orders-api. Containers in a Pod share its network, so the toolbox's traffic carries the orders-api labels.
Step 2 — A Script That Tests Every Path
Testing policies by hand gets tedious. This script calls both Services from all three clients and prints a matrix. Each probe is retried once before it counts as blocked, so a single slow DNS answer cannot fake a result.
Save it as check.sh, make it executable, and run it before any policy exists:
This is the flat network. The frontend can call payments directly, and so can anything in any namespace.
Step 3 — Deny All Ingress
Start by closing every door into the namespace. The empty podSelector selects every Pod, and listing Ingress without any ingress rules allows nothing:
Everything is blocked, which proves that this cluster's network plugin enforces policies. If your matrix still shows OK everywhere, your CNI ignores NetworkPolicies and nothing in the rest of this lesson will have any effect.
Note that Pods inside the same namespace are blocked too. A NetworkPolicy does not treat "same namespace" as trusted.
Step 4 — Allow Frontend to Orders
Open the first path. The port is 8080, the container port, not the Service's port 80:
Exactly one cell changed. describe shows the policy in plain words:
Step 5 — Allow Orders to Payments
The same pattern protects the payments API:
The frontend can no longer skip a tier. Even if it is compromised, payments is one hop further away.
Step 6 — Allow the Monitoring Namespace, and Fall into the Trap
The monitoring team's scraper must reach every Pod in sec-shop. That requires a peer in another namespace, so you need a namespaceSelector. Write it correctly first, with both selectors in one list item:
Now start some other tool in the monitoring namespace, one that should not have access:
Blocked, as intended. Now apply the version with one extra dash before podSelector, the OR form:
The random tool is in. The policy now admits every Pod in sec-monitoring, plus any Pod labelled app=metrics-scraper in sec-shop. Nothing in kubectl apply warned you. Restore the correct version before going on:
Step 7 — Deny All Egress, and Break DNS
Ingress control stops attackers from getting in. Egress control stops a compromised Pod from reaching out, whether to scan the rest of the cluster or to send data to the internet. Deny all egress in the namespace:
The scraper still works: it lives in another namespace, and the egress policy applies only to Pods in sec-shop. The shop's own calls fail, and not only because of HTTP:
The frontend cannot even ask DNS where orders-api is. DNS is an egress connection to the CoreDNS Pods in kube-system, on port 53, and default-deny egress blocks it.
Step 8 — Allow DNS
Allow every Pod in the namespace to reach the cluster DNS Pods, over UDP and TCP, since DNS falls back to TCP for large answers:
Name resolution works again. Every egress default-deny you write should come with this policy. The CoreDNS Pods carry the label k8s-app: kube-dns in most distributions, but check yours with kubectl get pods -n kube-system --show-labels.
Step 9 — Allow the Application Paths, with the Right Port
The frontend needs egress to orders-api. Write it with the Service port first, as many people do:
Still blocked. The frontend connects to the Service on port 80, but by the time the packet is checked it has been rewritten to the Pod's IP and port 8080. Fix the frontend's rule and add the orders-api egress rule at the same time:
This matrix matches the architecture table exactly, now with both directions controlled.
Step 10 — Allow the Internet, but Not the Inside
orders-api must call an external service over HTTPS. Right now it cannot:
External services have IP addresses you do not control, so use an ipBlock. Allow everything on port 443 except private ranges and the link-local range. That keeps the rule from opening a back door to other Pods, the nodes, or a cloud metadata endpoint at 169.254.169.254:
The internet works on 443. The node, at 172.18.0.5 on this cluster, is inside an excluded range, so the kubelet port stays closed, and the frontend, which has no internet rule, stays offline:
ipBlock is for addresses outside the cluster. Do not use it to match Pod IPs: Pods are replaced and get new addresses, which is exactly why labels exist. If you need to allow a fixed set of external hosts by name rather than by IP range, plain NetworkPolicy cannot do it. Cilium and Calico add DNS-based egress rules through their own policy types, and an egress gateway or proxy is the other common solution.
The namespace now has nine small policies:
Small policies with one purpose each are easier to review than one large policy, and because policies are additive, splitting them changes nothing about their combined effect.
Step 11 — Clean Up
Summary
- Kubernetes networking is flat by default: every Pod can reach every Pod. NetworkPolicies turn that into an allow-list.
- A Pod selected by any policy for a direction is deny-by-default in that direction. Policies are additive and have no deny rules; responses are allowed automatically.
- The network plugin enforces policies. Prove it with a default-deny test. kindnet, Calico and Cilium enforce; Flannel alone does not.
- Start every namespace with default-deny ingress and egress, then add one small allow policy per path.
- Every egress default-deny needs a DNS exception to
k8s-app: kube-dnsinkube-systemon UDP and TCP 53. - Rules match the Pod's port (
targetPort), not the Service port. namespaceSelectorandpodSelectorin one list item mean AND; as separate items they mean OR, which fails open.- Use
ipBlockwithexceptfor internet egress, excluding private ranges and169.254.0.0/16.
In the next lesson you will enforce rules about the objects themselves, such as required labels or forbidden image tags, with ValidatingAdmissionPolicy.