Allow-everything, until something says otherwise
A Pod having no NetworkPolicy selecting it means exactly that -- unrestricted. The moment any NetworkPolicy's podSelector matches a Pod, that Pod flips to deny-by-default for whichever directions (ingress, egress, or both) that policy declares, and only the traffic explicitly allowed -- by this policy or any other one also matching that Pod -- gets through. Multiple policies matching the same Pod are additive (OR'd together), never conflicting with each other.
This is a different model than RBAC, not just a network version of it. RBAC's default is "allow nothing until a RoleBinding grants it." NetworkPolicy's default is "allow everything until a NetworkPolicy restricts it" -- and most clusters never add one at all, which is the gap this page is about closing.
Selecting pods and traffic
A rule's peers -- who it applies to, and what it allows traffic to or from -- are expressed with the same three selectors throughout:
- podSelector — Pods matching a label, in the same namespace as the policy (or the only thing evaluated if it's the top-level selector that decides which Pods the policy even applies to).
- namespaceSelector — every Pod in any namespace matching a label. Every namespace is auto-labeled with
kubernetes.io/metadata.name: <its own name>since Kubernetes 1.21, so selecting one specific namespace by name doesn't require labeling it yourself first. - ipBlock — a CIDR range, for traffic to or from outside the cluster's Pod network entirely.
Ingress and egress are separate switches
policyTypes decides which directions a policy actually touches. A policy with only Ingress in policyTypes does nothing at all to that Pod's outbound traffic -- egress stays wide open -- even if the policy has no egress: block to suggest otherwise. This is the single most common NetworkPolicy mistake: writing a policy that looks like it locks a Pod down, while one whole direction was never declared and so was never touched.
Starting from deny-all
The usual pattern is to deny everything in a namespace first, then add specific allow rules on top -- the reverse of the cluster's own default, which is why most real setups add this explicitly rather than relying on anything implicit.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: staging
spec:
podSelector: {} # matches every Pod in this namespace
policyTypes:
- Ingress
- Egress
# No ingress: or egress: block -- declaring the type with no rules
# to satisfy it means nothing gets through, in either direction.A worked example: web talks to db, and nothing else
With the deny-all above in place, web needs its own policy opening exactly what it actually uses: inbound from the ingress controller's namespace, and outbound to the database on its port -- plus DNS, which is so easy to forget that it deserves calling out on its own.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: web-allow
namespace: staging
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 3000
egress:
- to:
- podSelector:
matchLabels:
app: db
ports:
- protocol: TCP
port: 5432
# DNS lookups go out over the network too -- without this, every
# hostname lookup (including the Service DNS names Compose users
# are used to getting for free) just times out.
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53Omitting to entirely on that last rule (rather than writing an empty list) means it isn’t restricted by destination at all -- only by port. That's deliberate: DNS needs to reach the cluster's DNS service, and pinning the destination to a specific ClusterIP is more fragile than just allowing the one port it runs on.
The catch: NetworkPolicy only works if your CNI enforces it
NetworkPolicy is a Kubernetes API object, not a feature the control plane itself implements. Whether any of this actually does anything is entirely up to the CNI plugin -- Calico, Cilium, and most managed-Kubernetes default CNIs on EKS/GKE/AKS enforce it; some simpler or older CNI setups silently accept the YAML and apply none of it. kubectl apply succeeding is not confirmation that traffic is actually being blocked -- check your CNI's own documentation, and verify the policy behaves as expected (see below) rather than trusting that it does.
Checking what's actually allowed
There's no equivalent of kubectl auth can-i for NetworkPolicy -- reading the rules back, and testing the real connection, are the only ways to know what's actually enforced.
# See every policy in a namespace and what it selects
kubectl get networkpolicy -n staging
kubectl describe networkpolicy web-allow -n staging
# Test it directly: a throwaway Pod trying the connection the policy
# is supposed to allow -- or block
kubectl run -n staging netcheck --rm -it --image=nicolaka/netshoot -- \
curl -sS --max-time 3 http://db:5432If that connection is supposed to be blocked and it still succeeds, the most likely explanation isn’t a mistake in the YAML -- it's that the cluster's CNI isn’t enforcing NetworkPolicy at all, which is worth confirming before debugging the policy any further. For restricting what's reachable from outside the cluster rather than Pod-to-Pod, see Services & Ingress.