Default: everything talks to everything
Out of the box the pod network is flat. Any pod can open a connection to any pod IP in any namespace, on any port. Namespaces are not a network boundary; neither is a Service. So a compromised web pod can talk straight to the payments database. In a bank that is an audit finding waiting to happen, and NetworkPolicy is the answer the auditors expect.
What you need to know already: labels and selectors, namespaces (15.26), Services and kube-proxy's DNAT (16.1, 16.3), "refused" vs "timed out" (9.1, 16.3), CoreDNS in kube-system (16.13), CIDR notation (8.3), firewalls as "allow / deny by address and port" (Docker's iptables rules, 11.15).
What a NetworkPolicy is
A NetworkPolicy is a firewall rule for pods, written as a Kubernetes object. It picks some pods (with a label selector) and lists which traffic may reach them and which traffic they may send. Two directions:
ingress traffic INTO the selected pods (who may connect to them)
egress traffic OUT of the selected pods (what they may connect to)
Careful with the word: this "ingress" means "incoming traffic". It has nothing to do with the Ingress object of 16.19.
The rule that everyone gets wrong
A pod is isolated in a direction only when some NetworkPolicy selects it and lists that direction in policyTypes. Once isolated, only traffic that matches at least one rule of at least one policy selecting it is allowed. So:
no policy selects the pod everything allowed, both ways
a policy selects it with policyTypes [Ingress] ingress deny-by-default,
egress STILL WIDE OPEN
policyTypes [Ingress, Egress] both directions deny-by-default
several policies select it the UNION of their rules (additive -
there are no deny rules)
Policies only ever add allowances. You cannot write "deny traffic from X": you isolate, then allow what should pass. And a policy with only ingress rules does not restrict egress at all: a compromised pod can still call out.
If policyTypes is omitted, the API server fills it in: [Ingress], plus Egress if the policy has an egress section.
Default deny
The standard first policy in a namespace, default deny: select every pod, isolate both directions, allow nothing.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: np-lab
spec:
podSelector: {} # {} = every pod in the namespace
policyTypes: [Ingress, Egress]
# no rules at all = nothing allowed
kubectl describe netpol (netpol = networkpolicy) translates a policy into sentences - use it to check you wrote what you meant:
# np-lab: the NetworkPolicy mission's namespace
k describe netpol default-deny -n np-lab
Name: default-deny
Namespace: np-lab
Created on: 2026-09-22 20:00:18 +0000 UTC
Labels: <none>
Annotations: <none>
Spec:
PodSelector: <none> (Allowing the specific traffic to all pods in this namespace)
Allowing ingress traffic:
<none> (Selected pods are isolated for ingress connectivity)
Allowing egress traffic:
<none> (Selected pods are isolated for egress connectivity)
Policy Types: Ingress, Egress
PodSelector: <none> = all pods; "isolated ... <none>" = nothing allowed.
Always allow DNS next
Right after this, DNS is broken for the whole namespace: CoreDNS lives in kube-system and egress to it is now denied. The symptoms look nothing like a network policy - bad address, "unknown host", Could not resolve host - so the first rule you write after a default deny is always DNS:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: np-lab
spec:
podSelector: {}
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
Read it as: "every pod here may send to pods labelled k8s-app=kube-dns in the namespace kube-system, on port 53 UDP and TCP".
namespaceSelectorpicks namespaces by label.kubernetes.io/metadata.nameis a label the API server puts on every namespace automatically, holding its name - the reliable way to select a namespace by name.- TCP 53 matters too: large DNS answers and some resolvers fall back to TCP.
Note what the rule targets: the CoreDNS pods, not the 10.96.0.10 ClusterIP. Policies are checked on pod IPs after kube-proxy's DNAT (16.3), so a rule naming a ClusterIP does nothing useful.
Rules in full
spec:
podSelector:
matchLabels: {tier: backend} # WHO this policy protects
policyTypes: [Ingress]
ingress:
- from: # ...from any of these peers
- podSelector:
matchLabels: {tier: frontend} # pods in THIS namespace
- namespaceSelector:
matchLabels: {kubernetes.io/metadata.name: monitoring} # any pod there
- ipBlock:
cidr: 10.0.3.0/24
except: [10.0.3.66/32]
ports: # ...on any of these ports
- protocol: TCP
port: 8080 # or a NAMED container port: http
- protocol: TCP
port: 9000
endPort: 9100 # a range
The words:
peer one entry under from/to: who is on the other end
podSelector (as a peer) pods with these labels, in the policy's own namespace
namespaceSelector (as a peer) every pod in namespaces with these labels
ipBlock addresses outside the cluster, as a CIDR (8.3), minus except
ports which destination ports; endPort makes a range
How they combine:
- Inside one rule,
fromandportsare ANDed (this peer AND this port). - Items of
fromare ORed (any one of the peers). - Rules of
ingressare ORed. - Omitting
frommeans "from anyone"; omittingportsmeans "any port". - Egress mirrors all of it with
to.
What is not affected:
- Replies: policies are stateful (like conntrack, 16.3). Allow A -> B and B's answers come back.
- The node's own traffic to its pods (the kubelet's health checks) - always allowed.
- hostNetwork pods (16.13) cannot be selected; their traffic looks like the node's.
- A pod talking to itself.
Testing
Blocked traffic is dropped, not refused: the client hangs until its timeout. Always test with an explicit short timeout, or you wait two minutes per try:
# np-lab: the NetworkPolicy mission's namespace
k exec -n np-lab toolbox -- nc -zv -w 2 db 6379
nc: db (10.96.21.4:6379): Connection timed out
k exec -n np-lab deploy/web -- /agnhost connect api:80 --timeout=2s
TIMEOUT
k exec -n np-lab toolbox -- wget -qO- -T 2 api/hostname
wget: download timed out
nc -zv -w 2 (16.1): connect-only, verbose, 2-second limit. agnhost connect HOST:PORT is what Kubernetes' own tests use to check policies: silent on success, TIMEOUT or REFUSED otherwise.
"Timed out" where you expected "refused" is the fingerprint of a policy drop. There is no kubectl command that says "this packet was dropped by policy X"; some network plugins (below) add their own tools for that.
Who enforces it
The API server only stores NetworkPolicies. The CNI plugin enforces them. The CNI plugin is the network add-on that gives pods their IPs and connects them (15.7; 16.35 goes deeper). Calico (this cluster) and Cilium enforce policies. Flannel does not: on a Flannel cluster every policy is accepted, listed, described - and completely ignored. No error, no warning.
So before trusting a policy, prove it with a connection that should fail. On clusters from a cloud provider, policy enforcement is often an option you choose when creating the cluster.
What you can now do:
- write a default-deny policy, and the DNS rule that must follow it
- read a policy's peers and ports, and how they AND and OR
- prove a policy works with a connection that should time out