Network Policies
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Services and
DNS and Service Discovery describe how any Pod finds and
reaches any other Pod or Service; by default, nothing stops it from doing so. A NetworkPolicy (networking.k8s.io/v1)
is the object that narrows that default down to an explicit allow-list, at the IP-and-port level (OSI layers 3-4),
for a selected group of Pods. This page is written against Kubernetes 1.37; see
the section index for the full version baseline. Neither O’Reilly book’s sampler
chapters cover NetworkPolicy at all — it is one of the "topics neither book covers" the official documentation alone
supplies — so everything below draws only on kubernetes.io, with no "book era" table to reconcile.
The default: every Pod is non-isolated
Before any NetworkPolicy selects a Pod, that Pod is non-isolated: every other Pod, on any namespace, and every
external address, can open a connection to it on any port, and it can open a connection to anything in turn. There is
no implicit firewall to opt out of — Kubernetes ships with an entirely flat, allow-all Pod network, and isolation is
something a NetworkPolicy adds, never something a Pod starts with and a policy relaxes. That matters for reading
the rules below correctly: a NetworkPolicy is not a stateful firewall rule with a default DROP, it is closer to a
label selector that switches a Pod from "reachable from everywhere" to "reachable only from what an ingress rule
names" — the moment spec.podSelector in any NetworkPolicy in the namespace matches a Pod for a given direction
(Ingress or Egress), that Pod becomes isolated for that direction, and every rule from every matching policy
is then combined additively: if any policy allows a connection, it is allowed, regardless of how many other policies
stay silent about it.
Source: Kubernetes Docs — Network Policies.
The NetworkPolicy object
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: catalog-api-to-postgres
namespace: bookshelf
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: postgres
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: catalog-api
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: bookshelf
podSelector:
matchLabels:
app.kubernetes.io/name: catalog-api
ports:
- protocol: TCP
port: 5432
Source: Kubernetes Docs — Network Policies.
| Field | Meaning |
|---|---|
|
Which Pods, in this namespace only, the policy applies to. An empty selector ( |
|
Which directions this policy governs: |
|
A list of peers, each one of |
|
Which protocol ( |
Source: Kubernetes Docs — Network Policies.
A NetworkPolicy is enforced from both ends of a connection at once: catalog-api-to-postgres above only grants
ingress into postgres; catalog-api still needs no egress policy of its own to reach it,
because it remains non-isolated for egress until some other policy selects them (the allow-DNS-egress policy
below is the first thing in Bookshelf that does). Traffic to and from the node a Pod runs on is always allowed
regardless of any policy, a Pod’s reply to a connection it initiated is always allowed (NetworkPolicy has no
notion of "half-open" a legitimate response can violate), and a Pod can never be blocked from reaching itself.
Default-deny-all
The single most common NetworkPolicy in any real cluster is not one that allows a specific flow, but one that
switches a whole namespace from the flat, open default to "nothing, unless another policy says otherwise" — written with an empty podSelector and no rules at all:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: bookshelf
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
podSelector: {} matches every Pod in bookshelf; an empty ingress list under policyTypes: [Ingress] means
"no rule allows anything", so every selected Pod becomes fully isolated for ingress and egress alike, with no
traffic getting through until a more specific policy — catalog-api-to-postgres above, or the allow-DNS-egress
policy below — opens a named hole back up. Applying this first, before any allow policy exists, is the standard
way to move a namespace from implicit-allow to explicit-allow without a gap where everything is briefly reachable;
applying it after other allow policies already exist changes nothing for the flows those policies already permit,
since NetworkPolicies compose additively regardless of the order they were created in.
Allowing DNS egress
A default-deny-all policy has one immediate, easy-to-miss consequence: it also blocks the DNS lookups every Pod
needs before it can reach anything by name, including the CoreDNS lookups
DNS and Service Discovery describes for
postgres.bookshelf.svc.cluster.local itself. An egress rule that allows only DNS, applied to every Pod alongside
the default-deny policy, is the standard fix:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: bookshelf
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
Source: Kubernetes Docs — Network Policies.
Both UDP/53 and TCP/53 are listed because CoreDNS answers most lookups over UDP but falls back to TCP for
answers too large for a single UDP datagram; the namespaceSelector + podSelector pair targets CoreDNS’s own
Pods specifically (labeled k8s-app: kube-dns in kube-system on a stock kubeadm or kind cluster) rather than
opening egress to the whole kube-system namespace. kubernetes.io/metadata.name, the label every namespace
carries automatically since Kubernetes 1.22 with its own name as the value, is what makes a namespaceSelector
usable against a specific namespace at all without that namespace having to be labelled by hand first.
The Bookshelf policies: only catalog-api reaches PostgreSQL
Putting the pieces together, Bookshelf's bookshelf namespace ends up with three NetworkPolicies: the
default-deny-all above, the allow-DNS-egress above, and catalog-api-to-postgres from the top of this page,
narrowing PostgreSQL’s reachable clients down to catalog-api alone. Two details finish the
picture:
-
Nothing above grants
catalog-apiegress topostgresexplicitly — only ingress intopostgresis restricted. Sincecatalog-apiis itself selected bydefault-deny-all’s empty `podSelector, it also needs an egress policy allowing it out topostgres:5432, mirroringcatalog-api-to-postgresfrom the other direction:apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: catalog-api-egress-to-postgres namespace: bookshelf spec: podSelector: matchLabels: app.kubernetes.io/name: catalog-api policyTypes: - Egress egress: - to: - podSelector: matchLabels: app.kubernetes.io/name: postgres ports: - protocol: TCP port: 5432Source: Kubernetes Docs — Network Policies.
-
reviews-api,webandnotifierare deliberately left with no rule mentioningpostgresat all: oncedefault-deny-allisolates every Pod, the absence of an allowing rule is the block — there is no separate "explicit deny" object to write, unlike a traditional firewall’s explicitDENYrule. Attemptingpsql -h postgres.bookshelf …from areviews-apiorwebPod after these four policies are applied times out rather than being refused, which is normal for NetworkPolicy: packets are silently dropped, not rejected with a TCPRSTor an ICMP error.
Selectorless Services and ipBlock peers
Not every peer a policy needs to reference is a Pod inside the cluster. ipBlock names a raw CIDR range instead of
a label selector, which is how a policy allows or restricts traffic to something outside the Pod network entirely — a selectorless Service pointing at a legacy database reachable only by IP, or
a partner API Bookshelf calls out to on a known address range:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: notifier-egress-to-partner-api
namespace: bookshelf
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: notifier
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 203.0.113.0/24
except:
- 203.0.113.128/28
ports:
- protocol: TCP
port: 443
Source: Kubernetes Docs — Network Policies.
except carves a narrower range back out of the allowed block — here, a /28 reserved for the partner’s own
internal tooling, not notifier’s traffic to reach. `ipBlock addresses are always evaluated against a Pod’s
observed source or destination IP, never against a Service’s ClusterIP, since a NetworkPolicy sits below the level
a Service’s virtual IP is rewritten at; the deprecated Endpoints API and its EndpointSlice successor
(Services) exist to make that distinction moot for selectorless Services
targeting Pods, but an external, non-Kubernetes address is exactly where ipBlock is the only option.
CNI requirement: does kind’s default kindnet enforce NetworkPolicy?
A NetworkPolicy object is only ever a request — nothing in the API server itself blocks a packet. Enforcement is
entirely the job of the CNI plugin, which must watch NetworkPolicy objects and program the node’s packet filtering
(iptables, nftables or an eBPF dataplane) to match. Applying the manifests above to a cluster whose CNI ignores
NetworkPolicy silently does nothing: kubectl get networkpolicy still shows the objects, kubectl describe still
shows their rules, and every connection this page describes as blocked keeps working exactly as before.
kind’s default CNI, kindnetd, gained out-of-the-box NetworkPolicy enforcement in kind v0.24.0 (August 2024),
built on the sigs.k8s.io/kube-network-policies controller, with a DNS-handling fix following in v0.25.0, so a
default kind create cluster on any recent kind release enforces every policy above with no extra installation
step. Confirm this on any given
cluster rather than taking it on faith, since it depends on which kind version actually created the cluster and
whether networking.disableDefaultCNI was set:
kind version
kubectl get pods -n kube-system -l app=kindnet -o wide
kubectl get pods -n kube-system -l app=kindnet \
-o jsonpath='{.items[0].spec.containers[0].image}'
kind v0.33.0 go1.24.1 darwin/arm64
NAME READY STATUS RESTARTS AGE IP NODE
kindnet-7f9x2 1/1 Running 0 5m 172.18.0.3 bookshelf-control-plane
kindnet-p2m6q 1/1 Running 0 5m 172.18.0.4 bookshelf-worker
kindest/kindnetd:v20260528-9350166c
The most direct check is the temporary-Pod test below: apply default-deny-all, confirm a Pod that used to succeed
now times out, then delete the policy and confirm it succeeds again. If enforcement is genuinely absent — an older
kind version, or a cluster created with disableDefaultCNI: true and no NetworkPolicy-capable CNI installed in its
place — the standard fallback is to install Calico on the kind node network instead of kindnet, following
Calico’s own kind quickstart:
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.30.3/manifests/calico.yaml
kubectl -n kube-system rollout status daemonset/calico-node
Source: kind Docs — Configuration (disableDefaultCNI) and
Calico Docs — Install Calico Networking and
Network Policy for Kubernetes. Cilium is a common alternative offering the same NetworkPolicy enforcement plus
its own CRDs for L7 and DNS-aware policies; see
Cluster Networking, CNI and Service Mesh for a fuller
comparison of CNI plugins beyond NetworkPolicy enforcement alone.
AdminNetworkPolicy, in brief
NetworkPolicy objects are always namespace-scoped and owned by whoever can create objects in that namespace — there is no cluster-wide policy a platform team can set that an application team’s own namespaced policies cannot
override or leave gaps in. The sigs.k8s.io/network-policy-api subproject (a separate CRD-based API, group
policy.networking.k8s.io, not part of core Kubernetes) fills that gap with AdminNetworkPolicy and
BaselineAdminNetworkPolicy — cluster-scoped policies a platform operator sets, evaluated before (admin tier) or
after (baseline tier, as a fallback default) every namespace’s own NetworkPolicy objects. As of this project’s
current v1alpha2 iteration, those two resources have been superseded in the API’s own roadmap by a unified
ClusterNetworkPolicy resource carrying both an Admin and a Baseline tier in one object; none of this family is
part of the core, stable networking.k8s.io/v1 API this page otherwise describes, and support depends entirely on
the CNI in use — Calico, Cilium and OVN-Kubernetes each implement some portion of it, kindnet does not. Bookshelf
does not need a cluster-scoped policy, so this section stops at naming the mechanism rather than working an example.
Testing with a temporary Pod
Every policy above is worth confirming against the live cluster rather than trusting the manifest, since a typo in
a label selector fails silently the same way an unenforced CNI does — both look like "nothing happened" from
kubectl apply’s point of view. A short-lived, throwaway Pod is the standard tool, created with `--rm so it is
deleted the moment the shell session exits:
# from inside the bookshelf namespace, as catalog-api would see it
kubectl run netpol-test -n bookshelf --rm -it --restart=Never \
--image=nicolaka/netshoot --labels="app.kubernetes.io/name=catalog-api" -- \
sh -c 'nc -zv -w3 postgres.bookshelf.svc.cluster.local 5432'
# from web, which no policy allows into postgres
kubectl run netpol-test -n bookshelf --rm -it --restart=Never \
--image=nicolaka/netshoot --labels="app.kubernetes.io/name=web" -- \
sh -c 'nc -zv -w3 postgres.bookshelf.svc.cluster.local 5432'
Connection to postgres.bookshelf.svc.cluster.local 5432 port [tcp/postgresql] succeeded!
nc: connect to postgres.bookshelf.svc.cluster.local port 5432 (tcp) timed out: Operation now in progress
The --labels flag matters as much as the image: catalog-api-to-postgres’s ingress rule matches on
`app.kubernetes.io/name, so a test Pod carrying the wrong label is indistinguishable, from the policy’s point of
view, from any other unauthorised Pod, regardless of which real Deployment it is meant to stand in for. Repeating
the same check with the default-deny-all and allow-dns-egress policies temporarily deleted (kubectl delete
networkpolicy --all -n bookshelf) and confirming every combination now succeeds is a useful sanity check that the
failures above came from the policies and not from a Service or DNS problem
(Debugging and Troubleshooting covers that failure
mode, "NetworkPolicy block", as one row of its failure-state table).
📊 The allowed flows
Source: Kubernetes Docs — Network Policies.