NetworkPolicy
There is a property of Kubernetes that surprises everyone arriving from the world of traditional networks: any Pod can talk to any Pod. No firewalls, no VLANs, no access lists. A compromised Pod in the dumbest corner of the cluster can open a connection to your database, because the flat network is the premise everything else is built on.
The NetworkPolicy is the tool for fencing off that open field. And it works with the same logic as RBAC: nothing changes until the first policy shows up, and from that moment on, whatever is not allowed is forbidden.
Work from the dev-machine tab.
Step 1: The wide-open scenario
Three pieces: an api that needs protecting, a web that has the right to call it and an intruso (Spanish for "intruder") that does not. Create escenario.yaml:
cat << 'EOF' > escenario.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: nginx
image: ghcr.io/iximiuz/labs/nginx:alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
ports:
- port: 80
targetPort: 80
---
apiVersion: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
containers:
- name: cliente
image: ghcr.io/iximiuz/labs/nginx:alpine
command: ["sh", "-c", "sleep infinity"]
---
apiVersion: v1
kind: Pod
metadata:
name: intruso
labels:
app: intruso
spec:
containers:
- name: cliente
image: ghcr.io/iximiuz/labs/nginx:alpine
command: ["sh", "-c", "sleep infinity"]
EOF
Apply it and check the starting situation, which is the one that keeps you up at night:
kubectl apply -f escenario.yaml
kubectl wait --for=condition=Available deployment/api --timeout=60s
kubectl wait --for=condition=Ready pod/web pod/intruso --timeout=60s
kubectl exec web -- wget -qO- -T 3 http://api | head -4
kubectl exec intruso -- wget -qO- -T 3 http://api | head -4
Both get in. The intruso has no permission, no RBAC, no relationship with the api, and yet it reaches it without breaking a sweat. Nobody allowed it: it is just that nobody forbade it.
Step 2: Total denial
The first policy of any serious namespace allows nothing: it forbids everything. Create denegar-todo.yaml (denegar-todo means "deny-all"):
cat << 'EOF' > denegar-todo.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: denegar-todo
spec:
podSelector: {}
policyTypes:
- Ingress
EOF
The YAML, explained in questions and answers
What does an empty podSelector mean?
"All the Pods in this namespace". It is the same selection mechanism as always, but a selector with no conditions matches everything. If you put matchLabels: {app: api}, the policy would only affect the api.
Where are the rules of this policy?
There are none. And that is the trick: a NetworkPolicy that declares policyTypes: Ingress without any ingress section means "zero allowed sources". The object does not explicitly block anything; what it does is make the selected Pods become governed by policies, and from that moment on only what some policy allows is allowed.
Why only Ingress and not Egress?
Because they are two independent dimensions: Ingress controls who may get into these Pods, Egress where they may go out to. Closing the entry is the first step and the most profitable one. Closing the exit is more aggressive (it breaks even DNS if you forget to allow port 53 toward CoreDNS) and usually comes later.
Are policies additive, or do they override each other?
Additive, exactly like RBAC. Traffic is allowed if some policy allows it. There are no explicit deny rules: denial is the default state as soon as a Pod is selected by at least one policy.
Do NetworkPolicies work in any cluster?
Only if the network plugin implements them. Just as an Ingress without an Ingress controller is a decorative object, a NetworkPolicy in a cluster whose CNI does not support it gets created without errors and protects absolutely nothing. It is one of the most dangerous mirages in Kubernetes, because the object exists and gives a false sense of security. This playground does enforce them.
Apply it and repeat the two calls:
kubectl apply -f denegar-todo.yaml
kubectl exec web -- wget -qO- -T 3 http://api
kubectl exec intruso -- wget -qO- -T 3 http://api
Now both hang until the timeout runs out. Notice the detail: the traffic is not rejected, it is silently dropped. There is no clean "connection refused", there is a timeout. When in production an application starts giving odd timeouts right after a rollout, this lesson will be the first thing you remember.
Step 3: Opening only the necessary door
With the namespace closed, you open just enough. Create permitir-web.yaml (permitir-web means "allow-web"):
cat << 'EOF' > permitir-web.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: permitir-web
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: web
ports:
- protocol: TCP
port: 80
EOF
The YAML, in three questions
How many selectors are there here, and what does each one pick?
Two, and mixing them up is mistake number one. The podSelector at the top picks the destination (whom this rule protects: the api). The podSelector inside from picks the allowed source (the web). They read like this: "toward the app=api Pods, allow ingress from the app=web Pods, on TCP port 80".
Can I allow access from another namespace?
Yes, with namespaceSelector (which selects namespaces by their labels) and combining it with podSelector. Watch out for the syntax: two separate elements in the from list are an OR, while namespaceSelector and podSelector inside the same element are an AND. One dash too many or too few completely changes the meaning of the policy.
And to allow traffic from outside the cluster?
With ipBlock and a CIDR. It is the way to let in, for example, a load balancer that does not live in the Pod network.
Apply it and check both sides:
kubectl apply -f permitir-web.yaml
kubectl exec web -- wget -qO- -T 3 http://api | head -4
kubectl exec intruso -- wget -qO- -T 3 http://api
The web gets in. The intruso stays out, and now no longer by accident but by design. We have not touched the Service, nor the Deployment, nor a single line of the application: the network is an independent governance layer, governed by labels, like everything in Kubernetes.
Summary
- Without policies, the Kubernetes network is flat: everybody talks to everybody.
- A NetworkPolicy does not block, it selects: as soon as a Pod is selected by one, it is only allowed what some policy allows it.
- The professional pattern is always the same:
denegar-todofirst, then one policy per legitimate path. - Blocked traffic is silently dropped: it shows up as timeouts, not as connection errors.
- Without a CNI that implements them, NetworkPolicies are decoration. Check it before trusting them.
- Previous lesson
- Gateway API
- Next lesson
- CoreDNS and the internal DNS