Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Kubernetes NetworkPolicies: isolating traffic between Pods

By updated

Improve this page

kubernetes logo

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:

CNINetworkPolicy supportNotes
CalicoYes, fullExtended policies available
CiliumYes, fullOptional L7 filtering
AntreaYes, fullVMware/vSphere oriented
Weave NetYes, fullOriginal repository archived in June 2024, only a community fork is maintained
FlannelNo"For network policy, other projects such as Calico can be used" (upstream README)
kindnetYesThe default CNI of kind, the cluster used throughout this course

Check the installed CNI:

Terminal window
kubectl get pods -n kube-system -l k8s-app=calico-node
kubectl get pods -n kube-system -l k8s-app=cilium

Good 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: backend

This 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.

DirectionControlsWhen to restrict
IngressIncoming connections to the PodLimit who can reach a service
EgressOutgoing connections from the PodLimit 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
- Egress

Union 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 → backend
Policy B: allows monitoring → backend
Result: frontend AND monitoring can reach backend

Source 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 permission

It 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/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: default
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080

What this policy does:

  1. Selects the app: backend Pods in the default namespace
  2. Restricts their ingress traffic (incoming traffic becomes controlled)
  3. Only allows connections from app: frontend Pods
  4. 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:

Terminal window
kubectl get svc backend -o jsonpath='{.spec.ports[0].targetPort}'

Apply and test:

Terminal window
kubectl apply -f allow-frontend-to-backend.yaml
# From a frontend Pod
kubectl exec -it deploy/frontend -- curl backend:8080
# Result: connection succeeds
# From another Pod
kubectl exec -it deploy/other-app -- curl backend:8080
# Result: blocked

The 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/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {} # Every Pod of the namespace
policyTypes:
- Ingress
# No ingress section = nothing is allowed

This policy:

  • selects every Pod of the production namespace;
  • declares that it controls ingress;
  • allows no incoming connection (no ingress rule).

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/v1
kind: NetworkPolicy
metadata:
name: deny-ingress-backend
namespace: default
spec:
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/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
# No egress section = nothing is allowed

Allowing 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/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: production
spec:
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: 53

Simpler alternative (less restrictive):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress-simple
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53

This 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/v1
kind: NetworkPolicy
metadata:
name: backend-egress-controlled
namespace: production
spec:
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: 5432

Selecting 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/v1
kind: NetworkPolicy
metadata:
name: allow-from-monitoring
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: monitoring

Every 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: prometheus

Watch 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: prometheus

Using ipBlock

ipBlock lets you allow IP address ranges, typically for systems outside the cluster.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-api
namespace: production
spec:
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: 443

Two 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.

block-metadata.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-metadata
namespace: production
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
- ports:
- port: 53
protocol: UDP

The 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:

FeatureNetworkPolicyAlternative
Filter by HTTP URL or pathNoService mesh (Istio, Linkerd)
Mutual authentication (mTLS)NoService mesh
Filter by HTTP method (GET, POST)NoService mesh, API Gateway
Payload inspectionNoWAF
Logging blocked connectionsNoSpecific CNI (Cilium, Calico)
Cross-cluster policiesNoMulti-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:

  1. List the namespace's policies

    Terminal window
    kubectl get networkpolicy -n production
  2. Examine a policy in detail

    Terminal window
    kubectl describe networkpolicy allow-frontend-to-backend -n production

    The 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?
  3. Check the Pods' labels

    Terminal window
    kubectl get pods -n production --show-labels
    kubectl get pods -n production -l app=backend

    A Pod without the right label is not selected by the policy.

  4. Test connectivity

    Terminal window
    # From the source Pod
    kubectl exec -it deploy/frontend -n production -- nc -vz backend 8080
    # DNS test
    kubectl exec -it deploy/frontend -n production -- nslookup backend
  5. 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.

Terminal window
# Debug Pod with network tools
kubectl run netshoot --rm -it --image=nicolaka/netshoot:v0.16@sha256:b09d9b21381f47a79b3cbcb30da25266dc17186ea00ae65e99fdc51396f48e70 -- bash
# Inside the netshoot pod
nslookup backend.production.svc.cluster.local
curl -v backend.production:8080
nc -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 drop

Common 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.

SymptomLikely causeSolution
Timeout towards a serviceMissing ingress policy on the destination sideAdd a from rule
DNS no longer resolvesEgress deny without a DNS exceptionAllow port 53
Policy ignoredIncompatible CNICheck NetworkPolicy support
Pod not selectedWrong labelsCheck --show-labels
Unexpected allowed flowUnion of several policiesList all applicable policies

Documenting your NetworkPolicies

To keep policies readable over time:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
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.

  1. Read an existing policy

    Quickly identify which Pods are selected (podSelector), which traffic is controlled (policyTypes), which rules apply.

  2. Create a default deny ingress

    podSelector: {}
    policyTypes: [Ingress]
    # No ingress section
  3. Allow a specific flow

    Frontend to backend, port 8080:

    ingress:
    - from:
    - podSelector:
    matchLabels:
    app: frontend
    ports:
    - port: 8080
  4. Allow DNS in egress

    As soon as you restrict egress, allow UDP/TCP 53.

  5. Diagnose a block

    • kubectl get networkpolicy -n <ns>
    • kubectl describe networkpolicy <name>
    • kubectl get pods --show-labels
    • kubectl exec ... -- nc -vz <service> <port>

Key points

  1. NetworkPolicies operate at L3/L4 (IP and port), not HTTP
  2. The CNI must support policy enforcement, otherwise they are ignored
  3. A Pod becomes isolated (ingress or egress) as soon as a policy selects it
  4. Allowed rules are the union of applicable policies
  5. A connection must be allowed on both sides (source egress plus destination ingress)
  6. As soon as egress is restricted, think of DNS (port 53)
  7. ipBlock is for stable external IPs, not for targeting Pods
  8. Declare policyTypes explicitly 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

8 questions
6 min
90% to pass

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

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.

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn