Network Policies Basics
A NetworkPolicy is a namespaced firewall for pods, selecting them by label and allowing specific ingress and egress. This page walks the core patterns from allow-all to targeted rules.
Prerequisites
- A Kubernetes 1.36.2 cluster with a policy-enforcing CNI (Calico or Cilium; AWS VPC CNI needs its network policy agent enabled).
kubectlconfigured for the cluster.- A test namespace and two workloads to observe traffic between.
Quick setup for a scratch namespace:
kubectl create namespace demo
kubectl label namespace demo team=demo
kubectl -n demo run web --image=nginx --labels="app=web" --port=80
kubectl -n demo run client --image=nicolaka/netshoot --command -- sleep infinityBasic Examples
1. Verify enforcement first
Before writing policy, confirm your CNI actually enforces it - a policy on a non-enforcing CNI is silently ignored.
kubectl -n demo exec client -- curl -sS --max-time 3 http://web- A successful response confirms open-by-default connectivity.
- If this hangs after you apply a deny policy later, enforcement works.
- Calico and Cilium enforce natively; check your CNI docs otherwise.
- Keep this
clientpod around as your probe throughout.
2. The empty policyTypes and pod selector
A NetworkPolicy always has a podSelector that picks which pods it governs in its namespace.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: web-policy
namespace: demo
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- IngresspodSelectorselects target pods by label within the same namespace.- An empty
podSelector: {}would select every pod in the namespace. policyTypesdeclares which directions this policy governs.- Listing
Ingresswith no ingress rules means deny all ingress toweb.
3. Default-deny all ingress
Selecting every pod with no rules is the canonical namespace ingress lockdown.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: demo
spec:
podSelector: {}
policyTypes:
- IngresspodSelector: {}matches all pods indemo.- No
ingress:block means no traffic is allowed in. - Egress is untouched, so pods can still make outbound calls.
- This is the foundation you build allow-rules on top of.
4. Allow ingress from a specific pod
Now poke a hole: let only pods labeled app=client reach web.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-client-to-web
namespace: demo
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: client
ports:
- protocol: TCP
port: 80- Traffic is allowed only from pods labeled
app=clientin the same namespace. - The
portslist restricts the allowed destination port to 80/TCP. - Policies are additive: this one unions with any other allow rules for
web. - Anything not matched by an allow rule stays denied.
5. Allow ingress from another namespace
Cross-namespace rules use namespaceSelector, matching on namespace labels.
ingress:
- from:
- namespaceSelector:
matchLabels:
team: platform
ports:
- protocol: TCP
port: 80namespaceSelectormatches namespaces by their labels, not their names.- Label your namespaces (
kubectl label ns platform team=platform) to target them. - This allows all pods in matching namespaces on port 80.
- Combine with a
podSelectorin the samefromentry to narrow further.
6. AND vs OR in from rules
The structure of from decides whether selectors combine with AND or OR - a classic gotcha.
ingress:
- from:
- namespaceSelector:
matchLabels:
team: platform
podSelector:
matchLabels:
app: api- Two selectors inside one
fromelement are ANDed: pods labeledapp=apiinteam=platformnamespaces. - Listing them as two separate
-items would OR them instead. - Getting this wrong is the most common policy mistake.
- Always re-read the indentation when a rule seems too broad.
7. Default-deny egress
Locking egress is symmetric, but be careful: it will break DNS unless you allow it.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: demo
spec:
podSelector: {}
policyTypes:
- Egress- With egress denied, pods cannot even resolve names via CoreDNS.
- You must add an egress allow for UDP/TCP 53 to
kube-systemDNS. - Test with the
clientprobe after applying to see what breaks. - Egress lockdown is powerful but requires deliberate allow-listing.
Intermediate Examples
8. Allow egress to CoreDNS
The mandatory companion to any egress lockdown - permit DNS so name resolution survives.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: demo
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53- The built-in
kubernetes.io/metadata.namelabel reliably selectskube-system. - Both UDP and TCP 53 are needed; large responses fall back to TCP.
- Pair this with a targeted egress allow to your actual dependencies.
- Without it, every DNS lookup times out and services appear "down".
9. Combine ingress and egress in one policy
A single policy can govern both directions when you list both policyTypes.
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: client
egress:
- to:
- podSelector:
matchLabels:
app: db
ports:
- protocol: TCP
port: 5432webaccepts ingress only fromclientand may only egress todbon 5432.- Both directions are explicit, so unmatched traffic is denied.
- Keep DNS egress allowed separately or add it here too.
- This pattern models a tidy three-tier allow-list per workload.
10. Inspect and troubleshoot
Once policies exist, verify what applies to a pod and confirm behavior.
kubectl -n demo get networkpolicy
kubectl -n demo describe networkpolicy allow-client-to-web
kubectl -n demo exec client -- curl -sS --max-time 3 http://webdescribeshows the resolved selectors and rules for each policy.- Re-run the
curlprobe to confirm allowed paths pass and others hang. - A hang (not a refusal) is the typical signature of a policy drop.
- Use your CNI's flow logs (Calico or Cilium Hubble) for deeper tracing.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).