NetworkPolicy

A NetworkPolicy is a namespaced Kubernetes API object (networking.k8s.io/v1, kind: NetworkPolicy) that declares pod-level firewall rules — ingress (incoming) and egress (outgoing) allow-lists expressed via Pod label selectors, namespace selectors, IP CIDR blocks, and port ranges (kubernetes.io/docs — Network Policies). The API moved from extensions/v1beta1 to the new networking.k8s.io/v1 group and was promoted to GA in Kubernetes 1.7 (June 2017) — the extensions/v1beta1 version remained served for backward compatibility until it was removed in 1.16 (PR #39164, Deprecated API Migration Guide). NetworkPolicy is not enforced by Kubernetes itself: the apiserver stores the objects, but the actual packet filtering is the responsibility of the cluster’s CNI plugin. Calico, Cilium, Weave Net, Antrea, kube-router, and Romana implement NetworkPolicy; Flannel in its default configuration does not — installing a NetworkPolicy in a Flannel-only cluster has no effect (kubernetes.io/docs — Network Policies). The model is default-allow until a policy selects a Pod, then default-deny for the selected direction with explicit additive allows — a subtle two-state contract that is the source of most NetworkPolicy bugs. A separate, newer API in the policy.networking.k8s.io group — historically AdminNetworkPolicy and BaselineAdminNetworkPolicy (both v1alpha1), being consolidated as of 2025-10-09 into a single v1alpha2 ClusterNetworkPolicy resource — covers the cluster-admin layer that can override or backstop namespace-owned NetworkPolicies (network-policy-api.sigs.k8s.io). It ships as out-of-tree CRDs, not in the kube-apiserver.

Mental Model

flowchart TB
    subgraph "Phase 1: no NetworkPolicy"
        P1["Pod A label app=web<br/>(no NetworkPolicy selects it)"]
        P2["Pod B"]
        P3["Pod C"]
        P1 ---|"all in / all out"| P2
        P1 ---|"all in / all out"| P3
        EXT[External IP]
        P1 ---|"all in / all out"| EXT
        NOTE1["Pod isolation: NONE<br/>Effective rule: ALLOW-ALL"]
    end

    subgraph "Phase 2: one NetworkPolicy selects Pod A on Ingress"
        NP["NetworkPolicy<br/>podSelector: app=web<br/>policyTypes: [Ingress]<br/>ingress:<br/>- from:<br/>  - podSelector: app=client<br/>  ports:<br/>  - port: 8080"]

        PA["Pod A label app=web"]
        PC["Pod C label app=client"]
        PD["Pod D label app=other"]
        EXT2[External IP]

        NP -. selects .-> PA
        PC ==>|"allowed: matches from"| PA
        PD -.x|"BLOCKED: not in any 'from'"| PA
        EXT2 -.x|"BLOCKED: not in any 'from'"| PA
        PA ==>|"egress still ALLOW-ALL<br/>(no Egress policy selects Pod A)"| INTERNET[Anywhere]

        NOTE2["Pod A: Ingress ISOLATED, Egress NOT isolated<br/>Default behavior FLIPPED on Ingress only"]
    end

What the diagram shows. NetworkPolicy is stateful at the per-Pod, per-direction level. Phase 1 — no policy selects Pod A: ingress and egress are both default-allow; any Pod or external IP can reach Pod A on any port, and Pod A can reach anywhere. Phase 2 — one policy with policyTypes: [Ingress] selects Pod A: the ingress direction has flipped to default-deny, and only from peers explicitly listed in any policy that selects Pod A are allowed in. Egress is still default-allow because no Egress policy selects Pod A. The insight to extract: NetworkPolicy is not a stateful firewall list with deny rules. It is a contract: “for each (Pod, direction) pair, if at least one NetworkPolicy mentions you, then only peers explicitly allowed by any such policy may communicate; otherwise everything is allowed.” Multiple policies are additive — they take a union of allows. There are no explicit deny rules in the basic NetworkPolicy API; absence-from-allowlist is the denial. (The newer AdminNetworkPolicy API adds explicit Allow/Deny/Pass actions for cluster-admin use.)

Mechanical Walk-through

The two-state isolation model

The single hardest part of NetworkPolicy is when a Pod becomes “isolated” (kubernetes.io/docs — Network Policies):

  • A Pod is not isolated for ingress unless at least one NetworkPolicy with Ingress in policyTypes selects it. When not isolated → all incoming traffic is allowed. When isolated → only ingress rules in selecting policies (union) are allowed; everything else is denied.
  • Symmetrically for egress: at least one policy with Egress in policyTypes selecting the Pod flips the direction to default-deny.

policyTypes is not always inferable: a NetworkPolicy with no ingress: field but Ingress in policyTypes is a valid default-deny-ingress policy. The mental model is “policyTypes declares which directions this policy makes the Pod isolated in; the absence of allow rules within that direction is a deny.” If policyTypes is omitted, Kubernetes implicitly sets Ingress always, and Egress only if egress: is non-empty.

Reply traffic is always allowed implicitly — CNIs treat NetworkPolicy as stateful at the connection level (or via conntrack), so allowing a request implicitly allows its response. You only ever write rules for the initiating direction.

Selectors: where peers come from

Inside an ingress[].from[] or egress[].to[] element, you list at most one of three peer kinds (an element with multiple kinds applies all of them, ANDed — easy footgun, see Failure Modes):

  • podSelector — matches Pods in the same namespace as the NetworkPolicy. An empty {} selector matches all Pods in the namespace.
  • namespaceSelector — matches by namespace labels. An empty {} selector matches all namespaces. Each namespace has the standard label kubernetes.io/metadata.name: <ns> so you can target a namespace by name.
  • ipBlock — a CIDR (cidr: 10.0.0.0/8) with optional except: [10.1.0.0/16, ...] exclusions. Used for off-cluster destinations (the API server, external databases, the public internet).
ingress:
- from:
  - podSelector:                          # Pod in the same namespace with these labels
      matchLabels: { app: client }
  - namespaceSelector:                    # AND any Pod in a labelled namespace
      matchLabels: { tier: trusted }
  - ipBlock:
      cidr: 10.0.0.0/8
      except: [10.0.0.0/24, 10.0.1.0/24]
  ports:
  - protocol: TCP
    port: 8080

The list under from: is OR: a connection from any of the three peer kinds is allowed. The list under ports: is OR too. But — critically — combining podSelector and namespaceSelector within a single list element is AND:

from:
- podSelector:       { matchLabels: { app: client } }
  namespaceSelector: { matchLabels: { tier: trusted } }
# Allows: Pods labelled app=client IN namespaces labelled tier=trusted

versus

from:
- podSelector:       { matchLabels: { app: client } }
- namespaceSelector: { matchLabels: { tier: trusted } }
# Allows: Pods labelled app=client in same namespace, OR any Pod in a tier=trusted namespace

The dash placement is the entire semantics. This subtle YAML indentation difference is the most common NetworkPolicy bug.

ports semantics

Each rule (ingress or egress) optionally restricts to specific ports and protocols. Protocol is TCP (default), UDP, or SCTP. Port can be a number, a named container port, or — in newer K8s versions — a range via endPort (e.g., port: 32000, endPort: 32100). If ports is omitted, all ports for the listed protocols are allowed.

NetworkPolicy is Layer 3 / 4 only. There is no L7 selection (no path or method matching, no host header) — that lives in Ingress, Gateway API, or service-mesh policies.

The Kubernetes docs themselves enumerate a “What you can’t do with network policies (at least, not yet)” list, and two entries on it are operationally important: there is no built-in way to log denied (or allowed) connections, and there are no explicit deny rules — the basic API expresses denial only as absence-from-an-allowlist (kubernetes.io/docs — Network Policies). The absence of deny-logging is the single biggest debugging pain: when a connection is dropped, the API surfaces nothing — the only signal is a client-side timeout. Observability of dropped packets is therefore a CNI feature, not a NetworkPolicy feature: Cilium emits policy-verdict Hubble flow events, Calico can log denied packets via its felix configuration, and Antrea exposes audit logging — none of which is portable across CNIs. The same list also notes you cannot force a default policy onto all namespaces from the core API (that is exactly the gap the cluster-scoped Admin/Baseline tiers fill), cannot make the cluster aware of Services by name (you select Pods and namespaces, never Service objects), and cannot block loopback or host-originated traffic to a Pod.

Reply traffic, hairpin traffic, node traffic

A few important explicit rules from the spec:

  • Reply packets are always allowed — you write rules for the initiator.
  • Traffic to/from the node a Pod runs on is always allowed, regardless of CIDR or selector. This exists so kubelet health checks reach Pods without policy maintenance, but it does mean an attacker on the node can talk to Pods even if no NetworkPolicy authorises them (kubernetes.io/docs).
  • A Pod cannot block traffic to itself via NetworkPolicy — local loopback inside the Pod’s network namespace is always allowed.

The CNI requirement

A NetworkPolicy object in the API has no effect unless a CNI plugin programs it into the kernel. The kube-apiserver does not enforce NetworkPolicy. Enforcement responsibility:

  • Calico — iptables-based by default, with an optional eBPF dataplane; comprehensive NetworkPolicy support including Calico-specific extensions.
  • Cilium — eBPF-based; in addition to NetworkPolicy supports the richer CiliumNetworkPolicy (L7 via embedded Envoy, DNS-based egress).
  • Weave Net — NetworkPolicy supported, project in maintenance.
  • Antrea — Open vSwitch-based, NetworkPolicy + Antrea-specific extensions.
  • kube-router — iptables-based.
  • Flannel — by default does not implement NetworkPolicy. Some Flannel deployments pair Flannel for connectivity with Calico for policy (“Canal” pattern, now deprecated).
  • AWS VPC CNI — native NetworkPolicy enforcement added 2023-08-29 in VPC CNI v1.14.0 (an eBPF-based network-policy agent and controller bundled with the addon), supported on EKS clusters running Kubernetes v1.25+ (AWS containers blog, 2023-08-29); older deployments relied on Calico-as-overlay-policy.

The operational implication: changing CNIs is also changing your NetworkPolicy enforcement. A correct policy that worked on Calico may behave differently on Cilium if your manifests rely on Calico-specific extension behaviour.

AdminNetworkPolicy and BaselineAdminNetworkPolicy

NetworkPolicy is namespace-scoped and namespace-owned — any namespace owner can write a policy that selects their own Pods. This conflicts with cluster-admin requirements like “no Pod anywhere may dial the cloud metadata IP 169.254.169.254” or “every namespace must allow DNS queries to kube-dns.” Plain NetworkPolicy cannot express these because it has no cluster-scoped, admin-only, higher-than-tenant-priority layer. The SIG-Network Network Policy API working group fills this gap with a separate, out-of-tree project (the policy.networking.k8s.io API group, shipped as installable Custom Resource Definitions, not built into the kube-apiserver) (network-policy-api.sigs.k8s.io, KEP-2091).

The history matters because the API is mid-redesign. The original two resources were:

  • AdminNetworkPolicy (ANP)cluster-scoped. Rules have explicit actions Allow, Deny, or Pass. ANP rules are evaluated before NetworkPolicy, and an Allow or Deny in ANP overrides namespace-owned NetworkPolicies. Pass defers the decision to NetworkPolicy. Priorities are integers in the range 0–1000; lower priority value means higher precedence — evaluated first (per the spec reference). Used to express cluster-wide guardrails.
  • BaselineAdminNetworkPolicy (BANP) — a singleton, cluster-scoped resource (only one, named default). Evaluated after NetworkPolicy, so it applies only when neither an ANP nor a NetworkPolicy matched. Used to set a cluster-wide “default-deny” backstop without overriding tenant policies.

The overall precedence chain is therefore a three-tier hierarchy: Admin tier (ANP, highest) → namespace NetworkPolicy → Baseline tier (BANP, lowest).

As of 2026-05-23, both ANP and BANP remain at policy.networking.k8s.io/v1alpha1 and are explicitly not stable — the working group describes them as “mostly used to get early feedback on the API design.” More importantly, the API has been consolidated: an update published 2025-10-09 introduced v1alpha2.ClusterNetworkPolicy, a single resource that merges ANP and BANP, with a tier field (Admin or Baseline) selecting which evaluation tier a given policy lives in (network-policy-api.sigs.k8s.io blog, 2025-10-09). In v1alpha2 the action verbs were also renamed: the old Allow becomes Accept, while Deny and Pass are retained (spec reference). The planned Beta release will be based on ClusterNetworkPolicy, not on the original ANP/BANP pair. Early adopters can keep using ANP/BANP at their latest tagged release (v0.1.7) but are advised to plan migration to ClusterNetworkPolicy.

Uncertain

Verify: the stability graduation timeline for ClusterNetworkPolicy (when, or whether, v1alpha2/Beta/GA ships) and the per-implementation support matrix. Reason: the API is actively being redesigned (the v1alpha2 consolidation landed 2025-10-09) and graduation dates from KEP-2091 (originally Alpha 1.24 / Beta 1.26 / Stable 1.28) have repeatedly slipped; CNI support for ANP/BANP vs ClusterNetworkPolicy differs by implementation (OVN-Kubernetes, Antrea, and Calico have implemented ANP/BANP to varying degrees; ClusterNetworkPolicy support is newer). To resolve: check the current network-policy-api release tags, the conformance/implementation page, and each CNI’s release notes at the time of use. The operational model in all cases is “install the CRDs and a CNI that implements them” — there is no built-in kube-apiserver enforcement path. uncertain

Configuration / API Surface

A representative “default-deny + selective allows” pair, with line-by-line commentary:

# 1. Default-deny all ingress and egress for the namespace.
apiVersion: networking.k8s.io/v1                  # the only stable group/version
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: prod
spec:
  podSelector: {}                                 # empty = "all Pods in this namespace"
  policyTypes:                                    # declares isolation for BOTH directions
  - Ingress
  - Egress
  # no ingress[] or egress[] sections means: zero allow rules → everything denied.
---
# 2. Allow DNS lookups from every Pod (you almost always need this).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: prod
spec:
  podSelector: {}                                 # applies to all Pods
  policyTypes: [Egress]                           # only need to declare egress here
  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
---
# 3. Allow the payments service to receive HTTP from the API gateway.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: payments-allow-from-gateway
  namespace: prod
spec:
  podSelector:
    matchLabels:
      app: payments                              # selects Pods app=payments
  policyTypes: [Ingress]
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress
      podSelector:
        matchLabels:
          app: ingress-nginx                     # cross-namespace, AND-combined
    ports:
    - protocol: TCP
      port: 8080
---
# 4. Allow payments to call its database in another namespace AND the public internet
#    except RFC1918, except the cloud metadata endpoint.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: payments-egress
  namespace: prod
spec:
  podSelector: { matchLabels: { app: payments } }
  policyTypes: [Egress]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: data
      podSelector:
        matchLabels:
          app: postgres
    ports: [{ protocol: TCP, port: 5432 }]
  - to:
    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 10.0.0.0/8                             # RFC 1918
        - 172.16.0.0/12
        - 192.168.0.0/16
        - 169.254.169.254/32                     # AWS/GCP cloud metadata
    ports: [{ protocol: TCP, port: 443 }]

A few less-obvious points:

  • podSelector: {} is not a no-op — it explicitly matches every Pod in the namespace. Forgetting the empty selector and writing podSelector: alone is also “all Pods,” but some YAML parsers reject the bare key — write {} to be explicit.
  • policyTypes is the authoritative direction list. If you list Egress but include no egress: rules, that’s a default-deny-egress on the selected Pods, even if ingress: rules are present.
  • namespaceSelector matches by namespace labels, not by name. Use the implicit kubernetes.io/metadata.name label (auto-set by the apiserver in modern K8s) to target by name; relying on hand-set labels is fragile.
  • endPort for port ranges is GA since 1.25; mixing port and endPort in a single rule covers a contiguous range. Older clusters need one rule per port.
  • Cluster-internal traffic is not the only thing to firewall. Egress to the cloud-metadata endpoint (169.254.169.254) is a classic privilege-escalation vector for SSRF attacks on cloud-hosted services — block it explicitly cluster-wide.

Failure Modes

“My pod can still reach everything.” The most common diagnostic — and almost always a CNI question. The cluster’s CNI does not implement NetworkPolicy (Flannel default), or the CNI’s NetworkPolicy controller is unhealthy. kubectl get networkpolicy shows the policy; the kernel does not enforce it. Fix: install Calico-for-policy alongside Flannel, or switch to a NetworkPolicy-capable CNI.

podSelector + namespaceSelector AND/OR confusion. As detailed under selectors. If the policy is “more permissive than expected,” check the dash placement: two list elements is OR; one element with both keys is AND. Test with kubectl-np-viewer or Cilium’s policy-trace tool.

Default-allow ambush. A team writes one ingress allow policy assuming “this is the only allowed path.” But because no other policy selects their Pod for egress or additional ingress rules, the Pod can still initiate connections to anywhere and receive connections from the node. Mitigation: every namespace ships with a default-deny-all baseline (or a cluster BaselineAdminNetworkPolicy) and every desired path is explicit.

DNS breaks after a default-deny-egress. Pods can no longer resolve any name because port-53/UDP egress to kube-dns is denied. The application sees obscure connection errors. The first allow rule in any deny-all setup must be DNS. (Kubernetes — Declare Network Policy example shows this.)

Selector matches nothing. A policy with podSelector: matchLabels: {app: paymnt} (typo) selects zero Pods. The policy is “live” but has no effect. Diagnose: kubectl get pods -n <ns> -l app=paymnt returns empty.

Reply traffic confusion. Some operators try to write return rules (“allow port 32768 ingress for ephemeral source-port responses”). Don’t — NetworkPolicy is connection-oriented in every CNI implementation; reply packets are matched against connection state, not against your rules.

Cross-CNI rule semantics. A Calico-specific Order field or tier annotation in a Calico cluster is silently a no-op when you migrate to Cilium. NetworkPolicy itself is portable; CNI-specific extensions are not. Always check kubectl get networkpolicy -o yaml for non-standard fields before a CNI migration.

Pod IP changes during connection. A long-lived connection from Pod A to Pod B; Pod B is replaced; the new Pod B (same Service, different IP) is not yet covered by a refreshed CNI rule. Some CNIs handle this via aggressive informer reconcile, others briefly drop. Mitigation: applications should retry on connection failure (standard mesh / SRE hygiene) and NetworkPolicies should be label-driven (which automatically covers replacement Pods) rather than IP-driven.

Hostport bypass. A Pod with hostPort: 8080 is reachable on every node’s IP at port 8080 — and NetworkPolicy on the target Pod does not always cover this path (CNI-implementation-defined). Avoid hostPort in production; if used, layer node-level firewall rules.

Egress to LoadBalancer Service VIP. A Pod’s egress NetworkPolicy with ipBlock: cidr: 0.0.0.0/0 does not obviously cover traffic to a LoadBalancer Service’s external LB IP — because that IP may be on the cluster’s external interface but not in the Pod’s egress path the way an arbitrary public IP is. CNIs handle this differently; test cross-cluster paths explicitly.

Order of policy creation matters for “default-deny” rollout. Applying a default-deny policy across an entire namespace while applications are running cuts existing connections at next reconnect. Roll out with a maintenance window or apply allow policies first, then the default-deny.

Alternatives and When to Choose Them

  • Service mesh mTLS + AuthorizationPolicy (Istio, Linkerd). Mesh policies are L7-aware (HTTP method, path, header, JWT claim) and identity-aware (SPIFFE identity, not just labels). When you need application-aware authorization, NetworkPolicy is the wrong layer.
  • Cloud-provider security groups / VPC firewalls. For node-to-node and cluster-to-external rules, the cloud’s firewall is more efficient than per-Pod CNI rules. Use it for the cluster-perimeter; use NetworkPolicy for intra-cluster.
  • CiliumNetworkPolicy / CiliumClusterwideNetworkPolicy. Cilium’s superset CRDs offer DNS-based egress allow lists (“allow egress to api.github.com” by resolving the name in the kernel) and L7 HTTP rules. Use when you’ve adopted Cilium and need these features, knowing you’ve coupled to Cilium.
  • Calico NetworkPolicy (projectcalico.org/v3). Calico’s superset with order, tiers, named selectors, and ICMP rules. Used inside Calico-only clusters to express administrative ordering before AdminNetworkPolicy existed.
  • OPA Gatekeeper / Kyverno admission policies. Don’t replace NetworkPolicy — they catch bad manifests at admission. Pair them: Gatekeeper rejects Deployments lacking a NetworkPolicy in their namespace; the NetworkPolicy enforces the runtime rule.
  • AdminNetworkPolicy / ClusterNetworkPolicy for cluster-admin use cases (see AdminNetworkPolicy and BaselineAdminNetworkPolicy). For “namespaces can’t override this,” the cluster-scoped Admin tier is the right layer once it stabilises.

Production Notes

  • Adopt default-deny per namespace as a baseline. Every namespace ships with default-deny-all-ingress, default-deny-all-egress, allow-dns. Application teams then layer specific allows on top. Without a default-deny baseline, NetworkPolicy is decorative.
  • Label namespaces consistently. A namespaceSelector that depends on team: payments works only if the namespace is actually labelled — and labelling is done at namespace-creation time. Bake it into your namespace provisioning.
  • Generate policies from intent, don’t hand-write them. Tools like cilium policy generate and inspektor-gadget infer policies from observed traffic. Adopt them for greenfield deployments; do not assume teams will write correct NetworkPolicies by hand.
  • Audit and visualise. kubectl-np-viewer, kubectl-np-cleaner, Calico’s calicoctl get networkpolicy rendered views are essential to understanding “what’s allowed for Pod X.” NetworkPolicy is hard to read in raw YAML at scale.
  • Test with nettest patterns. A canonical pre-deploy CI test: launch a probe Pod with known labels, attempt connections to every protected service, assert which succeed. NetworkPolicy bugs are silent (the only feedback is “connection times out”); explicit tests are the only safety net.
  • Mind kube-dns and metrics-server. Cluster-wide default-deny that forgets to allow egress to kube-dns destroys every service. Forgetting metrics-server access kills kubectl top and HPA.
  • Cloud metadata blocking. A cluster-wide rule denying egress to 169.254.169.254/32 prevents SSRF-based credential exfiltration on AWS/GCP. Add it day one.
  • Failure stories. k8s.af and engineering blogs record: A Spotify outage when a default-deny policy was applied to a namespace whose Service was indirectly used by every other team; a Capital One incident demonstrating cloud-metadata SSRF on a K8s-hosted app with no egress filtering; a Pinterest investigation of slow connections traced to NetworkPolicy churn rate exceeding the CNI’s reconcile speed during a mass rolling restart. Common pattern: the apiserver accepts the YAML, the CNI is the unsung hero or villain.

See Also