You Shall Not Pass – Segmenting Pod Traffic with NetworkPolicies
- Overview
- Why it matters
- What does it solve
- Your Mission
- Debrief
- Extra: Network Policy Patterns (Reading)
- Pattern Format
- Pattern: DENY All Non-Whitelisted Traffic to a Namespace
- Pattern: LIMIT Traffic to an Application
- Pattern: DENY All Traffic from Other Namespaces
- Pattern: ALLOW Traffic Only to a Metrics Port
- Pattern: DENY External Egress Traffic
- Pattern: DENY All Inbound to an Application (Except Specific Source)
- Operational Notes
- Next Steps & Enhancements
- Appendix: Validation Snippets
- Cleanup
Duration: ~15 minutes
Overview
Move from open pod-to-pod traffic to NetworkPolicies that deny by default and allow only the flows you need.
Show open pod-to-pod traffic, install a default-deny NetworkPolicy, add a narrow allow rule, and confirm which flows still work.
Why it matters
By default, pods in a namespace can usually reach each other. If one pod is compromised, that becomes a free path to databases and internal APIs. NetworkPolicies flip the model: deny first, then allow only the clients and ports you choose. That limits how far an incident spreads and gives responders a concrete answer to what a pod was allowed to talk to.
What does it solve
-
Prevents every compromised pod becoming a pivot
-
Reduces accidental exposure of internal services
-
Eases incident scoping (“what could it reach?”)
-
Supports regulated segmentation requirements
Your Mission
A foothold in any pod can scan sideways to databases and internal APIs when east–west traffic is open. Your mission: prove the open path exists, slam the door with default deny, then reopen only the client you trust.
Click each step only if you need a hint.
Stage the flat network the attacker wants
Spin up an API plus two clients—one “allowed,” one “denied”—before any policy exists.
oc new-project 101-05-n-netpol-demo
oc create deployment api --image=registry.access.redhat.com/ubi9/python-311 -- python3 -m http.server 8080
oc expose deployment api --port=8080 --target-port=8080
oc run client-allowed --image=registry.access.redhat.com/ubi9/ubi -l role=allowed -- sleep infinity
oc run client-denied --image=registry.access.redhat.com/ubi9/ubi -l role=denied -- sleep infinity
SVC=$(oc get svc api -o jsonpath='{.spec.clusterIP}')
Confirm lateral movement works with no policy
Both clients should reach the API. That is the blast radius you must shrink.
oc exec client-allowed -- curl -s -o /dev/null -w '%{http_code}\n' http://$SVC:8080
oc exec client-denied -- curl -s -o /dev/null -w '%{http_code}\n' http://$SVC:8080
flowchart LR
subgraph P1["No Network Policies"]
CA1[client-allowed]
CD1[client-denied]
API1[api]
CA1 -->|200| API1
CD1 -->|200| API1
end
Drop the blast radius — default deny ingress
Close every inbound door. Compromised pods should time out instead of probing freely.
oc apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingress
EOF
Verify the attacker’s pivots now fail
Both clients should time out. Good—lateral movement is blocked until you explicitly reopen a path.
oc exec client-allowed -- sh -c "curl -s --max-time 3 -o /dev/null http://$SVC:8080" >/dev/null 2>&1 || echo TIMEOUT
oc exec client-denied -- sh -c "curl -s --max-time 3 -o /dev/null http://$SVC:8080" >/dev/null 2>&1 || echo TIMEOUT
flowchart LR
subgraph P2["Default Deny Ingress"]
CA2[client-allowed]
CD2[client-denied]
API2[api]
CA2 -.->|timeout| API2
CD2 -.->|timeout| API2
end
Reopen only the trusted client’s path
Allow TCP/8080 solely from pods labeled role=allowed. Everything else stays locked out.
oc apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-api-from-allowed
spec:
podSelector:
matchLabels:
app: api
ingress:
- from:
- podSelector:
matchLabels:
role: allowed
ports:
- protocol: TCP
port: 8080
policyTypes:
- Ingress
EOF
Prove allow vs block (mission complete when denied stays dark)
Allowed client returns 200; denied client times out. Business traffic works; the attacker-shaped client does not.
oc exec client-allowed -- curl -s -o /dev/null -w '%{http_code}\n' http://$SVC:8080
oc exec client-denied -- sh -c "curl -s --max-time 3 -o /dev/null http://$SVC:8080" >/dev/null 2>&1 || echo BLOCKED
flowchart LR
subgraph P3["Allow Specific Client"]
CA3[client-allowed]
CD3[client-denied]
API3[api]
CA3 -->|200| API3
CD3 -.->|timeout| API3
end
Cleanup
oc delete project 101-05-n-netpol-demo --wait=false
Debrief
You started from a flat, open namespace network, applied default-deny ingress, then reopened only the trusted client path—shrinking east-west blast radius.
What breaks without this:
-
No NetworkPolicy → every pod can reach every other pod by default
-
Over-broad labels → accidental allows for the wrong workloads
-
Missing egress rules when needed → exfil or C2 paths stay open
Controls that matter: default-deny in sensitive namespaces, smallest allow set that keeps the app working, consistent app/role/tier/env labels, and validation of allow vs deny paths.
Quick facts: policies select pods; once selected, only explicit allows pass. CNI enforces at the pod interface—not a substitute for node firewalls. Pattern references below help you design the next rules.
Extra: Network Policy Patterns (Reading)
The following reference patterns are adapted from docs/network-policies.adoc. Use them to reason about design choices; keep YAML only in the hands-on section above.
Pattern Format
Each pattern includes: Explanation, Use Case, Risk (Why it matters), Implementation Checklist, Quick Validation steps.
Pattern: DENY All Non-Whitelisted Traffic to a Namespace
flowchart LR
subgraph ns_other ["namespace other"]
Blog[app=blog]
end
subgraph ns_default ["namespace default"]
API[app=api]
Guest[app=guestbook]
end
Blog -.-> Guest
Blog -. ❌ .-> API
API -. ❌ .-> Guest
- Explanation
-
Only approved cross-namespace flow (blog → guestbook) is permitted; other cross or internal flows are blocked.
- Use Case
-
Multi-tenant cluster; restrict which external namespace may call a frontend.
- Why it Matters
-
Reduces lateral movement between namespaces.
- Implementation Checklist
-
-
NetworkPolicy selecting protected pods (e.g. guestbook)
-
Ingress rules with
fromincludingnamespaceSelector + podSelectorfor allowed source -
Specify ports
-
policyTypes: [Ingress]
-
Pattern: LIMIT Traffic to an Application
flowchart LR Coffee[app=coffeeshop\\nrole=api] BookAPI[app=bookstore\\nrole=api] BookFE[app=bookstore\\nrole=frontend] BookAPI -.-> BookFE Coffee -. ❌ .-> BookAPI
- Explanation
-
Frontend (role=frontend) may call bookstore API; other APIs denied.
- Use Case
-
Enforce intra-namespace microservice boundaries.
- Why it Matters
-
Prevents accidental/malicious service calls to internal APIs.
- Implementation Checklist
-
podSelector for API pods; ingress from frontend label; restrict ports;
policyTypes: [Ingress].
Pattern: DENY All Traffic from Other Namespaces
flowchart LR
subgraph ns_foo ["namespace: foo"]
FooPod[Any Pod]
end
subgraph ns_default ["namespace: default"]
Web[app=web]
DB[app=db]
end
subgraph ns_bar ["namespace: bar"]
BarPod[Any Pod]
end
Web -.-> DB
DB -.-> Web
FooPod -. ❌ .-> Web
FooPod -. ❌ .-> DB
BarPod -. ❌ .-> Web
BarPod -. ❌ .-> DB
- Explanation
-
Only internal namespace communication is permitted.
- Use Case
-
Tenant isolation; environment boundary.
- Why it Matters
-
Prevents privilege creep and meets audit separation requirements.
- Implementation Checklist
-
Policy selecting web & db; ingress limited to same-namespace (no namespaceSelectors) OR selective addition for trusted namespaces.
Pattern: ALLOW Traffic Only to a Metrics Port
flowchart LR
Prom[app=prometheus\\nrole=monitoring]
subgraph API ["app=api"]
Metrics[":5000 (metrics)"]
HTTP[":8000 (http)"]
end
Prom -.-> Metrics
Prom -. ❌ .-> HTTP
- Explanation
-
Prometheus may scrape metrics port; general HTTP port is blocked.
- Use Case
-
Observability access minimization.
- Why it Matters
-
Reduces exposure of non-observability endpoints to monitoring credentials.
- Implementation Checklist
-
Ingress from monitoring pods; allow port 5000 only.
Pattern: DENY External Egress Traffic
flowchart LR
subgraph ns_default ["namespace: default"]
App1[app=web]
App2[app=db]
end
External[External services / Internet]
App1 -.-> App2
App2 -.-> App1
App1 -. ❌ .-> External
App2 -. ❌ .-> External
- Explanation
-
Internal communication allowed; outbound to external networks denied.
- Use Case
-
Regulated workloads (PCI, OT) requiring strict egress control.
- Why it Matters
-
Prevents data exfiltration and command-and-control callbacks.
- Implementation Checklist
-
Egress policy; allow only explicit internal destinations (DNS, logging, etc.);
policyTypes: [Egress].
Pattern: DENY All Inbound to an Application (Except Specific Source)
flowchart LR
subgraph ns_default ["namespace: default"]
Web[app=web]
end
subgraph ns_foo ["namespace: foo"]
FooPod[Any Pod]
end
AnyOther[Any Pod]
FooPod -.-> Web
Web -.-> AnyOther
Web -. ❌ .-> FooPod
Web -. ❌ .-> AnyOther
- Explanation
-
Web can make outbound calls but only FooPod can reach it inbound.
- Use Case
-
Backend reachable only via controlled proxy or connector.
- Why it Matters
-
Prevents accidental exposure and narrows attack surface.
- Implementation Checklist
-
Policy selecting web; ingress rule permitting only proxy label; add
policyTypes: [Ingress,Egress]if controlling both directions.
Operational Notes
-
Selection Principle: Pods not selected by any policy remain open (all ingress/egress allowed). Once selected, only explicitly allowed traffic passes.
-
Namespace Scope: Policies do not cross namespaces without
namespaceSelector. -
Default Deny Strategy: Add an empty (or minimal) policy selecting pods to shift them into deny-by-default, then add granular policies.
Appendix: Validation Snippets
# Test an allowed path
oc exec pod/frontend -- curl -s -o /dev/null -w '%{http_code}\n' http://api:8080
# Test a blocked path with timeout fallback
oc exec pod/untrusted -- curl -s --max-time 3 http://api:8080 || echo BLOCKED
