Kubernetes Network Policies: Securing Pod Traffic
Master Kubernetes network policies to control pod-to-pod traffic, implement microsegmentation, and build defense-in-depth for your cluster workloads.

By default, every pod in a Kubernetes cluster can talk to every other pod. This flat network model is convenient for development but dangerous in production. A compromised pod can reach your database, internal APIs, and control plane without restriction.
Network policies are Kubernetes-native firewalls that control which pods can communicate. They implement microsegmentation—the practice of defining granular traffic rules between workloads rather than relying on perimeter security alone.
Understanding Default Network Behavior
Before any NetworkPolicy is applied, Kubernetes allows all ingress and egress traffic between pods. The moment you apply a policy to a pod, that pod switches from "allow all" to "deny by default" for the direction specified.
# ❌ No network policies: every pod can reach everything
# Pod A (frontend) → Pod B (api) ✓
# Pod A (frontend) → Pod C (database) ✓ ← This shouldn't happen
# Pod B (api) → Pod C (database) ✓
# Pod D (compromised) → Pod C (database) ✓ ← Dangerous# ✅ Default deny: block everything, then allow explicitly
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # Applies to ALL pods in namespace
policyTypes:
- Ingress
- EgressThis single policy immediately locks down the entire namespace. No pod can receive or send traffic until you create allow rules. Start here and build up—it's much safer than starting open and trying to close gaps.
Building Allow Rules for Common Patterns
The most common pattern is a three-tier application: frontend talks to API, API talks to database. Each tier should only reach its immediate neighbor.
# Allow frontend pods to receive traffic from ingress controller
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-to-frontend
namespace: production
spec:
podSelector:
matchLabels:
app: frontend
tier: web
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-system
podSelector:
matchLabels:
app: nginx-ingress
ports:
- protocol: TCP
port: 3000# Allow API pods to receive traffic ONLY from frontend pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: production
spec:
podSelector:
matchLabels:
app: api-server
tier: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
tier: web
ports:
- protocol: TCP
port: 8080# Allow database pods to receive traffic ONLY from API pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-to-database
namespace: production
spec:
podSelector:
matchLabels:
app: postgres
tier: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: api-server
tier: backend
ports:
- protocol: TCP
port: 5432Now if the frontend pod is compromised, the attacker can reach the API but not the database directly. Each hop requires a separate compromise, dramatically increasing the difficulty of lateral movement.
Controlling Egress Traffic
Ingress policies protect pods from unwanted incoming connections. Egress policies prevent pods from making unwanted outgoing connections—equally important for preventing data exfiltration.
# ❌ Pod can reach any external IP (data exfiltration risk)
# A compromised pod could send data to attacker-controlled servers
# ✅ Restrict API server egress to known destinations
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-server-egress
namespace: production
spec:
podSelector:
matchLabels:
app: api-server
policyTypes:
- Egress
egress:
# Allow DNS resolution
- to:
- namespaceSelector:
matchLabels:
name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Allow connection to database
- to:
- podSelector:
matchLabels:
app: postgres
tier: database
ports:
- protocol: TCP
port: 5432
# Allow connection to Redis
- to:
- podSelector:
matchLabels:
app: redis
ports:
- protocol: TCP
port: 6379
# Allow connection to external payment API
- to:
- ipBlock:
cidr: 203.0.113.0/24 # Payment provider IP range
ports:
- protocol: TCP
port: 443Always include DNS in egress rules. Without it, pods can't resolve any hostnames—including internal service names. This is the most common mistake when implementing egress policies.
Policy Testing and Validation
Network policies are notoriously hard to test. A misconfigured policy can break your application silently. Use dedicated testing tools and patterns.
// Policy validation script
interface PolicyTestCase {
name: string;
source: { namespace: string; labels: Record<string, string> };
destination: { namespace: string; labels: Record<string, string>; port: number };
expected: "allow" | "deny";
}
const testCases: PolicyTestCase[] = [
{
name: "Frontend can reach API",
source: {
namespace: "production",
labels: { app: "frontend", tier: "web" },
},
destination: {
namespace: "production",
labels: { app: "api-server", tier: "backend" },
port: 8080,
},
expected: "allow",
},
{
name: "Frontend cannot reach database directly",
source: {
namespace: "production",
labels: { app: "frontend", tier: "web" },
},
destination: {
namespace: "production",
labels: { app: "postgres", tier: "database" },
port: 5432,
},
expected: "deny",
},
{
name: "API can reach database",
source: {
namespace: "production",
labels: { app: "api-server", tier: "backend" },
},
destination: {
namespace: "production",
labels: { app: "postgres", tier: "database" },
port: 5432,
},
expected: "allow",
},
{
name: "Random pod cannot reach database",
source: {
namespace: "production",
labels: { app: "debug-pod" },
},
destination: {
namespace: "production",
labels: { app: "postgres", tier: "database" },
port: 5432,
},
expected: "deny",
},
];
async function runPolicyTests(
tests: PolicyTestCase[]
): Promise<{ passed: number; failed: number; results: string[] }> {
let passed = 0;
let failed = 0;
const results: string[] = [];
for (const test of tests) {
const actual = await probeConnection(
test.source,
test.destination
);
if (actual === test.expected) {
passed++;
results.push(`✅ ${test.name}: ${actual} (expected ${test.expected})`);
} else {
failed++;
results.push(`❌ ${test.name}: ${actual} (expected ${test.expected})`);
}
}
return { passed, failed, results };
}
async function probeConnection(
source: PolicyTestCase["source"],
destination: PolicyTestCase["destination"]
): Promise<"allow" | "deny"> {
// In practice: kubectl run a temp pod with source labels,
// attempt connection to destination, check exit code
return "allow"; // Placeholder
}Run these tests in CI against a staging cluster with the same policies applied. They serve as both documentation and regression tests—if someone modifies a policy, the test suite catches unintended changes.
Cross-Namespace Policies
Microservices often span multiple namespaces. Network policies support cross-namespace rules through namespace selectors, but the syntax requires careful attention.
# Allow monitoring namespace to scrape metrics from all production pods
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-monitoring-scrape
namespace: production
spec:
podSelector: {} # All pods in production
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- protocol: TCP
port: 9090 # Metrics port# Allow production pods to send logs to logging namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-logging-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
name: logging
podSelector:
matchLabels:
app: fluentd
ports:
- protocol: TCP
port: 24224Label your namespaces consistently. The name: monitoring label on the monitoring namespace is what makes the cross-namespace selector work. Without proper namespace labels, cross-namespace policies silently fail to match.
Debugging Connectivity Issues
When a network policy blocks traffic unexpectedly, debugging can be frustrating. A systematic approach saves hours of guesswork.
# Step 1: Verify which policies apply to the target pod
kubectl get networkpolicy -n production -o wide
# Step 2: Check pod labels match policy selectors
kubectl get pod api-server-abc123 -n production --show-labels
# Step 3: Test connectivity from source pod
kubectl exec -it frontend-xyz789 -n production -- \
wget --timeout=3 -qO- http://api-server:8080/health
# Step 4: Check if CNI plugin supports network policies
# (Not all do! Flannel without Calico won't enforce policies)
kubectl get pods -n kube-system | grep -E 'calico|cilium|weave'// Automated connectivity diagnostic
interface ConnectivityDiagnostic {
sourcePopod: string;
targetPod: string;
port: number;
policiesApplied: string[];
cniSupportsPolicy: boolean;
namespaceLabelsCorrect: boolean;
podLabelsMatch: boolean;
portInPolicy: boolean;
}
function diagnoseBlockedTraffic(
diag: ConnectivityDiagnostic
): string[] {
const suggestions: string[] = [];
if (!diag.cniSupportsPolicy) {
suggestions.push(
"CNI plugin does not support NetworkPolicy. " +
"Install Calico, Cilium, or another policy-aware CNI."
);
}
if (diag.policiesApplied.length === 0) {
suggestions.push(
"No policies found for target pod. " +
"Check if default-deny exists in this namespace."
);
}
if (!diag.podLabelsMatch) {
suggestions.push(
"Source pod labels don't match any ingress 'from' selector. " +
"Verify labels on both pods and in the policy."
);
}
if (!diag.portInPolicy) {
suggestions.push(
`Port ${diag.port} is not listed in any matching policy's port list.`
);
}
return suggestions;
}Key Takeaways
Network policies transform Kubernetes security from a flat, open network into a properly segmented architecture. The most important principle is default-deny: start by blocking everything, then create explicit allow rules for known traffic patterns. This approach ensures that new deployments are secure by default rather than accidentally exposed.
The common pitfalls are predictable: forgetting DNS in egress rules, not labeling namespaces for cross-namespace policies, and deploying to a cluster whose CNI doesn't support policies. Test your policies in staging with automated connectivity probes, and treat them as code—versioned, reviewed, and deployed through CI alongside the workloads they protect.


