Lesson  in  DevSecOps in Practice: Container & Kubernetes Security

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

SourceDestinationResult
frontendbackend✓ Allowed
frontenddatabase✗ Blocked
backenddatabase✓ Allowed
backendinternet✗ Blocked

Cleanup

kubectl delete namespace netdemo