You Shall Not Pass – Segmenting Pod Traffic with NetworkPolicies

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 from including namespaceSelector + podSelector for 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.

Next Steps & Enhancements

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
giphy

Cleanup

Before moving to the next module, run the lab cleanup script to reset transient resources from this module.

cd ~/openshift-security-roadshow
bash setup/lab-cleanup.sh --module 101-05