Skip to main content
RunBook Academy

KubernetesXIV · Namespace ArchitectureTenancy and isolation

NetworkPolicy per namespace — microsegmentation at the cluster level

Advanced⏱ ~16 minkubectl

What you'll learn

  • Author NetworkPolicies that isolate namespaces
  • Distinguish default-allow vs default-deny models
  • Reason about ingress and egress rules and their interactions
  • Apply NetworkPolicy patterns for production multi-tenancy

Prerequisites

Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16

Not yet marked complete on this device.

NetworkPolicy is the in-cluster network firewall that turns namespace isolation from name-prefix to network-enforced isolation. This lesson covers the policy model, the default-allow vs default-deny choices, and the production patterns.

The NetworkPolicy model

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: team-a-isolation
  namespace: team-a-prod
spec:
  podSelector: {}                  # applies to all Pods in this namespace
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: team-a-prod
    - podSelector: {}               # or from same namespace
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53                      # DNS

The policy:

  • Applies to all Pods in team-a-prod (empty podSelector matches everything).
  • Ingress: allow TCP traffic on port 8080 from Pods in team-a-prod.
  • Egress: allow UDP traffic on port 53 to Pods in kube-system (DNS).

A Pod without matching ingress or egress rules cannot receive or send the corresponding traffic.

Default-allow vs default-deny

Two models:

  • Default-allow: every Pod can talk to every other Pod unless a NetworkPolicy explicitly denies. The cluster’s default. No NetworkPolicy = all traffic allowed.
  • Default-deny: every Pod is isolated unless a NetworkPolicy explicitly allows. Achieved by creating a policy that selects all Pods and denies all traffic.
flowchart LR
    Default[Default-allow<br/>no policy] --> Allow[All traffic allowed]
    Policy1[NetworkPolicy A] --> Allow[A overrides default]
    Policy2[NetworkPolicy B] --> Deny[B explicitly denies]
    
    DefaultDeny[Default-deny policy] --> Restrict[All denied by default]
    Policy3[NetworkPolicy C] --> Allow2[C explicitly allows]

Default-allow is simpler (no policy = open); default-deny is safer (no policy = closed). Production discipline: choose default-deny per namespace; explicitly allow what is needed.

Creating a default-deny policy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: team-a-prod
spec:
  podSelector: {}              # applies to all Pods
  policyTypes:
  - Ingress
  - Egress

This policy:

  • Selects all Pods in team-a-prod.
  • Sets policyTypes to Ingress and Egress.
  • Has no rules under ingress or egress — meaning no traffic is allowed.

The result: every Pod in team-a-prod is denied all ingress and egress. Any explicit allow policy takes precedence.

Adding allow policies

# Allow Pods in team-a-prod to talk to each other
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-internal
  namespace: team-a-prod
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector: {}            # from same namespace
  egress:
  - to:
    - podSelector: {}            # to same namespace

This adds to the default-deny policy: Pods in team-a-prod can communicate with each other. Other namespaces are still blocked.

Production patterns

Isolated namespace with internal + DNS:

# Default deny
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: team-a-prod
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]

---
# Allow internal traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-internal
  namespace: team-a-prod
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
  ingress:
  - from:
    - podSelector: {}
  egress:
  - to:
    - podSelector: {}

---
# Allow DNS
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: team-a-prod
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

---
# Allow egress to specific external services (e.g., a database)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-db
  namespace: team-a-prod
spec:
  podSelector:
    matchLabels:
      app: web
  policyTypes: [Egress]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: databases
    ports:
    - protocol: TCP
      port: 5432

This stack of policies:

  • Default deny all.
  • Allow internal (same-namespace) traffic.
  • Allow DNS to kube-system.
  • Allow web Pods to reach the database in the databases namespace.

Production discipline: layer policies; each policy is small and focused.

Egress and external traffic

Default-deny blocks egress to the internet (and to other cluster services). To allow external traffic, add explicit egress rules:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-external-https
  namespace: team-a-prod
spec:
  podSelector:
    matchLabels:
      app: external-client
  policyTypes: [Egress]
  egress:
  - ports:
    - protocol: TCP
      port: 443

This allows the Pods labelled app=external-client to connect to any IP on TCP port 443. Production discipline: limit by destination CIDR when possible:

egress:
- to:
  - ipBlock:
      cidr: 0.0.0.0/0        # any IP
      except:
      - 10.0.0.0/8          # except cluster internal
  ports:
  - protocol: TCP
    port: 443

The ipBlock field allows CIDR-based rules; the except field excludes specific CIDRs.

Auditing NetworkPolicy

# List all NetworkPolicies in a namespace
kubectl get networkpolicy -n team-a-prod

# List all namespaces with NetworkPolicy
kubectl get networkpolicy -A

# Find namespaces without any NetworkPolicy
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
  count=$(kubectl get networkpolicy -n $ns -o jsonpath='{.items}' | jq length)
  if [ "$count" -eq "0" ]; then
    echo "No policies: $ns"
  fi
done

Production discipline: every production namespace should have a default-deny policy. Namespaces without policies are vulnerable to lateral movement.

Cross-course references

  • The Linux course part XXV-Linux-Firewall covers iptables/nftables; NetworkPolicy is the cluster-level equivalent.
  • The VyOS course part XXXVII-VyOS-Firewall covers network firewall rules; NetworkPolicy is the cluster-level equivalent.
  • The Docker course part LI-Docker-Firewalls covers container firewall patterns; NetworkPolicy is the cluster-level extension.

Quiz

Knowledge check · 4 questions

  1. Q1. What is the right way to enforce default-deny on a namespace?

  2. Q2. Under a default-deny NetworkPolicy, Pods can still reach the cluster DNS service without explicit configuration.

  3. Q3. An audit reveals that namespace `team-a-prod` has no NetworkPolicy. A compromised Pod in another namespace can reach the production database. Walk through the fix.

    Cluster with three namespaces: `team-a-prod` (no NetworkPolicy), `team-b-staging` (no NetworkPolicy), `databases`. A compromised Pod in `team-b-staging` reaches the PostgreSQL database in `databases` via the database Service IP. Production database access from staging is a lateral movement vector.

  4. Q4. How do multiple NetworkPolicies interact when they select the same Pod?

Passing score: 75%. Answers are checked in this browser.

Production discipline

  • Default-deny per namespace. Every production namespace has a default-deny NetworkPolicy.
  • Allow DNS explicitly. Default-deny blocks DNS; Pods need name resolution.
  • Layer policies. Each policy is small and focused; combine for the complete traffic model.
  • Audit NetworkPolicies regularly. Namespaces without policies are vulnerable to lateral movement.
  • Verify the CNI supports NetworkPolicy. Flannel does not enforce; Cilium, Calico do.