Network Policies
By default, every pod in a Kubernetes cluster can talk to every other pod — there are no firewall rules. For a hobby project that's fine. For production, it's a problem: a compromised frontend pod can reach the database directly. Network Policies let you declare which pods are allowed to communicate, using label selectors and namespace filters instead of IP addresses.
Default Behavior
Without any Network Policies applied, Kubernetes networking is fully open:
- Any pod can reach any other pod in the cluster, regardless of namespace
- Any pod can reach any external IP
- Any external traffic allowed by a Service can reach the backing pods
Network Policies are additive and selective. A policy only applies to pods it selects via podSelector. Pods not selected by any policy remain fully open. Once a policy selects a pod for ingress traffic, all ingress not explicitly allowed is denied for that pod — and similarly for egress.
The Kubernetes API accepts NetworkPolicy objects on any cluster, but enforcement only happens if the CNI plugin supports it. Flannel does not enforce Network Policies. Calico, Cilium, WeaveNet, and others do. If you create policies on a cluster running Flannel, they are silently ignored.
NetworkPolicy Spec
A NetworkPolicy has three main parts: which pods it applies to (podSelector), which traffic directions it controls (policyTypes), and the allow-rules (ingress and/or egress).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: my-policy
namespace: default
spec:
podSelector: # which pods this policy applies to
matchLabels:
app: api
policyTypes:
- Ingress # control inbound traffic to selected pods
- Egress # control outbound traffic from selected pods
ingress: # list of allowed ingress rules
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress: # list of allowed egress rules
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
podSelector
The top-level podSelector targets pods within the same namespace that the policy belongs to. An empty selector (podSelector: {}) matches all pods in the namespace — useful for namespace-wide default-deny policies.
podSelector:
matchLabels:
app: api # only pods with this label are affected
# OR — apply to all pods in the namespace:
podSelector: {} # empty = match all pods
Inside ingress.from and egress.to blocks, podSelector and namespaceSelector are used as source/destination filters.
Ingress Rules
Each item in the ingress list is an allow rule. Traffic is permitted if it matches all conditions in that item — including both from sources and ports. Multiple items in the list are ORed together.
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
# Rule 1: allow traffic from frontend pods on port 8080
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
# Rule 2: allow traffic from monitoring namespace on port 9090
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- protocol: TCP
port: 9090
Two selectors inside the same from list item are ANDed — both must match. Two separate items in the from list are ORed. A common mistake: putting podSelector and namespaceSelector as siblings in one item ANDs them (pod in that namespace). Putting them as separate items ORs them (pods with that label OR pods in that namespace).
Egress Rules
Egress rules control traffic leaving the selected pods. The same AND/OR logic applies. Always include DNS egress (port 53 UDP/TCP) when restricting egress — pods can't resolve Service names without it.
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Egress
egress:
# Allow DNS queries (required for Service name resolution)
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow outbound to postgres pods only
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
# Allow outbound to external payment API
- to:
- ipBlock:
cidr: 203.0.113.0/24
ports:
- protocol: TCP
port: 443
Default-Deny Pattern
The recommended security posture is default-deny all, then allow what's needed. Apply these two policies to every namespace:
# Deny all ingress to all pods in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
---
# Deny all egress from all pods in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
With these in place, pods in production have no connectivity until additional policies explicitly allow specific traffic flows. Add policies incrementally as you identify legitimate traffic paths.
Namespace Isolation
Isolate namespaces from each other while allowing intra-namespace traffic:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
# Only allow ingress from pods in the same namespace
- from:
- podSelector: {} # empty podSelector = all pods in this namespace
Combined with default-deny, this creates hard namespace boundaries — no cross-namespace traffic except where explicitly allowed by additional policies.
ipBlock
Use ipBlock to allow or deny traffic to/from external CIDR ranges. You can add exceptions within the CIDR:
ingress:
- from:
- ipBlock:
cidr: 10.0.0.0/8 # allow from the 10.x.x.x range
except:
- 10.0.0.0/16 # but not from this specific subnet
Note that ipBlock matches the IP seen by the network — after any NAT. In cloud environments, traffic from pods in other nodes may appear to come from the node IP, not the pod IP, depending on the CNI and network topology.
CNI Requirement
Network Policies are enforced at the kernel level by the CNI plugin — Kubernetes only stores the policy objects. Choose a CNI that supports enforcement:
| CNI | Network Policy support | Notes |
|---|---|---|
| Calico | Full | Also has CalicoNetworkPolicy for L7 rules and egress gateways |
| Cilium | Full + extended | eBPF-based; supports L7 (HTTP, gRPC) policies, DNS-based policies |
| WeaveNet | Full | Simple setup; performance lower than eBPF options |
| Flannel | None | Does not enforce policies — requires adding Calico or Cilium alongside |
| kindnet (kind) | None | Local dev only; no policy enforcement |
kubectl Commands
# List all NetworkPolicies in a namespace
kubectl get networkpolicy -n <namespace>
kubectl get netpol -n <namespace> # shorthand
# Describe a policy (shows selectors, rules)
kubectl describe networkpolicy default-deny-ingress -n production
# Apply a policy
kubectl apply -f policy.yaml
# Delete a policy
kubectl delete networkpolicy default-deny-ingress -n production
# Check which pods a policy selects
kubectl get pods -n production -l app=api
# Test connectivity from a pod (after applying policies)
kubectl exec -it -n production deploy/api -- \
curl -m 2 http://postgres.production:5432
# Cilium: visualize policy enforcement
kubectl exec -n kube-system ds/cilium -- \
cilium policy get