Network Policies
By default every Pod in a Kubernetes cluster can reach every other Pod across every namespace and node. This flat model is intentional for simplicity but catastrophic for security in a multi-tenant cluster.
The Problem: Flat Networking by Default
Section titled “The Problem: Flat Networking by Default”Every Pod gets a unique cluster-routable IP from its node’s CNI-assigned subnet. No routing boundaries, no namespace walls:
- Unrestricted cross-namespace access - a compromised Pod in a public-facing namespace can reach databases, internal APIs, and control-plane adjacent services in any other namespace.
- All ports open cluster-wide - every
containerPortis reachable from anywhere in the cluster unless explicitly locked down. - No enforced topology - nothing prevents a backend Pod from calling a frontend Pod, or a batch job from talking to a payments service.
NetworkPolicy resources fix this by acting as pod-level firewall rules based on label selectors.
CNI Enforcement Requirement
Section titled “CNI Enforcement Requirement”NetworkPolicy is a declaration - it has no effect without a network policy controller running in the cluster. The API server accepts any NetworkPolicy manifest regardless of whether a controller exists.
| CNI | Policy enforcement |
|---|---|
| Flannel | None - policies accepted, completely ignored |
| Calico | Full enforcement via iptables or eBPF |
| Cilium | Full enforcement via eBPF (kernel-level, no iptables) |
| Weave Net | Full enforcement |
| AWS VPC CNI | Partial (node-level via security groups) |
Verifying a Policy-Capable CNI (Cilium)
Section titled “Verifying a Policy-Capable CNI (Cilium)”# Confirm Cilium pods are runningkubectl get pods -n kube-system -l k8s-app=cilium
# Expected output:# NAME READY STATUS RESTARTS AGE# cilium-k5td6 1/1 Running 0 110s# cilium-operator-… 1/1 Running 0 110s
# Check Cilium statuscilium statusNetworkPolicy Anatomy
Section titled “NetworkPolicy Anatomy”NetworkPolicy is namespace-scoped, part of networking.k8s.io/v1, and must be created declaratively - there is no kubectl create networkpolicy imperative command.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: api-allow namespace: production # policy is scoped to this namespacespec: podSelector: # which Pods this policy applies TO matchLabels: app: api policyTypes: - Ingress # declare which directions this policy governs - Egress ingress: # allowlist rules for incoming traffic - from: - podSelector: matchLabels: app: web # only web Pods may send traffic in ports: - protocol: TCP port: 8080 # only on this port egress: # allowlist rules for outgoing traffic - to: - podSelector: matchLabels: app: db ports: - protocol: TCP port: 5432Top-Level spec Fields
Section titled “Top-Level spec Fields”| Field | Purpose |
|---|---|
podSelector | Selects the Pods this policy applies to (the target). {} = all Pods in namespace. |
policyTypes | Ingress, Egress, or both. Declares which direction(s) this policy governs. |
ingress | Ordered list of allowlist rules for incoming traffic. |
egress | Ordered list of allowlist rules for outgoing traffic. |
Selectors: Picking Sources and Destinations
Section titled “Selectors: Picking Sources and Destinations”ingress.from[] and egress.to[] accept three selector types that can be combined:
podSelector - Same Namespace
Section titled “podSelector - Same Namespace”Allow traffic from/to Pods matching a label within the same namespace:
from:- podSelector: matchLabels: app: web # Pods labeled app=web in the SAME namespacenamespaceSelector - All Pods in a Namespace
Section titled “namespaceSelector - All Pods in a Namespace”Allow traffic from/to all Pods in namespaces matching a label:
from:- namespaceSelector: matchLabels: kubernetes.io/metadata.name: monitoring # all Pods in the 'monitoring' nsCombined - Specific Pods in a Specific Namespace
Section titled “Combined - Specific Pods in a Specific Namespace”Both selectors in the same list entry = AND logic (Pod must match both):
from:- namespaceSelector: matchLabels: team: platform podSelector: matchLabels: app: prometheus # only prometheus Pods inside platform namespace# OR - traffic allowed from monitoring NS OR from any Pod labeled app=prometheusfrom:- namespaceSelector: matchLabels: team: monitoring- podSelector: matchLabels: app: prometheus
# AND - traffic only from prometheus Pods INSIDE the monitoring NSfrom:- namespaceSelector: matchLabels: team: monitoring podSelector: matchLabels: app: prometheusipBlock - External IP Ranges
Section titled “ipBlock - External IP Ranges”Allow traffic from/to CIDR ranges (useful for external systems):
from:- ipBlock: cidr: 10.0.0.0/8 except: - 10.10.0.0/16 # carve out a sub-range to denyPort-Level Controls
Section titled “Port-Level Controls”Without ports, an allowed selector grants access to all open container ports. Always specify ports to enforce least-privilege:
ports:- protocol: TCP port: 80- protocol: TCP port: 8443- protocol: UDP port: 53 # DNS - needed if egress allows CoreDNSprotocol:TCP(default) orUDP. SCTP is also supported.port: numeric port number or a named port from the Pod’scontainerPort.endPort: (optional) defines a range fromporttoendPort.
# Port range exampleports:- protocol: TCP port: 8000 endPort: 9000 # allows 8000-9000 inclusiveDefault Deny Pattern (Principle of Least Privilege)
Section titled “Default Deny Pattern (Principle of Least Privilege)”The recommended security baseline: lock everything down first, then open only what is required.
Step 1 - Default Deny All (namespace-wide)
Section titled “Step 1 - Default Deny All (namespace-wide)”apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-all namespace: productionspec: podSelector: {} # {} = matches ALL Pods in this namespace policyTypes: - Ingress - Egress # both directions locked down # no ingress/egress rules = nothing is allowedAfter applying this, all wget/curl between Pods in production will time out:
kubectl exec api-pod -n production -- wget --spider --timeout=1 http://db-svc# wget: download timed outStep 2 - Selectively Open Required Paths
Section titled “Step 2 - Selectively Open Required Paths”# Allow web Pods to reach api Pods on port 8080apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-web-to-api namespace: productionspec: podSelector: matchLabels: app: api policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: web ports: - protocol: TCP port: 8080---# Allow api Pods to reach db on port 5432apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-api-to-db namespace: productionspec: podSelector: matchLabels: app: db policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: api ports: - protocol: TCP port: 5432Step 3 - Allow DNS Egress
Section titled “Step 3 - Allow DNS Egress”When default-deny-egress is in place, Pods cannot resolve DNS. Explicitly allow CoreDNS:
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-dns-egress namespace: productionspec: 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: 53Policy Evaluation Model
Section titled “Policy Evaluation Model”Policies are additive - they build an allowlist, never a denylist:
- If no
NetworkPolicyselects a Pod, that Pod has unrestricted ingress and egress. - Once any
NetworkPolicyselects a Pod, all traffic in the governed direction(s) is denied unless explicitly allowed by a rule. - Additional policies only add to the allowlist - you cannot use a policy to revoke what another policy has permitted.
No policy applied -> allow all trafficFirst policy applied -> deny all (for governed direction) + allowlist entries from that policySecond policy applied -> union of both allowlists (additive)Native NetworkPolicy Limitations
Section titled “Native NetworkPolicy Limitations”Understanding what vanilla NetworkPolicy cannot do is as important as knowing what it can. These gaps drive when to reach for extended solutions.
| Limitation | Detail |
|---|---|
| L3/L4 only | No HTTP method, URL path, header, or gRPC service matching. Opening port 80 allows all HTTP traffic on that port. |
| Allow-only rules | NetworkPolicy can only permit traffic - there is no explicit Deny action. ClusterNetworkPolicy adds this. |
| No ordering or priority | Multiple policies matching the same Pod are unioned additively - no precedence control within the namespace tier. |
| No FQDN/DNS targets | Cannot write egress.to: api.github.com. Only IP addresses and CIDRs are valid targets in ipBlock. |
| Namespace scope only | Cannot enforce cluster-wide rules from a single resource. Each namespace needs its own copy. |
| No node-to-pod rules | Host-network traffic (node processes talking to Pods) is not governed by standard NetworkPolicy. |
L7 Policies (CiliumNetworkPolicy)
Section titled “L7 Policies (CiliumNetworkPolicy)”When Cilium is the CNI, it provides CiliumNetworkPolicy (CRD) which extends enforcement to Layer 7 by redirecting matching flows through a node-local Envoy proxy - no sidecar required.
This enables matching on HTTP methods, URL paths (including regex), headers, and gRPC service/method pairs.
HTTP Method and Path Matching
Section titled “HTTP Method and Path Matching”apiVersion: cilium.io/v2kind: CiliumNetworkPolicymetadata: name: api-http-policy namespace: productionspec: endpointSelector: matchLabels: app: api # same role as podSelector ingress: - fromEndpoints: - matchLabels: app: web toPorts: - ports: - port: "8080" protocol: TCP rules: http: - method: GET path: /api/v1/.* # regex path - read-only access only - method: POST path: /api/v1/orders # POST allowed only on this specific pathTraffic matching app=web on port 8080 but using DELETE or hitting /admin is dropped with HTTP 403 - impossible to enforce with vanilla NetworkPolicy.
gRPC Service/Method Matching
Section titled “gRPC Service/Method Matching”gRPC runs over HTTP/2, so Cilium parses it as HTTP/2 frames:
rules: http: - method: POST path: /payments.PaymentService/Charge # allow only Charge method - method: POST path: /payments.PaymentService/Refund # allow Refund too # GetAllPayments is NOT listed - blockedL7 Policy Considerations
Section titled “L7 Policy Considerations”| Consideration | Detail |
|---|---|
| Performance | Flows hit an Envoy proxy - adds latency. Benchmark before enabling on high-throughput paths (>3k rps). |
| Scope | L7 rules are port-specific. Mixing L4 and L7 rules on the same port requires careful ordering. |
| Observability | Use Hubble (hubble observe) to see which L7 rule dropped a request in real time. |
Inspecting Network Policies
Section titled “Inspecting Network Policies”# List all policies - short alias: netpolkubectl get networkpolicy -n productionkubectl get netpol -n production# NAME POD-SELECTOR AGE# allow-web-to-api app=api 5m# default-deny-all <none> 10m
# Detailed rules - includes ports, selectors, and policy typeskubectl describe netpol allow-web-to-api -n productionkubectl describe output structure:
Name: allow-web-to-apiNamespace: productionSpec: PodSelector: app=api Allowing ingress traffic: To Port: 8080/TCP From: PodSelector: app=web Not affecting egress traffic Policy Types: Ingresskubectl getshowsPOD-SELECTORandAGEonly - no rule details.kubectl describeshows full rule specification but does not enumerate live Pod IPs that currently match the selectors.- Functional verification requires test Pods and connectivity probes.
Common Patterns Quick Reference
Section titled “Common Patterns Quick Reference”| Scenario | Pattern |
|---|---|
| Lock down a namespace | podSelector: {} + both policyTypes, no rules |
| Allow internal service | ingress.from.podSelector with matching labels |
| Allow cross-namespace | ingress.from.namespaceSelector with NS label |
| Restrict to one port | Add ports array to every from/to entry |
| Allow DNS resolution | Egress to kube-system on UDP/TCP 53 |
| External ingress (LB) | ingress.from.ipBlock with load balancer CIDR |
| Block specific CIDR | ipBlock.cidr with except sub-range |
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Fix |
|---|---|---|
| Policy applied but traffic still flows | CNI has no policy controller (e.g. Flannel) | Switch to Cilium, Calico, or Weave |
wget times out after adding allow rule | Missing egress policy or DNS blocked | Add DNS egress rule; check both directions |
| Cross-namespace traffic blocked despite NS selector | Namespace missing required label | kubectl label ns monitoring kubernetes.io/metadata.name=monitoring |
| AND vs. OR confusion causing wrong selector | Selectors in wrong list structure | Verify same-entry (AND) vs. separate-entry (OR) |
| Pods not matched by policy | Label mismatch | kubectl get pods -l app=api -n production --show-labels |
# Quick connectivity test between Podskubectl exec -n production deploy/web -- wget --spider --timeout=2 http://api-svc:8080# 200 OK = allowed; timeout = blocked by policy; refused = app error
# Verify labels on target Podkubectl get pod api-pod -n production --show-labels
# List all policies affecting a namespacekubectl get netpol -n production -o yamlCluster-Wide Policies (ClusterNetworkPolicy)
Section titled “Cluster-Wide Policies (ClusterNetworkPolicy)”Vanilla NetworkPolicy is namespace-scoped - you need a copy in every namespace to enforce a cluster-wide rule. In 2026, the Network Policy API working group unified AdminNetworkPolicy and BaselineAdminNetworkPolicy into a single ClusterNetworkPolicy resource with a tier field.
The 3-Tier Evaluation Model
Section titled “The 3-Tier Evaluation Model”Tier 1 - Admin (ClusterNetworkPolicy tier: Admin) Non-overridable cluster rules, evaluated FIRST Example: block all egress to known malicious IPs | vTier 2 - Namespaced (standard NetworkPolicy) Namespace-scoped rules by app teams, evaluated SECOND | vTier 3 - Baseline (ClusterNetworkPolicy tier: Baseline) Overridable cluster defaults, evaluated LAST Example: allow monitoring namespace to scrape all Pods (unless a namespace policy blocks it)| Tier | Overridable by namespaced policy? | Use case |
|---|---|---|
| Admin | No - always enforced | Mandatory security rules (block C2 IPs, require DNS via CoreDNS only) |
| Namespaced | N/A - is the override layer | App team-defined ingress/egress rules |
| Baseline | Yes - namespaced policy can tighten or open | Cluster defaults (allow monitoring, default log egress) |
Example: Admin-Tier Policy (non-overridable block)
Section titled “Example: Admin-Tier Policy (non-overridable block)”apiVersion: policy.networking.k8s.io/v1alpha2kind: ClusterNetworkPolicymetadata: name: block-external-egressspec: tier: Admin # evaluated before any namespaced NetworkPolicy priority: 100 # lower number = higher priority within tier subject: namespaces: matchExpressions: - key: kubernetes.io/metadata.name operator: NotIn values: [kube-system] # applies to all non-system namespaces egress: - action: Deny # explicit Deny - not possible in vanilla NetworkPolicy to: - ipBlock: cidr: 203.0.113.0/24 # known malicious rangeExample: Baseline-Tier Policy (cluster default, overridable)
Section titled “Example: Baseline-Tier Policy (cluster default, overridable)”apiVersion: policy.networking.k8s.io/v1alpha2kind: ClusterNetworkPolicymetadata: name: allow-monitoring-scrapespec: tier: Baseline # app teams can override with a stricter NetworkPolicy priority: 10 subject: pods: namespaceSelector: {} # all namespaces podSelector: matchLabels: prometheus-scrape: "true" ingress: - action: Allow from: - namespaces: matchLabels: kubernetes.io/metadata.name: monitoring ports: - protocol: TCP port: 9090