Kubernetes Network Policy
By default, every pod in a Kubernetes cluster can reach every other pod. Apply a default-deny NetworkPolicy, then selectively re-open only the paths that need to exist: frontend to backend, backend to database — but never frontend directly to database.
Default Allow to Default Deny
Kubernetes Has No Network Isolation by Default
Every pod in a namespace can reach every other pod — including your database. Let's prove it, then lock it down.
Deploy three application tiers
kubectl create namespace netdemo
cat > apps.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: netdemo
spec:
replicas: 1
selector:
matchLabels: {app: frontend, tier: frontend}
template:
metadata:
labels: {app: frontend, tier: frontend}
spec:
containers:
- name: frontend
image: public.ecr.aws/nginx/nginx:1.25-alpine
ports:
- containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: netdemo
spec:
replicas: 1
selector:
matchLabels: {app: backend, tier: backend}
template:
metadata:
labels: {app: backend, tier: backend}
spec:
containers:
- name: backend
image: public.ecr.aws/nginx/nginx:1.25-alpine
ports:
- containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: database
namespace: netdemo
spec:
replicas: 1
selector:
matchLabels: {app: database, tier: database}
template:
metadata:
labels: {app: database, tier: database}
spec:
containers:
- name: database
image: public.ecr.aws/nginx/nginx:1.25-alpine
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: backend-svc
namespace: netdemo
spec:
selector: {app: backend}
ports:
- port: 80
---
apiVersion: v1
kind: Service
metadata:
name: database-svc
namespace: netdemo
spec:
selector: {app: database}
ports:
- port: 80
EOF
kubectl apply -f apps.yaml
kubectl wait --for=condition=Available deployment/frontend deployment/backend deployment/database \
-n netdemo --timeout=90s
Prove the problem
FRONTEND=$(kubectl get pod -n netdemo -l app=frontend -o jsonpath='{.items[0].metadata.name}')
echo "=== Frontend → Database (security problem) ==="
kubectl exec -n netdemo $FRONTEND -- wget -qO- --timeout=3 http://database-svc && echo "CONNECTED"
Expected: CONNECTED — the frontend can reach the database directly. This is a lateral movement risk.
Apply Default Deny
cat > default-deny.yaml << 'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: netdemo
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
EOF
kubectl apply -f default-deny.yaml
echo "Default deny applied."
Verify all traffic is now blocked
kubectl exec -n netdemo $FRONTEND -- wget -qO- --timeout=3 http://database-svc 2>&1 && echo "CONNECTED" || echo "BLOCKED"
Expected: BLOCKED
Selective Allow Policies
Open Only the Paths That Need to Exist
Now that everything is blocked, we selectively allow: frontend → backend and backend → database. The frontend → database path stays blocked forever.
Allow frontend → backend
cat > allow-frontend-backend.yaml << 'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: netdemo
spec:
podSelector:
matchLabels:
tier: frontend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
tier: backend
ports:
- protocol: TCP
port: 80
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-ingress-from-frontend
namespace: netdemo
spec:
podSelector:
matchLabels:
tier: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: frontend
ports:
- protocol: TCP
port: 80
EOF
kubectl apply -f allow-frontend-backend.yaml
Allow backend → database
cat > allow-backend-database.yaml << 'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-to-database
namespace: netdemo
spec:
podSelector:
matchLabels:
tier: backend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
tier: database
ports:
- protocol: TCP
port: 80
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-database-ingress-from-backend
namespace: netdemo
spec:
podSelector:
matchLabels:
tier: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
tier: backend
ports:
- protocol: TCP
port: 80
EOF
kubectl apply -f allow-backend-database.yaml
Test the enforced traffic matrix
FRONTEND=$(kubectl get pod -n netdemo -l app=frontend -o jsonpath='{.items[0].metadata.name}')
BACKEND=$(kubectl get pod -n netdemo -l app=backend -o jsonpath='{.items[0].metadata.name}')
echo "=== Frontend → Backend (should SUCCEED) ==="
kubectl exec -n netdemo $FRONTEND -- wget -q --timeout=5 http://backend-svc -O /dev/null && echo "CONNECTED" || echo "BLOCKED"
echo "=== Frontend → Database (should FAIL) ==="
kubectl exec -n netdemo $FRONTEND -- wget -q --timeout=5 http://database-svc -O /dev/null 2>&1 && echo "CONNECTED" || echo "BLOCKED"
echo "=== Backend → Database (should SUCCEED) ==="
kubectl exec -n netdemo $BACKEND -- wget -q --timeout=5 http://database-svc -O /dev/null && echo "CONNECTED" || echo "BLOCKED"
Enforced Traffic Matrix
| Source | Destination | Result |
|---|---|---|
| frontend | backend | ✓ Allowed |
| frontend | database | ✗ Blocked |
| backend | database | ✓ Allowed |
| backend | internet | ✗ Blocked |
Cleanup
kubectl delete namespace netdemo
- Previous lesson
- Namespace & RBAC Isolation
- Next lesson
- Mutual TLS with Istio Service Mesh