The Kubernetes NetworkPolicy API has no deny rule and no rule priorities. Given that, how do you make a namespace deny-by-default, and why can you not later add a policy that carves out an exception to an existing broad allow?
answer
- podSelector: {} = all pods, not none
- policyTypes listed plus no rules = deny all
- Union of allowances, never subtraction
- No priority, no order, no deny verb
- Express exclusions via positive capability labels
basics
~20 sApply a policy with an empty podSelector (selecting every pod in the namespace) and empty ingress and egress rule lists. Selecting a pod with no allowances denies everything for those directions. Exceptions are impossible because policies are purely additive - their allowances union, and nothing subtracts - so you must narrow the broad policy itself.
solid answer
~60 s**Default-deny** is written as a policy that selects everything and allows nothing: ```yaml spec: podSelector: {} policyTypes: ["Ingress", "Egress"] ``` An empty `podSelector` matches every pod in the namespace; listing a direction in `policyTypes` with no corresponding rules means zero allowed traffic in that direction. Then you layer narrow allow policies alongside it. The reason exceptions do not work is **additive semantics**. Every policy selecting a pod contributes allowances; the effective permission is the **union** of all of them. There is no deny verb, no ordering, no priority field, no "last match wins". So if one policy allows all pods in the namespace to reach the database, adding a second policy that allows only `app: api` does not restrict anything - it just adds an allowance already covered. The fix is to edit the broad policy so it never grants the traffic you want excluded. Practically that means owning the policy set as a coherent whole rather than letting teams append rules. Some CNIs add cluster-scoped, ordered, deny-capable policy CRDs precisely because the upstream API deliberately omits them.
code
yaml · 8 linesapiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: shop
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]go deeper
Recall the default-deny YAML shape and that policies only ever allow, never deny.
Explain union semantics, why exceptions require editing the broad policy, and why default-deny must be applied per namespace.
Talk about governance: who may create policies, keeping the namespace's policy set reviewed as one artifact, and testing deny paths in CI.
Justify the additive design - commutativity and multi-team safety - and decide deliberately between portable NetworkPolicy and vendor or AdminNetworkPolicy CRDs for cluster-wide guardrails.
## Additive-only by design The NetworkPolicy API contains exactly one kind of statement: *this traffic is allowed*. There is no `deny`, no `action` field, no `priority` or `order`, and no evaluation sequence. Restriction is produced not by rules but by **selection**: a pod that is selected by at least one policy for a direction becomes deny-by-default in that direction, and the only traffic permitted is the union of every allowance from every policy selecting it. This was a deliberate design choice. Additive, unordered rules are **commutative** - the result does not depend on which policy was created first, or by whom - which makes the model safe for multiple teams writing policies into the same namespace and easy for a CNI to compile into a dataplane. Ordered allow/deny lists (classic firewall ACLs) are far more expressive, and far easier to get catastrophically wrong through a misplaced rule. ## Writing default-deny The canonical form: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: shop spec: podSelector: {} policyTypes: ["Ingress", "Egress"] ``` Three details matter: - `podSelector: {}` means *all pods in this namespace*. It is not "no pods" - a common misreading. - Omitting the `ingress` and `egress` keys entirely, while naming both in `policyTypes`, yields zero allowances. `ingress: []` is equivalent. - It is namespace-scoped. There is no built-in cluster-wide default-deny, so a platform must apply this object to every namespace - typically via a namespace-provisioning template, an operator, or a policy engine that blocks namespace creation without it. Many teams start with **ingress-only** default-deny, because egress default-deny immediately breaks DNS and anything reaching external APIs, and needs a companion allow policy before it is safe. ## Why exceptions are impossible Suppose namespace `shop` has: - Policy A: pods labelled `app: db` accept ingress from `podSelector: {}` (everything in the namespace). - You now want *everything except* `app: batch` to reach the database. Adding Policy B allowing only `app: api` changes nothing: B's allowances are a subset of A's, and the union is still A. Adding a "policy that denies batch" is not expressible. The only correct move is to **rewrite A** so it enumerates the permitted peers, or to re-label so the intended set is selectable positively - for instance give allowed clients a `db-client: "true"` label and have A allow `podSelector: {matchLabels: {db-client: "true"}}`. Positive-capability labelling is the idiomatic way to express exclusions in this model. This has an important operational consequence: **the policy set is a single artifact**. If any team can add policies into a namespace, any team can widen access, and no team can narrow it. Real platforms therefore keep policies in version control next to the workload, review them like code, and often restrict who can create NetworkPolicy objects in shared namespaces via RBAC. ## What you give up, and what fills the gap The missing capabilities are real: no deny, no priorities, no cluster-scoped policy, no L7 matching, and no way to express "allow all except X" directly. CNIs fill this with their own CRDs - cluster-wide policies, explicit deny actions with ordering, DNS-name-based egress rules, and L7 matching. Those are genuinely useful, at the cost of portability: they are vendor APIs, not Kubernetes APIs, so a cluster migration means rewriting them. The upstream project has been closing some gaps with **AdminNetworkPolicy** and **BaselineAdminNetworkPolicy** (cluster-scoped, ordered, with explicit Allow/Deny/Pass actions, intended for cluster administrators to set guardrails above tenant policies). These are newer APIs with staged support across CNIs, so for now the additive namespaced NetworkPolicy remains the portable baseline you should be able to reason about. ## Practical checklist 1. Apply default-deny ingress to every namespace as part of provisioning. 2. Add egress default-deny only together with explicit DNS allowances. 3. Express "who may talk to what" as positive capability labels rather than trying to subtract. 4. Keep the namespace's policies together in one reviewed manifest set - the union is the security boundary, so a single over-broad policy defeats the rest. 5. Test the deny path, not just the allow path, and keep those tests in CI.
- Two policies select the same pod: one allows ingress from app=frontend, the other allows ingress from the monitoring namespace. What is permitted?Both. The effective allowance is the union of every policy selecting the pod, so traffic from frontend pods and from the monitoring namespace are each permitted, and everything else is still denied. Order of creation and policy names are irrelevant - the model is commutative by design.
- Why is a cluster-wide default-deny not achievable with the standard NetworkPolicy API alone?NetworkPolicy is namespaced and only ever selects pods in its own namespace, so a new namespace starts unrestricted until an object is created in it. Platforms therefore inject a default-deny policy at namespace provisioning, or use cluster-scoped alternatives such as a CNI's global policy CRD or the upstream AdminNetworkPolicy API to set a baseline above tenant namespaces.
Policies are like signed permission slips: any number of people can hand out more permission, but nobody can hand out a slip that revokes one. To take access away you must retrieve the slip that granted it.
saying these in an interview costs you the question
- Reading podSelector: {} as selecting no pods
- Believing a narrower policy overrides or restricts a broader one
- Expecting rule order, priority, or last-match-wins evaluation
- Assuming one default-deny policy protects the whole cluster rather than one namespace
- Applying egress default-deny without a DNS allowance and calling the resulting breakage a CNI bug