Network Policies
Kubernetes-native L3/L4 firewall rules that control traffic flow between pods, namespaces, and external CIDRs. Network policies are declarative, namespace-scoped, and enforced by the CNI plugin — not by kube-proxy.
What This Page Covers
- NetworkPolicy resource model: podSelector, policyTypes, ingress, egress rules
- Peer selectors: podSelector, namespaceSelector, ipBlock, and their combinations
- AND vs OR logic within a single rule entry
- Default-deny and default-allow patterns for ingress and egress
- Ports: named ports, protocol (TCP/UDP/SCTP), endPort range
- DNS egress: why CoreDNS must be explicitly allowed under default-deny egress
- Full isolation pattern: default-deny + selective allow
- Common policy patterns: namespace isolation, database tier, ingress-controller allow, monitoring allow
- AdminNetworkPolicy and BaselineAdminNetworkPolicy (KEP-2091, GA 1.31)
- CNI enforcement: which CNIs enforce NetworkPolicy; Flannel does not
- Calico GlobalNetworkPolicy and NetworkPolicy extensions
- CiliumNetworkPolicy: L7 HTTP rules, DNS FQDN egress, toFQDNs
- Testing policies: netcat, kubectl exec, policy-tester tools
- Visualizing policies: np-viewer, Cilium Hubble
- Interaction with service mesh (Istio/Linkerd) mTLS
- 6 metrics + 4 alerting rules + 5 troubleshooting runbooks
- 9 best practices for production policy management
How Network Policies Work
By default, Kubernetes allows all pod-to-pod communication — any pod can reach any other pod on any port. NetworkPolicy objects change this by selecting pods and defining allowed traffic. Key behavioral rules:
- Additive only — policies never block existing allowed traffic; they only add permissions. Multiple policies applying to the same pod are unioned.
- Pod selection activates enforcement — a pod only becomes subject to policy when at least one NetworkPolicy selects it. A pod with no matching policy has all traffic allowed.
- Separate ingress and egress — each is independently controlled. Selecting a pod in
policyTypes: [Ingress]activates ingress enforcement only. - Enforced by the CNI plugin — NetworkPolicy objects have no effect without a CNI that implements them (Calico, Cilium, Weave Net, Antrea). Flannel alone does not enforce policies.
- Stateful — reply packets for established connections are automatically allowed. You do not need a return-direction rule.
If your cluster uses Flannel as the CNI, NetworkPolicy objects are accepted by the API server but silently ignored. You must add Calico as a policy engine (Canal = Flannel + Calico policy) or switch to a policy-capable CNI. Always verify enforcement with a connectivity test.
NetworkPolicy Anatomy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-policy
namespace: production # policies are namespace-scoped
spec:
podSelector: # which pods this policy applies to
matchLabels:
app: api # empty {} = all pods in namespace
policyTypes:
- Ingress # activates ingress enforcement
- Egress # activates egress enforcement
ingress:
- from: # list of allowed sources (OR between list items)
- podSelector: # same-namespace pods with this label
matchLabels:
app: frontend
- namespaceSelector: # pods in namespaces with this label
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- protocol: TCP
port: 8080
endPort: 8090 # port range (GA 1.25)
egress:
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
- to: # allow DNS (critical under default-deny egress)
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Selector Logic: AND vs OR
The most common source of NetworkPolicy bugs is misunderstanding when selectors are ANDed vs ORed. The rule is:
OR — separate list entries
from:
- podSelector: # entry 1
matchLabels:
app: frontend
- namespaceSelector: # entry 2
matchLabels:
env: prod
# Allows pods with app=frontend
# OR pods in env=prod namespaces
# (different list items = OR)
AND — combined in one entry
from:
- podSelector: # combined
matchLabels: # in one
app: frontend # entry
namespaceSelector:
matchLabels:
env: prod
# Allows pods with app=frontend
# AND that are in env=prod namespaces
# (same list item = AND)
When you have namespaceSelector and podSelector as separate list items (OR), namespaceSelector alone allows all pods in matching namespaces regardless of their labels. This is often unintentionally permissive. Put both in the same entry (AND) to require both conditions simultaneously.
ipBlock — External CIDRs
ipBlock matches traffic from/to external IP ranges. It supports except to carve out sub-ranges:
ingress:
- from:
- ipBlock:
cidr: 10.0.0.0/8 # allow entire RFC1918 /8
except:
- 10.42.0.0/16 # except the pod CIDR (avoid confusion)
- 10.96.0.0/12 # except the service CIDR
ports:
- protocol: TCP
port: 443
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0 # allow all internet egress
except:
- 169.254.169.254/32 # block AWS IMDS from workloads
ipBlock applies to the pod's source IP, not to Service ClusterIPs. ClusterIPs are virtual (see kube-proxy internals) — DNAT happens before the NetworkPolicy is evaluated. You should use podSelector/namespaceSelector to match pod-to-pod traffic, not ipBlock with pod CIDRs.
Named Ports and Port Ranges
# Named port — references the port name defined in the container spec
# Useful when different pods use different port numbers for the same protocol
# Pod spec:
spec:
containers:
- name: api
ports:
- name: http-api
containerPort: 8080
# NetworkPolicy references the name:
ports:
- port: http-api # resolved to containerPort 8080 on matching pods
protocol: TCP
# Port range (GA since Kubernetes 1.25):
ports:
- protocol: TCP
port: 10000
endPort: 11000 # allows ports 10000-11000 inclusive
# SCTP support (GA 1.20):
ports:
- protocol: SCTP
port: 9999
Default-Deny and Default-Allow Patterns
Default Deny Ingress
Selects all pods in the namespace; specifies Ingress policyType with no rules — no ingress is allowed. Must be combined with specific allow policies.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {} # matches ALL pods in this namespace
policyTypes:
- Ingress # ingress enforcement active; empty rules = deny all
Default Deny Egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress # egress enforcement active; empty rules = deny all
Default Deny All (Ingress + Egress)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Allow All (override deny for debugging)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all-ingress
namespace: production
spec:
podSelector: {}
ingress:
- {} # one empty rule = allow all ingress
policyTypes:
- Ingress
When you apply default-deny egress to a namespace, pods lose DNS resolution because UDP/TCP port 53 to CoreDNS is blocked. You must add an explicit DNS egress rule or all service names stop resolving. This is the #1 cause of mysterious application failures after adding egress policies.
DNS Egress Rule — Always Required
Under default-deny egress, add this policy to every namespace that applies egress control. It allows DNS queries to CoreDNS in kube-system:
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 # CoreDNS pods
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53 # DNS over TCP for large responses
If your cluster uses NodeLocal DNSCache (see 04-dns.html), DNS queries go to 169.254.20.10 on the node — which is an ipBlock address, not a pod IP. Add an additional rule allowing UDP/TCP 53 to 169.254.20.10/32 when NodeLocal DNSCache is active.
Common Policy Patterns
Namespace Isolation
Prevent cross-namespace traffic while allowing intra-namespace communication:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: namespace-isolation
namespace: team-a
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {} # allow from any pod in SAME namespace
egress:
- to:
- podSelector: {} # allow to any pod in SAME namespace
- to: # allow DNS
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Database Tier Isolation
Allow only the application tier to connect to the database; deny everything else including monitoring (add a separate monitoring policy if needed):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: postgres-policy
namespace: production
spec:
podSelector:
matchLabels:
app: postgres
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: api # only API pods can connect
ports:
- protocol: TCP
port: 5432
egress:
- to: # postgres replication to standby
- podSelector:
matchLabels:
app: postgres
role: standby
ports:
- protocol: TCP
port: 5432
Allow Traffic from Ingress Controller
Applications behind an Ingress need to allow traffic from the ingress controller pods. The controller runs in a separate namespace (typically ingress-nginx):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-controller
namespace: production
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 443
Allow Prometheus Scraping
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus-scrape
namespace: production
spec:
podSelector: {} # all pods in namespace
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 9090 # adjust to actual metrics port
- protocol: TCP
port: 8080 # some apps expose /metrics here
Allow Specific External Egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-api
namespace: production
spec:
podSelector:
matchLabels:
app: payment-processor
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 203.0.113.0/24 # payment gateway IP range
ports:
- protocol: TCP
port: 443
- to: # also allow DNS
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Production Full-Isolation Pattern
The recommended layered approach for a production namespace — apply in this order:
# Apply all at once with a Kustomize overlay or Helm values:
# namespace-policies/
# 01-default-deny-all.yaml
# 02-allow-dns-egress.yaml
# 03-allow-intra-namespace.yaml
# 04-allow-ingress-controller.yaml
# 05-allow-prometheus-scrape.yaml
kubectl apply -f namespace-policies/
AdminNetworkPolicy & BaselineAdminNetworkPolicy
Standard NetworkPolicy has a critical limitation: cluster administrators cannot set policies that override or pre-empt namespace-scoped policies. The AdminNetworkPolicy API (KEP-2091, GA 1.31) solves this with cluster-scoped policy objects that take precedence over namespace policies.
AdminNetworkPolicy (ANP)
- Cluster-scoped (not namespaced)
- Priority field (0–1000, lower = higher priority)
- Actions: Allow, Deny, Pass
- Pass = defer to namespace NetworkPolicy
- Overrides namespace NetworkPolicy when action is Allow or Deny
- Can select pods across namespaces
BaselineAdminNetworkPolicy (BANP)
- Singleton (only one per cluster)
- No priority (evaluated after all ANPs)
- Only actions: Allow, Deny
- Acts as cluster-wide default when no namespace policy matches
- Lower precedence than ANP and namespace NetworkPolicy
Precedence Order
apiVersion: policy.networking.k8s.io/v1alpha1
kind: AdminNetworkPolicy
metadata:
name: deny-to-kube-system
spec:
priority: 10 # evaluated first (lowest number = highest priority)
subject:
namespaces:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: [kube-system, monitoring] # all namespaces except these
ingress:
- name: "deny-non-system-to-api-server"
action: Deny
from:
- namespaces:
namespaceSelector: {} # from any namespace
ports:
- portNumber:
protocol: TCP
port: 443
egress:
- name: "pass-to-namespace-policy"
action: Pass # let namespace NetworkPolicy decide
to:
- namespaces:
namespaceSelector: {}
---
apiVersion: policy.networking.k8s.io/v1alpha1
kind: BaselineAdminNetworkPolicy
metadata:
name: default # must be named "default"
spec:
subject:
namespaces: {} # all namespaces
ingress:
- name: "allow-same-namespace"
action: Allow
from:
- namespaces:
sameLabels: [kubernetes.io/metadata.name]
- name: "deny-all-else"
action: Deny
from:
- namespaces:
namespaceSelector: {}
CNI-Specific Policy Extensions
Calico GlobalNetworkPolicy
Calico extends standard NetworkPolicy with cluster-scoped GlobalNetworkPolicy and richer matching capabilities. See 02-cni-plugins.html for Calico architecture details.
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
name: deny-ssh-external
spec:
selector: all() # all pods cluster-wide
order: 100 # lower = evaluated first
types:
- Ingress
ingress:
- action: Deny
protocol: TCP
destination:
ports: [22]
source:
nets:
- "0.0.0.0/0"
notNets:
- "10.0.0.0/8" # except internal networks
---
apiVersion: projectcalico.org/v3
kind: NetworkPolicy # Calico namespaced NetworkPolicy (richer than K8s)
metadata:
name: api-policy
namespace: production
spec:
selector: app == 'api'
types:
- Ingress
- Egress
ingress:
- action: Allow
protocol: TCP
source:
selector: app == 'frontend'
destination:
ports: [8080]
egress:
- action: Allow
protocol: TCP
destination:
selector: app == 'postgres'
ports: [5432]
- action: Allow
protocol: UDP
destination:
selector: k8s-app == 'kube-dns'
namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
ports: [53]
CiliumNetworkPolicy — L7 HTTP Rules
Cilium's CiliumNetworkPolicy extends standard NetworkPolicy to L7 — it can enforce HTTP method, path, and headers, and allow DNS egress by FQDN rather than by IP. See 02-cni-plugins.html for Cilium architecture.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: api-l7-policy
namespace: production
spec:
endpointSelector:
matchLabels:
app: api
ingress:
- fromEndpoints:
- matchLabels:
app: frontend
toPorts:
- ports:
- port: "8080"
protocol: TCP
rules:
http:
- method: "GET" # only allow GET /v1/*
path: "^/v1/.*"
- method: "POST"
path: "^/v1/orders$"
headers:
- "Content-Type: application/json"
egress:
# DNS egress by FQDN (resolves to IPs automatically)
- toFQDNs:
- matchName: "payments.stripe.com"
- matchPattern: "*.stripe.com"
toPorts:
- ports:
- port: "443"
protocol: TCP
# Allow DNS queries themselves
- toEndpoints:
- matchLabels:
"k8s:k8s-app": kube-dns
"k8s:io.kubernetes.pod.namespace": kube-system
toPorts:
- ports:
- port: "53"
protocol: ANY
rules:
dns:
- matchPattern: "*" # allow all DNS lookups
Standard NetworkPolicy requires ipBlock rules with static IPs. For cloud services like Stripe or S3 that rotate IPs, this is unmanageable. Cilium's toFQDNs resolves hostnames and dynamically updates the allowed IP set — no manual CIDR maintenance needed.
Interaction with Service Mesh (mTLS)
When using Istio or Linkerd, all pod traffic is intercepted by a sidecar proxy. NetworkPolicy is evaluated against the pod IP after the sidecar receives and decrypts the mTLS connection from another sidecar.
This creates two enforcement layers:
| Layer | What It Controls | Where Enforced |
|---|---|---|
| NetworkPolicy (L3/L4) | IP + port access between pods; works on pod IP even with sidecars | CNI dataplane (iptables/eBPF) |
| Istio AuthorizationPolicy (L7) | Service identity (SPIFFE), HTTP method/path, JWT claims | Envoy sidecar |
Istio ambient mode (no sidecar) uses port 15008 for intra-mesh traffic. If you have default-deny NetworkPolicy, you must explicitly allow TCP 15008 between workloads for ambient mesh traffic to flow. This does not apply to sidecar mode.
Testing Network Policies
Quick Connectivity Test with netshoot
# Spin up a debug pod in the source namespace
kubectl run netshoot --image=nicolaka/netshoot -n team-a --rm -it -- /bin/bash
# From inside: test TCP connectivity
nc -zv postgres.production.svc.cluster.local 5432 # should succeed
nc -zv redis.production.svc.cluster.local 6379 # should fail if blocked
# Test DNS resolution
nslookup kubernetes.default.svc.cluster.local
dig +short postgres.production.svc.cluster.local
# Test HTTP
curl -sv http://api.production.svc.cluster.local:8080/health
Targeted Connectivity Test (kubectl exec)
# Test from a specific pod
kubectl exec -n production deploy/api -- nc -zv postgres 5432
kubectl exec -n production deploy/api -- curl -s http://external-api.example.com
# Test DNS from pod
kubectl exec -n production deploy/api -- nslookup postgres
Policy Verification with np-viewer
# np-viewer: visualize which policies apply to a pod
# https://github.com/runoncloud/np-viewer
kubectl np-viewer pod -n production -l app=api
# Cilium: check policy enforcement status
cilium policy get
cilium endpoint list
cilium endpoint get
# Calico: check which policies select a pod
calicoctl get networkpolicy -n production -o wide
kubectl exec -n kube-system deploy/calico-kube-controllers -- /bin/sh -c \
"calico-kube-controllers status"
Hubble (Cilium) — Visualize Dropped Packets
# Watch for dropped flows in real time
hubble observe --verdict DROPPED -f
# Filter to specific namespace
hubble observe --namespace production --verdict DROPPED -f
# Check why a specific flow was dropped (shows which policy)
hubble observe --from-pod production/api --to-pod production/postgres
Metrics, Alerting & Troubleshooting
Key Metrics
| Metric | Source | What It Tells You |
|---|---|---|
cilium_drop_count_total | Cilium | Packets dropped by NetworkPolicy; label reason=POLICY_DENIED |
cilium_policy_count | Cilium | Total policies imported; drop after deployment = parse error |
cilium_policy_endpoint_enforcement_status | Cilium | Endpoints with/without policy enforcement enabled |
calico_iptables_rules_count | Calico Felix | Total iptables rules; very high values indicate scaling issue |
felix_active_local_policies | Calico Felix | Policies active on the node |
felix_int_dataplane_failures_total | Calico Felix | Dataplane programming failures; non-zero = policies not applied |
Alerting Rules
# Alert: High policy drop rate (possible misconfiguration)
- alert: NetworkPolicyHighDropRate
expr: |
rate(cilium_drop_count_total{reason="POLICY_DENIED"}[5m]) > 100
for: 5m
labels:
severity: warning
annotations:
summary: "High NetworkPolicy drop rate on {{ $labels.node }}"
description: "Check if a new policy is blocking expected traffic"
# Alert: Calico dataplane programming failures
- alert: CalicoDataplaneFailure
expr: rate(felix_int_dataplane_failures_total[5m]) > 0
for: 5m
labels:
severity: critical
annotations:
summary: "Calico Felix dataplane programming failures"
# Alert: DNS resolution failures (often caused by missing DNS egress rule)
- alert: PodDNSResolutionFailures
expr: |
rate(coredns_dns_response_rcode_count_total{rcode="SERVFAIL"}[5m]) > 1
for: 5m
labels:
severity: warning
# Alert: NetworkPolicy not enforced (no CNI policy support)
- alert: NetworkPoliciesNotEnforced
expr: kube_networkpolicy_created > 0 unless cilium_policy_count > 0
for: 30m
labels:
severity: critical
Troubleshooting Runbooks
Runbook 1: Pod Cannot Reach Another Pod (Connection Refused / Timeout)
# Step 1: Identify which policies apply to source and destination pods
kubectl get networkpolicy -n <source-namespace> -o yaml
kubectl get networkpolicy -n <dest-namespace> -o yaml
# Step 2: Check if destination pod is selected by any egress-blocking policy
kubectl get networkpolicy -n <dest-namespace> -o json | \
jq '.items[] | select(.spec.policyTypes[] | contains("Ingress"))'
# Step 3: Verify selector labels match
kubectl get pod <dest-pod> -n <dest-namespace> --show-labels
# Step 4: Use Cilium Hubble to see drop reason
hubble observe --to-pod <dest-namespace>/<dest-pod> --verdict DROPPED -f
# Step 5: Temporarily remove the blocking policy (test environment only)
kubectl delete networkpolicy <policy-name> -n <namespace>
Runbook 2: DNS Resolution Failures After Adding Egress Policy
# Symptom: pods get connection refused to service names but not to pod IPs
# Cause: missing DNS egress rule under default-deny egress
# Confirm DNS is blocked:
kubectl exec -n production deploy/api -- nslookup kubernetes.default
# Should return: SERVFAIL or timeout
# Fix: apply DNS egress policy
kubectl apply -f - <<'EOF'
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
EOF
Runbook 3: Policy Applied But Traffic Still Blocked
# Possible causes:
# 1. Policy in wrong namespace
kubectl get networkpolicy --all-namespaces | grep <policy-name>
# 2. podSelector labels don't match pod labels
kubectl get pod <pod> -n <ns> --show-labels
# Compare with NetworkPolicy podSelector/matchLabels
# 3. AND vs OR selector mistake (see selector logic section)
# 4. CNI not supporting NetworkPolicy (Flannel)
kubectl get daemonset -n kube-system # check for calico-node or cilium
kubectl get pods -n kube-system -l k8s-app=calico-node
# 5. Calico Felix not programmed on the node
kubectl logs -n kube-system -l k8s-app=calico-node -c calico-node | grep -i error
Runbook 4: Ingress Controller Cannot Reach Backend Pod
# After adding default-deny-ingress, ingress controller gets 502
# Cause: need to allow ingress from ingress-nginx namespace
# Find ingress controller namespace and pod labels
kubectl get pods -n ingress-nginx --show-labels
# Apply allow rule (adjust labels to match your controller)
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-nginx
namespace: production
spec:
podSelector: {}
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 8080
- protocol: TCP
port: 443
EOF
Runbook 5: NetworkPolicy Not Enforced (Flannel Cluster)
# Verify your CNI
kubectl get daemonset -n kube-system
# If you see flannel but no calico-node or cilium: policies are NOT enforced
# Option 1: Add Canal (Flannel + Calico policy engine)
# https://docs.tigera.io/calico/latest/getting-started/kubernetes/flannel/flannel
# Option 2: Replace Flannel with Calico or Cilium (requires re-provisioning)
# Option 3: Quick smoke test to confirm enforcement:
kubectl create ns netpol-test
kubectl run server -n netpol-test --image=nginx --expose --port=80
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: netpol-test
spec:
podSelector: {}
policyTypes: [Ingress]
EOF
kubectl run client -n netpol-test --image=alpine --rm -it \
-- wget -T5 -O- http://server/ # should fail if enforced
kubectl delete ns netpol-test
Best Practices
- Start with default-deny-all per namespace, then add allows — never start permissive and try to harden later. It's much harder to audit.
- Always include a DNS egress allow rule with default-deny-egress — template it into your namespace provisioning automation so it's never missed.
- Use namespaceSelector with
kubernetes.io/metadata.name— this built-in label (auto-set since 1.21) gives stable, unique namespace identity without requiring custom labels. - Verify AND vs OR semantics in every policy — use a connectivity test immediately after applying a policy to confirm it works as intended.
- Use AdminNetworkPolicy for cluster-wide baseline rules — don't replicate the same "deny external SSH" rule in 50 namespace policies. Write it once in ANP.
- Label namespaces consistently — agree on a label taxonomy (
env: prod/staging/dev,team: payments/catalog) before writing policies. Retroactively labeling is painful. - Never use ipBlock for pod-to-pod traffic — pod IPs are ephemeral. Use podSelector/namespaceSelector instead. ipBlock is only for traffic from/to outside the cluster.
- Store policies in Git alongside workload manifests — treating policies as code enables PR review, audit trail, and automated testing.
- Use Cilium CiliumNetworkPolicy or Calico GlobalNetworkPolicy for L7/FQDN rules — standard NetworkPolicy cannot handle HTTP path enforcement or dynamic IP egress. Use CNI extensions for these requirements.