Skip to content
Documentation Background

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.

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 containerPort is 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.


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.

CNIPolicy enforcement
FlannelNone - policies accepted, completely ignored
CalicoFull enforcement via iptables or eBPF
CiliumFull enforcement via eBPF (kernel-level, no iptables)
Weave NetFull enforcement
AWS VPC CNIPartial (node-level via security groups)
Terminal window
# Confirm Cilium pods are running
kubectl 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 status
cilium status

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/v1
kind: NetworkPolicy
metadata:
name: api-allow
namespace: production # policy is scoped to this namespace
spec:
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: 5432
FieldPurpose
podSelectorSelects the Pods this policy applies to (the target). {} = all Pods in namespace.
policyTypesIngress, Egress, or both. Declares which direction(s) this policy governs.
ingressOrdered list of allowlist rules for incoming traffic.
egressOrdered 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:

Allow traffic from/to Pods matching a label within the same namespace:

from:
- podSelector:
matchLabels:
app: web # Pods labeled app=web in the SAME namespace

namespaceSelector - 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' ns

Combined - 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=prometheus
from:
- namespaceSelector:
matchLabels:
team: monitoring
- podSelector:
matchLabels:
app: prometheus
# AND - traffic only from prometheus Pods INSIDE the monitoring NS
from:
- namespaceSelector:
matchLabels:
team: monitoring
podSelector:
matchLabels:
app: prometheus

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 deny

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 CoreDNS
  • protocol: TCP (default) or UDP. SCTP is also supported.
  • port: numeric port number or a named port from the Pod’s containerPort.
  • endPort: (optional) defines a range from port to endPort.
# Port range example
ports:
- protocol: TCP
port: 8000
endPort: 9000 # allows 8000-9000 inclusive

Default 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/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # {} = matches ALL Pods in this namespace
policyTypes:
- Ingress
- Egress # both directions locked down
# no ingress/egress rules = nothing is allowed

After applying this, all wget/curl between Pods in production will time out:

Terminal window
kubectl exec api-pod -n production -- wget --spider --timeout=1 http://db-svc
# wget: download timed out
# Allow web Pods to reach api Pods on port 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-web-to-api
namespace: production
spec:
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 5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-db
namespace: production
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api
ports:
- protocol: TCP
port: 5432

When default-deny-egress is in place, Pods cannot resolve DNS. Explicitly allow CoreDNS:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: production
spec:
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: 53

Policies are additive - they build an allowlist, never a denylist:

  1. If no NetworkPolicy selects a Pod, that Pod has unrestricted ingress and egress.
  2. Once any NetworkPolicy selects a Pod, all traffic in the governed direction(s) is denied unless explicitly allowed by a rule.
  3. Additional policies only add to the allowlist - you cannot use a policy to revoke what another policy has permitted.
No policy applied -> allow all traffic
First policy applied -> deny all (for governed direction) + allowlist entries from that policy
Second policy applied -> union of both allowlists (additive)

Understanding what vanilla NetworkPolicy cannot do is as important as knowing what it can. These gaps drive when to reach for extended solutions.

LimitationDetail
L3/L4 onlyNo HTTP method, URL path, header, or gRPC service matching. Opening port 80 allows all HTTP traffic on that port.
Allow-only rulesNetworkPolicy can only permit traffic - there is no explicit Deny action. ClusterNetworkPolicy adds this.
No ordering or priorityMultiple policies matching the same Pod are unioned additively - no precedence control within the namespace tier.
No FQDN/DNS targetsCannot write egress.to: api.github.com. Only IP addresses and CIDRs are valid targets in ipBlock.
Namespace scope onlyCannot enforce cluster-wide rules from a single resource. Each namespace needs its own copy.
No node-to-pod rulesHost-network traffic (node processes talking to Pods) is not governed by standard NetworkPolicy.

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.

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: api-http-policy
namespace: production
spec:
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 path

Traffic matching app=web on port 8080 but using DELETE or hitting /admin is dropped with HTTP 403 - impossible to enforce with vanilla NetworkPolicy.

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 - blocked
ConsiderationDetail
PerformanceFlows hit an Envoy proxy - adds latency. Benchmark before enabling on high-throughput paths (>3k rps).
ScopeL7 rules are port-specific. Mixing L4 and L7 rules on the same port requires careful ordering.
ObservabilityUse Hubble (hubble observe) to see which L7 rule dropped a request in real time.

Terminal window
# List all policies - short alias: netpol
kubectl get networkpolicy -n production
kubectl 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 types
kubectl describe netpol allow-web-to-api -n production

kubectl describe output structure:

Name: allow-web-to-api
Namespace: production
Spec:
PodSelector: app=api
Allowing ingress traffic:
To Port: 8080/TCP
From:
PodSelector: app=web
Not affecting egress traffic
Policy Types: Ingress
  • kubectl get shows POD-SELECTOR and AGE only - no rule details.
  • kubectl describe shows full rule specification but does not enumerate live Pod IPs that currently match the selectors.
  • Functional verification requires test Pods and connectivity probes.

ScenarioPattern
Lock down a namespacepodSelector: {} + both policyTypes, no rules
Allow internal serviceingress.from.podSelector with matching labels
Allow cross-namespaceingress.from.namespaceSelector with NS label
Restrict to one portAdd ports array to every from/to entry
Allow DNS resolutionEgress to kube-system on UDP/TCP 53
External ingress (LB)ingress.from.ipBlock with load balancer CIDR
Block specific CIDRipBlock.cidr with except sub-range

SymptomLikely causeFix
Policy applied but traffic still flowsCNI has no policy controller (e.g. Flannel)Switch to Cilium, Calico, or Weave
wget times out after adding allow ruleMissing egress policy or DNS blockedAdd DNS egress rule; check both directions
Cross-namespace traffic blocked despite NS selectorNamespace missing required labelkubectl label ns monitoring kubernetes.io/metadata.name=monitoring
AND vs. OR confusion causing wrong selectorSelectors in wrong list structureVerify same-entry (AND) vs. separate-entry (OR)
Pods not matched by policyLabel mismatchkubectl get pods -l app=api -n production --show-labels
Terminal window
# Quick connectivity test between Pods
kubectl 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 Pod
kubectl get pod api-pod -n production --show-labels
# List all policies affecting a namespace
kubectl get netpol -n production -o yaml

Cluster-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.

Tier 1 - Admin (ClusterNetworkPolicy tier: Admin)
Non-overridable cluster rules, evaluated FIRST
Example: block all egress to known malicious IPs
|
v
Tier 2 - Namespaced (standard NetworkPolicy)
Namespace-scoped rules by app teams, evaluated SECOND
|
v
Tier 3 - Baseline (ClusterNetworkPolicy tier: Baseline)
Overridable cluster defaults, evaluated LAST
Example: allow monitoring namespace to scrape all Pods (unless a namespace policy blocks it)
TierOverridable by namespaced policy?Use case
AdminNo - always enforcedMandatory security rules (block C2 IPs, require DNS via CoreDNS only)
NamespacedN/A - is the override layerApp team-defined ingress/egress rules
BaselineYes - namespaced policy can tighten or openCluster 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/v1alpha2
kind: ClusterNetworkPolicy
metadata:
name: block-external-egress
spec:
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 range

Example: Baseline-Tier Policy (cluster default, overridable)

Section titled “Example: Baseline-Tier Policy (cluster default, overridable)”
apiVersion: policy.networking.k8s.io/v1alpha2
kind: ClusterNetworkPolicy
metadata:
name: allow-monitoring-scrape
spec:
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