A team applies a Kubernetes NetworkPolicy object restricting traffic to their pods, but every connection they expected to block still succeeds. What are the two most likely explanations, and what does applying such a policy actually change?
answer
- Default = allow-all between pods
- Selection flips that pod to deny-by-default
- Ingress and Egress tracked independently
- CNI must enforce - Flannel silently ignores
- podSelector is namespace-scoped, matches pods not Services
basics
~20 sEither the cluster's CNI plugin does not enforce NetworkPolicy at all (the API object is accepted and silently ignored), or the policy's podSelector does not match the pods they think it does. A policy that does select a pod switches that pod from allow-all to deny-all for the directions the policy lists, permitting only what its rules allow.
solid answer
~60 sBy default every pod in Kubernetes can talk to every other pod - the model is **allow-all**. A NetworkPolicy is a whitelist that changes that for the pods its `podSelector` matches: once a pod is selected by at least one policy with `Ingress` in `policyTypes`, all inbound traffic is denied except what some policy allows. The same holds independently for `Egress`. So "nothing was blocked" has two usual causes: 1. **The CNI does not enforce it.** NetworkPolicy is an API, not an implementation. The API server happily stores it; enforcement is the network plugin's job. Flannel, for example, has no NetworkPolicy support - policies are accepted and ignored. Calico, Cilium, Antrea, Weave and most managed-cluster plugins do enforce. 2. **The selector matches nothing.** `podSelector` is namespace-scoped and label-based. Wrong labels, or the policy created in a different namespace than the workload, means zero selected pods and therefore zero effect. Verify by checking the plugin's documentation and by listing which pods the selector matches before assuming the rule is wrong.
code
yaml · 18 linesapiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-allow-frontend
namespace: shop
spec:
podSelector:
matchLabels:
app: api
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080go deeper
State the default allow-all model, that selecting a pod flips it to deny-except-listed, and that the CNI has to support enforcement.
Add the independence of ingress and egress, the empty-rule-list meaning, and how to verify a selector actually matches pods.
Emphasise the silent-no-op risk: policies accepted but unenforced give false assurance, so enforcement must be proven by test, ideally in CI.
Position NetworkPolicy as one L3/L4 layer of defence in depth - necessary for blast-radius reduction, insufficient for identity, and complemented by mTLS or L7 policy.
## The default is wide open Kubernetes' base network model requires that every pod can reach every other pod, on any port, across all namespaces, without NAT. That is a deliberate simplification - it makes service discovery and workload placement easy - and it means a compromised pod in one namespace can, by default, reach your database pod in another. **NetworkPolicy** is the mechanism for narrowing that. It is a namespaced object in the `networking.k8s.io/v1` API group with three moving parts: - **`podSelector`** - which pods *in the policy's own namespace* this policy applies to. An empty selector `{}` means every pod in the namespace. - **`policyTypes`** - which directions the policy governs: `Ingress`, `Egress`, or both. - **`ingress` / `egress` rule lists** - the allowed traffic, each rule combining peers (`from` / `to`) with `ports`. ## What applying one actually does The key mental model is **selection flips the default**. If no policy selects a pod, that pod is unrestricted. The moment one policy selects it with `policyTypes: [Ingress]`, that pod becomes deny-by-default for inbound traffic, and only traffic matching some ingress rule of some policy selecting it is allowed. Egress is tracked separately: a pod can be locked down for ingress and still have wide-open egress, if no policy with `Egress` in policyTypes selects it. A subtlety that trips people: an empty rule list is not "allow everything". `ingress: []` (or omitting `ingress` while listing `Ingress` in policyTypes) means *no* traffic is permitted - that is precisely how a default-deny policy is written. Another: policies are **additive and cannot deny**. There is no deny verb and no priority ordering. Multiple policies selecting the same pod union their allowances, so you cannot carve an exception out of a broad allow by adding a narrower policy - you must narrow the broad one. ## Enforcement lives in the CNI The API server validates and stores NetworkPolicy objects regardless of whether anything can enforce them. Actual enforcement is done by the network plugin - Calico, Cilium, Antrea, Weave Net, and the policy engines built into managed offerings - typically by programming iptables, nftables, or eBPF rules on each node keyed off pod identity. Plugins with no policy support (plain Flannel is the classic example) silently ignore the object. There is no admission error, no event, no status field to warn you. This "accepted but not enforced" behaviour is a genuine security trap: a team can believe it has segmentation and have none. So the first question when a policy appears ineffective is always: does this cluster's CNI enforce NetworkPolicy? Confirm from the plugin's documentation, then confirm empirically by applying a default-deny policy in a scratch namespace and checking a test pod really loses connectivity. ## The second failure: selector mismatch Because `podSelector` is label-based and namespace-scoped, several mistakes produce a policy that matches nothing: - Labels on the Deployment's pod template differ from what the policy selects (selecting `app: api` when pods carry `app.kubernetes.io/name: api`). - The policy was created in the wrong namespace. A NetworkPolicy only ever selects pods in its own namespace; there is no cluster-scoped built-in form. - Assuming the selector matches Services. It never does - policies match **pods** by label, and Service traffic is evaluated against the backend pod it is DNATed to. ## Verification habits - List what the selector matches: `kubectl get pods -n <ns> -l <same selector>`. Empty output explains everything. - `kubectl describe netpol <name> -n <ns>` prints the resolved rules and is good for spotting an accidentally empty rule. - Test from a throwaway client pod with a short-timeout probe, and test both the blocked case and an allowed case, so you know the test itself works. ## Where the boundary sits NetworkPolicy is an L3/L4 control: pods, namespaces, IP blocks, ports, protocols. It does not understand HTTP paths, methods, or identities - that is what a service mesh or an L7-aware policy CRD adds on top. And it governs the pod network only; it is not a substitute for authentication between services.
- If a pod is selected by a policy that only lists Ingress, what happens to its outbound traffic?Nothing changes - it stays unrestricted. Ingress and egress defaults are flipped independently, and only by policies whose policyTypes include that direction. To lock down outbound traffic you need a separate policy (or the same one) selecting the pod with Egress in policyTypes, which then denies all egress except what its rules allow.
- How would you prove to an auditor that NetworkPolicy is genuinely enforced in a cluster?Demonstrate empirically rather than by showing YAML. Create a scratch namespace with a default-deny policy and two pods, show that a connection between them fails with a timeout, then add an allowing policy and show the same connection succeeds. That proves the plugin is programming the dataplane, which the presence of the API object alone never does.
A NetworkPolicy is a guest list, not a ban list: putting a room on the list system means only listed guests get in, and if the venue never hired a doorman, the list changes nothing.
saying these in an interview costs you the question
- Believing an applied NetworkPolicy is enforced regardless of which CNI the cluster runs
- Thinking policies deny traffic globally rather than only for the pods they select
- Expecting podSelector to match Services or Deployments instead of pod labels
- Assuming a policy in one namespace can select pods in another
- Reading an empty ingress list as allow-all when it means allow-nothing