OnCallReady

Lesson 16.29 · Kubernetes: Networking & Storage · 16 min read

NetworkPolicy: default allow, isolation, and the rules

In plain words

Imagine a school where, by default, every classroom door is wide open and anyone can walk in anywhere. The headteacher can put a guard on a door. The moment a door has a guard, the guard only lets in people who are on a list; everyone else is turned away quietly. The lists can only say who is allowed, never who is forbidden. And a guard at the entrance of a room doesn't stop anyone inside from walking out, unless the room also gets a guard for leaving.

That's NetworkPolicy. A pod with no policy selecting it accepts and sends anything. Once a policy selects it for Ingress or Egress (policyTypes), only listed traffic passes, and rules only add allowances. The CNI (Calico here) is the one actually standing guard; Flannel just ignores the lists.

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

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:

What is not affected:

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:

Why it helps

In a bank, "flat pod network with no NetworkPolicies" is an audit finding, and you'll be the one writing the baselines. The most common self-inflicted outage is a default deny that forgets DNS: every pod in the namespace suddenly throws UnknownHostException and nobody thinks of the policy merged an hour ago. You'll know to add the DNS egress rule first, every time.

In incidents, "timed out where I expected refused" becomes your fingerprint for a policy drop. In PR reviews you'll catch ingress-only policies that leave egress wide open, and policies written against ClusterIPs that do nothing. And you'll insist on proving a policy works with a connection that must fail, because on a CNI that doesn't enforce them, everything looks fine.

FAQ

Can I write a rule that denies traffic from one specific pod?

Not with standard NetworkPolicy. Policies are purely additive allow-lists: once a pod is isolated, only traffic matching some rule of some policy selecting it is allowed, and multiple policies union their rules. To "deny X" you isolate and then allow everything except X. Real deny rules and priorities exist in AdminNetworkPolicy and in CNI-specific CRDs (Calico, Cilium).

Why did DNS break right after I added a default deny?

Because policyTypes: [Ingress, Egress] with no rules also blocks egress to CoreDNS in kube-system. Name lookups fail with errors that don't look like networking at all. Always pair a default deny with an egress rule to the CoreDNS pods (kubernetes.io/metadata.name: kube-system plus k8s-app: kube-dns) on UDP and TCP 53.

Can I allow traffic to a Service by its ClusterIP with ipBlock?

It won't do what you expect. Policies are evaluated on pod IPs after kube-proxy's DNAT has already rewritten the ClusterIP to a pod IP. Select the destination pods by label (and namespace) instead. ipBlock is for things outside the cluster, such as a partner API or an on-prem database.

Do I need to allow the reply traffic too?

No, policies are stateful. If A may connect to B, B's replies on that connection come back automatically. You also don't need rules for the node's own traffic to its pods (kubelet probes are always allowed) or for loopback. hostNetwork pods can't be selected by policies at all; their traffic looks like the node's.

How do I know the CNI actually enforces my policies?

Test with a connection that must fail. The API server accepts, stores and describes NetworkPolicies regardless; enforcement is the CNI's job. Calico and Cilium enforce them, Flannel silently doesn't. On a cloud provider's managed cluster the policy engine is often chosen at cluster creation. Use a short timeout, for example nc -zv -w 2 db 6379, and expect "timed out".

In an interview Junior

What is a NetworkPolicy, and what is the default behaviour without one?

Without any policy, everything talks to everything: any pod can reach any pod in any namespace on any port. Namespaces are not a network boundary.

A NetworkPolicy is a firewall rule for pods: it selects pods by label and lists which traffic may reach them (ingress) and which they may send (egress). The rule people get wrong: a pod is isolated in a direction only when some policy selects it and lists that direction in policyTypes; then only what some rule allows passes. Policies only add allowances - there is no "deny".

The usual start in a namespace:

  1. Default deny - podSelector: {}, policyTypes: [Ingress, Egress].
  2. Allow DNS straight after - egress to the k8s-app=kube-dns pods in kube-system on UDP and TCP 53, or every name lookup breaks.
  3. Allow each needed connection explicitly.

Blocked traffic is dropped (a timeout), and the CNI plugin enforces it: Calico and Cilium do, Flannel silently ignores policies. Prove each with a connection that should fail.

Also asked: You apply a default-deny policy and the application breaks in unexpected ways. What do you check? · Why must a NetworkPolicy target CoreDNS pods rather than the DNS Service IP? · Which component actually enforces NetworkPolicies?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.