
NetworkPolicies control which Pods may talk to each other and to the outside world at the IP and port level (L3/L4). By default, Kubernetes applies no restriction: every Pod talks freely to every other. NetworkPolicies let you partition those flows following the principle of least privilege.
This guide covers creating ingress and egress policies, critical cases such as DNS, and the debugging techniques you cannot do without.
What you will learn
- Understand the model: selection by labels, union of rules, source and destination logic
- Create policies: default deny, targeted allow, filtering by namespace
- Handle egress: keep DNS working, allow the flows you need
- Debug: diagnose why a flow is blocked or allowed
What are NetworkPolicies for?
When no network policy applies, Pods communicate freely with each other, including from one namespace to another. That is Kubernetes' permissive default behaviour, and it is exactly what a NetworkPolicy restricts.
NetworkPolicies let you:
- Restrict incoming connections to a Pod (ingress)
- Restrict outgoing connections from a Pod (egress)
- Isolate environments (production, staging, test)
- Limit lateral spread when a Pod is compromised
A word on the level at which all this operates: NetworkPolicies filter at layers 3 and 4, addresses and ports. They know nothing about HTTP paths, headers or methods. Application-level filtering of that kind belongs to a service mesh or to a CNI offering its own extensions, not to the standard object.
Prerequisite: a compatible CNI
Kubernetes defines NetworkPolicies, but does not enforce them itself. The network plugin (CNI) has to implement that feature.
Before writing NetworkPolicies, check in the documentation of your network plugin that it does enforce Kubernetes policies.
Among the implementations commonly used that support NetworkPolicies:
| CNI | NetworkPolicy support | Notes |
|---|---|---|
| Calico | Yes, full | Extended policies available |
| Cilium | Yes, full | Optional L7 filtering |
| Antrea | Yes, full | VMware/vSphere oriented |
| Weave Net | Yes, full | Original repository archived in June 2024, only a community fork is maintained |
| Flannel | No | "For network policy, other projects such as Calico can be used" (upstream README) |
| kindnet | Yes | The default CNI of kind, the cluster used throughout this course |
Check the installed CNI:
kubectl get pods -n kube-system -l k8s-app=calico-nodekubectl get pods -n kube-system -l k8s-app=ciliumGood news if you follow this course: a kind cluster is suitable. Its
CNI, kindnet, does enforce NetworkPolicies, which was not always the case and
remains little known. Checked on kind v0.33 with kindest/node:v1.37.0: a
request that succeeds before the policy times out after a default deny ingress, and a namespaceSelector does filter on the source namespace. The
egress behaviour has not been established on this cluster, the check having
failed for a reason unrelated to policies: treat it as still to be confirmed.
The k3s and RKE2 distributions ship Flannel, which could suggest that
policies are ignored there. That is wrong: k3s adds its own NetworkPolicy
controller, active by default, which you disable with
--disable-network-policy.
How does the model work?
Three ideas are enough to read any NetworkPolicy, and you meet them in this order: who the rule applies to, what its mere existence changes, and how several rules combine. The second one is the most counter-intuitive, and it is the one producing the most mistakes.
Selection by labels
A NetworkPolicy targets Pods through a podSelector based on their labels.
Without a valid selection, the policy applies to no Pod.
spec: podSelector: matchLabels: app: backendThis policy only applies to Pods carrying the app: backend label in the
namespace where it is deployed.
Selecting every Pod of a namespace:
spec: podSelector: {}Ingress and Egress
These two directions are seen from the Pod selected by the policy, never
from the network. Ingress means what enters this Pod, Egress what leaves
it. The most frequent confusion comes from the word "Ingress", which also names
a Kubernetes resource for HTTP routing: the two are unrelated. Remember too
that restricting egress is much riskier than restricting ingress, because it is
the direction that carries DNS.
| Direction | Controls | When to restrict |
|---|---|---|
| Ingress | Incoming connections to the Pod | Limit who can reach a service |
| Egress | Outgoing connections from the Pod | Limit where a Pod can connect |
Isolation is decided per direction, independently. A policyTypes that only mentions Ingress leaves outgoing traffic entirely free, and the other way round. It is a convenient property, but it explains many surprises: believing you closed everything when you closed only one direction.
Declare policyTypes explicitly to avoid ambiguity:
spec: policyTypes: - Ingress - EgressUnion of rules
NetworkPolicies do not replace one another. Allowed connections are the union of all the rules applying to the Pod for the direction concerned.
Example: if two policies allow different sources to the same Pod, both sources are allowed.
Policy A: allows frontend → backendPolicy B: allows monitoring → backendResult: frontend AND monitoring can reach backendSource plus destination logic
For a connection to work, it must be allowed on both sides when restrictions exist:
- the egress of the source Pod must allow leaving towards the destination;
- the ingress of the destination Pod must allow entering from the source.
Pod A (restricted egress) → Pod B (restricted ingress)
Works if: - A's egress policy allows B - B's ingress policy allows A
Fails if: - either policy lacks the permissionIt is a fundamental point for troubleshooting: always check the policies on both sides.
First case: allowing frontend to backend
Let us start with a classic case: only frontend Pods can reach backend
Pods on port 8080.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-frontend-to-backend namespace: defaultspec: podSelector: matchLabels: app: backend policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: frontend ports: - protocol: TCP port: 8080What this policy does:
- Selects the
app: backendPods in thedefaultnamespace - Restricts their ingress traffic (incoming traffic becomes controlled)
- Only allows connections from
app: frontendPods - Limits them to TCP port 8080
A policy's port is the container's, not the Service's
This is the number one NetworkPolicy trap, and it deserves its own section
because it does not show on review. The port field of a rule means the port
the container listens on, in other words the Service's targetPort, never
the port published by the Service.
Take a Service exposing 8080 towards a container listening on 80. Writing
port: 8080 in the policy blocks all traffic: address translation has
already happened when the packet reaches the Pod, and the port the policy sees
is 80. The symptom is particularly misleading, since the policy looks right
and nothing gets through, with no error message anywhere.
The check is a single command, to run before writing the rule:
kubectl get svc backend -o jsonpath='{.spec.ports[0].targetPort}'Apply and test:
kubectl apply -f allow-frontend-to-backend.yaml
# From a frontend Podkubectl exec -it deploy/frontend -- curl backend:8080# Result: connection succeeds
# From another Podkubectl exec -it deploy/other-app -- curl backend:8080# Result: blockedThe exact form of the block depends on the CNI, do not rely on a single
symptom. A CNI that sets a DROP rule produces a timeout; the controller
embedded in k3s returns an immediate connection refused. In both cases the
connection fails, but a "connection refused" does not mean your policy is out
of the picture.
How do you deny all ingress by default?
To fully isolate a namespace or a group of Pods, start by blocking all incoming traffic.
For a whole namespace
The manifest below is the shortest in the guide and one of the heaviest in
consequences. Two details make it blocking: podSelector: {} which, far from
selecting nothing, selects every Pod of the namespace, and the total
absence of an ingress key. A policy without rules allows nothing, and that
is what turns a mere declaration into a full block. Apply it on a test namespace
before production: it immediately cuts access to running applications.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-ingress namespace: productionspec: podSelector: {} # Every Pod of the namespace policyTypes: - Ingress # No ingress section = nothing is allowedThis policy:
- selects every Pod of the
productionnamespace; - declares that it controls ingress;
- allows no incoming connection (no
ingressrule).
The target posture is always the same: a default deny, then explicit allows. Forbidding case by case always leaves a path you had not foreseen, and above all says nothing about what you forgot.
The sequence to get there depends on the ground, and that is where production breaks. On a new namespace, set the default deny first: nothing runs yet, each flow opens as you go. On an existing namespace, the order reverses: inventory the real flows, write and apply the allows, check them, DNS resolution included, and only then set the default deny. Starting with the deny on a running application means cutting it off while you discover what it was calling.
For a specific group of Pods
This variant limits the block to Pods carrying a precise label, which lets you
isolate a sensitive component without freezing the whole namespace. Note the
difference in writing with the previous example: here the ingress: [] key is
present but empty, whereas above it was absent. Both forms produce the same
result, no allowed source; ingress: [] simply has the merit of making the
intent explicit for the next reader of the repository.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: deny-ingress-backend namespace: defaultspec: podSelector: matchLabels: app: backend policyTypes: - Ingress ingress: []How do you deny all egress by default?
Restricting outgoing connections matters just as much, but needs more care because it can break critical features.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-egress namespace: productionspec: podSelector: {} policyTypes: - Egress # No egress section = nothing is allowedAllowing DNS
It is the first reflex as soon as you restrict egress. Without DNS, your
applications can no longer resolve my-service.namespace.svc.cluster.local.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-dns-egress namespace: productionspec: podSelector: {} # Every Pod of the namespace policyTypes: - Egress egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53 - protocol: TCP port: 53Simpler alternative (less restrictive):
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-dns-egress-simple namespace: productionspec: podSelector: {} policyTypes: - Egress egress: - ports: - protocol: UDP port: 53 - protocol: TCP port: 53This version allows port 53 towards any destination. More permissive, but simpler to maintain.
The kube-system namespace automatically carries the kubernetes.io/metadata.name: kube-system label, set by Kubernetes itself. That is what makes the DNS rule above portable from one cluster to another, without assuming an administrator thought of labelling anything.
Full example: controlled egress
Here is a policy allowing a Pod to:
- resolve DNS;
- reach an internal PostgreSQL database;
- nothing else.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: backend-egress-controlled namespace: productionspec: podSelector: matchLabels: app: backend policyTypes: - Egress egress: # DNS - ports: - protocol: UDP port: 53 - protocol: TCP port: 53 # Internal PostgreSQL - to: - podSelector: matchLabels: app: postgresql ports: - protocol: TCP port: 5432Selecting by namespace
To allow flows between namespaces, use namespaceSelector.
Allowing from a specific namespace
The point to watch here is that namespaceSelector selects namespace
labels, not a name. Writing name: monitoring only works if the namespace
really carries that label, which is not automatic. Kubernetes does set a
kubernetes.io/metadata.name label on every namespace since 1.21, and it is
often the most reliable selector because nobody has to think of adding it.
Once this rule is applied, every Pod of the monitoring namespace can reach
the backend, on any port. To restrict to a precise Pod, you have to combine the
two selectors, which the next subsection covers.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-from-monitoring namespace: productionspec: podSelector: matchLabels: app: backend policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: name: monitoringEvery namespace receives the kubernetes.io/metadata.name label automatically, whose value is its own name. A namespaceSelector can therefore always designate a precise namespace, even if nobody ever added a label to it. Measured on the course cluster: the filtering lets the allowed namespace through and blocks the others.
Combining namespace and pod selectors
To target specific Pods in a specific namespace:
ingress: - from: - namespaceSelector: matchLabels: name: monitoring podSelector: matchLabels: app: prometheusWatch the syntax: both selectors in the same - item mean logical
AND. Separated, it is logical OR:
# Logical AND: namespace monitoring AND pod prometheus- from: - namespaceSelector: matchLabels: name: monitoring podSelector: matchLabels: app: prometheus
# Logical OR: namespace monitoring OR pod prometheus (in the same namespace)- from: - namespaceSelector: matchLabels: name: monitoring - podSelector: matchLabels: app: prometheusUsing ipBlock
ipBlock lets you allow IP address ranges, typically for systems outside
the cluster.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-external-api namespace: productionspec: podSelector: matchLabels: app: backend policyTypes: - Egress egress: - to: - ipBlock: cidr: 203.0.113.0/24 except: - 203.0.113.128/25 ports: - protocol: TCP port: 443Two limits of ipBlock are worth knowing before using it. It reasons in IP addresses, so it knows nothing about pods, whose addresses change on every re-creation: keep it for destinations outside the cluster. And depending on the CNI, the source address observed may be the node's rather than the sending pod's, which makes rules based on internal ranges unreliable.
Protecting the node's metadata endpoint
This is the most important use of except, and it is expected at the CKS,
whose Cluster Setup domain asks you to "protect node metadata and endpoints".
At a cloud provider, the node queries a metadata service on the link-local
address 169.254.169.254, and that service often hands out access
credentials. A compromised Pod that reaches it inherits the node's rights: an
application intrusion becomes an infrastructure compromise.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: block-metadata namespace: productionspec: podSelector: {} policyTypes: ["Egress"] egress: - to: - ipBlock: cidr: 0.0.0.0/0 except: - 169.254.169.254/32 - ports: - port: 53 protocol: UDPThe DNS rule has to be added separately, and it is the omission that wastes
the most time: as soon as an egress block exists, everything not listed in it
is refused, name resolution included. A Pod that no longer resolves anything
after a policy is applied is almost always a victim of this.
Checked on the course cluster: with this policy, a Pod reaches the other Pods normally and fails towards the excluded address.
What NetworkPolicies do not do
To avoid false expectations, here is what standard NetworkPolicies cannot do:
| Feature | NetworkPolicy | Alternative |
|---|---|---|
| Filter by HTTP URL or path | No | Service mesh (Istio, Linkerd) |
| Mutual authentication (mTLS) | No | Service mesh |
| Filter by HTTP method (GET, POST) | No | Service mesh, API Gateway |
| Payload inspection | No | WAF |
| Logging blocked connections | No | Specific CNI (Cilium, Calico) |
| Cross-cluster policies | No | Multi-cluster solutions |
NetworkPolicies are a first level of network segmentation, not a complete application security solution.
How do you debug a NetworkPolicy?
When a flow is blocked (or is not when it should be), follow this method:
-
List the namespace's policies
Terminal window kubectl get networkpolicy -n production -
Examine a policy in detail
Terminal window kubectl describe networkpolicy allow-frontend-to-backend -n productionThe output answers three questions, and you have to ask them in this order: does the rule target this Pod, and only then, what does it allow?
PodSelector: which Pods are targeted?Allowing ingress traffic: which sources are allowed?Allowing egress traffic: which destinations are allowed?
-
Check the Pods' labels
Terminal window kubectl get pods -n production --show-labelskubectl get pods -n production -l app=backendA Pod without the right label is not selected by the policy.
-
Test connectivity
Terminal window # From the source Podkubectl exec -it deploy/frontend -n production -- nc -vz backend 8080# DNS testkubectl exec -it deploy/frontend -n production -- nslookup backend -
Check both sides
If the source's egress AND the destination's ingress are restricted, check that both allow the flow.
Terminal window # Policies selecting the source Pod (egress)kubectl get networkpolicy -n production -o json | \jq '.items[] | select(.spec.podSelector.matchLabels.app=="frontend")'# Policies selecting the destination Pod (ingress)kubectl get networkpolicy -n production -o json | \jq '.items[] | select(.spec.podSelector.matchLabels.app=="backend")'
Advanced debug tools
A point of method before running these commands: a debug Pod created with
kubectl run carries different labels from your application, so the
policies protecting it do not apply to it. You risk concluding "the flow
works" when you simply tested from a Pod that is not concerned. For a
representative test, add the application's labels with
--labels="app=frontend", or use kubectl debug to attach an ephemeral
container to the real Pod.
# Debug Pod with network toolskubectl run netshoot --rm -it --image=nicolaka/netshoot:v0.16@sha256:b09d9b21381f47a79b3cbcb30da25266dc17186ea00ae65e99fdc51396f48e70 -- bash
# Inside the netshoot podnslookup backend.production.svc.cluster.localcurl -v backend.production:8080nc -vz backend.production 8080
# Check whether the CNI logs drops (Cilium)kubectl logs -n kube-system -l k8s-app=cilium -c cilium-agent | grep -i dropCommon problems
Read this table by telling two families of failures apart. The first four rows
describe a flow wrongly blocked: the cause is almost always a missing
permission or a label that does not match, and you will find it by comparing
kubectl describe networkpolicy with the Pods' real labels. The last row
describes the opposite, a flow wrongly allowed, and it is diagnosed
differently: it is not about finding a mistake in one policy, but about listing
all those selecting the Pod, since their permissions add up.
| Symptom | Likely cause | Solution |
|---|---|---|
| Timeout towards a service | Missing ingress policy on the destination side | Add a from rule |
| DNS no longer resolves | Egress deny without a DNS exception | Allow port 53 |
| Policy ignored | Incompatible CNI | Check NetworkPolicy support |
| Pod not selected | Wrong labels | Check --show-labels |
| Unexpected allowed flow | Union of several policies | List all applicable policies |
Documenting your NetworkPolicies
To keep policies readable over time:
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-frontend-to-backend namespace: production annotations: description: "Allows the frontend to reach the backend on the API port" owner: "team-platform" last-review: "2026-03-22"spec: # ...A NetworkPolicy has no description field. The only way to explain a rule to whoever reads it in six months is therefore an annotation, and it is a discipline that pays off: a policy reads badly, and nobody dares delete a rule whose purpose they do not know.
What you need for the CKAD
The exam being timed, the stake is not knowing every option but producing a
correct manifest without thinking about its structure. The five moves below
are the ones that come back; practise writing them from memory, then checking
them with kubectl describe. A useful reminder on the day:
kubectl explain networkpolicy.spec --recursive is available during the exam
and saves you from guessing how fields nest.
-
Read an existing policy
Quickly identify which Pods are selected (
podSelector), which traffic is controlled (policyTypes), which rules apply. -
Create a default deny ingress
podSelector: {}policyTypes: [Ingress]# No ingress section -
Allow a specific flow
Frontend to backend, port 8080:
ingress:- from:- podSelector:matchLabels:app: frontendports:- port: 8080 -
Allow DNS in egress
As soon as you restrict egress, allow UDP/TCP 53.
-
Diagnose a block
kubectl get networkpolicy -n <ns>kubectl describe networkpolicy <name>kubectl get pods --show-labelskubectl exec ... -- nc -vz <service> <port>
Key points
- NetworkPolicies operate at L3/L4 (IP and port), not HTTP
- The CNI must support policy enforcement, otherwise they are ignored
- A Pod becomes isolated (ingress or egress) as soon as a policy selects it
- Allowed rules are the union of applicable policies
- A connection must be allowed on both sides (source egress plus destination ingress)
- As soon as egress is restricted, think of DNS (port 53)
ipBlockis for stable external IPs, not for targeting Pods- Declare
policyTypesexplicitly to avoid ambiguity
Test your knowledge
The threshold is deliberately high at 90%: on this topic, a misunderstood rule turns into an application cut off in production or, worse, an isolation you believe active when it is not. The two points that trip people up most are the union of policies and the need to allow the flow on both sides.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Falco - Runtime threat detection in Kubernetes
A new full quiz with random questions
Work again only on the questions you missed
Put it into practice
Three levels of isolation, all validated by real connections and not by reading the YAML. You first isolate a database so that only its backend gets in, then partition three tiers in ingress and egress, and finish with the full default deny, the one that silently breaks DNS resolution until you reopen it.