By default, Kubernetes allows all Pods to talk to all Pods — a flat, open network. NetworkPolicies are firewall rules for Pod traffic: they let you restrict which Pods can communicate, implementing zero-trust networking at the workload level. This is a critical CKS topic and a production security requirement.

1. Default Behavior & Default-Deny

Without NetworkPolicies

All Pods accept traffic from everywhere and can send traffic everywhere. No restrictions. This is the K8s default.

How NetworkPolicies Change This

The fundamental rule: If a Pod is selected by ANY NetworkPolicy, then traffic not explicitly allowed by a policy is denied. If a Pod has no NetworkPolicy selecting it, all traffic is allowed (default-open). NetworkPolicies are additive — you can't write a "deny" rule, only "allow" rules. Denial is implicit from selection.

Default-Deny All (The Starting Point for Zero-Trust)

# Deny ALL ingress to all Pods in this namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}              # ← Empty = selects ALL Pods in namespace
  policyTypes:
    - Ingress                  # ← Only ingress is restricted
  # No ingress rules = deny all inbound traffic
# Deny ALL egress from all Pods in this namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  # No egress rules = deny all outbound traffic
# Deny BOTH ingress and egress (full lockdown):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
If you deny all egress, Pods can't reach CoreDNS (port 53). You must explicitly allow DNS egress in a separate policy, or all service discovery breaks. This is the #1 gotcha with default-deny-egress.

Allow DNS (Required with Egress Deny)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
CKA/CKS pattern: Start with default-deny for the namespace, then allow DNS, then add specific allow rules. This is the zero-trust approach that exams expect you to implement.

2. NetworkPolicy Anatomy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-policy
  namespace: production
spec:
  podSelector:                 # WHO this policy applies to
    matchLabels:
      app: api
  policyTypes:                 # WHAT directions are controlled
    - Ingress
    - Egress
  ingress:                     # ALLOW rules for inbound traffic
    - from:                    # Source(s) — OR logic between items
        - podSelector:
            matchLabels:
              app: web
        - namespaceSelector:
            matchLabels:
              env: production
      ports:                   # Destination port(s) on the selected Pods
        - protocol: TCP
          port: 8080
  egress:                      # ALLOW rules for outbound traffic
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432

Key Components

FieldPurposeEmpty = ?
podSelectorWhich Pods the policy applies to{} = all Pods in namespace
policyTypesDirections to restrict (Ingress, Egress, or both)Inferred from which rules exist
ingress[].fromAllowed sourcesOmitted = allow from everywhere
egress[].toAllowed destinationsOmitted = allow to everywhere
portsAllowed ports/protocolsOmitted = all ports

The AND / OR Logic

This is the most confusing part — get it right:
• Multiple items in the from/to array → OR (any one match allows)
• Multiple selectors within one item → AND (all must match)
• Multiple ingress/egress rules → OR (any rule can permit the traffic)
# OR logic — two separate "from" items:
ingress:
  - from:
      - podSelector:           # ← Pods with app=web in same namespace
          matchLabels:
            app: web
      - namespaceSelector:     # ← OR any Pod in namespace with env=staging
          matchLabels:
            env: staging

# AND logic — combined in ONE item:
ingress:
  - from:
      - podSelector:           # ← Pods with app=web
          matchLabels:
            app: web
        namespaceSelector:     # ← AND in namespace with env=production
          matchLabels:
            env: production
    # Both conditions must be true (Pod must have app=web AND be in a namespace labeled env=production)
CKA/CKS trap: the difference between two items (OR) and two selectors in one item (AND) is a single dash (-) in the YAML. Misplacing it changes the policy from "allow from web Pods OR staging namespace" to "allow from web Pods IN staging namespace." Practice this until it's automatic.

3. Selector Types

SelectorMatchesExample
podSelectorPods in the same namespace as the policyAllow from Pods with app=web
namespaceSelectorAll Pods in namespaces matching labelsAllow from any Pod in env=prod namespaces
Both combined (AND)Specific Pods in specific namespacesAllow from app=web Pods in env=prod namespaces
ipBlockCIDR ranges (external traffic)Allow from 10.0.0.0/8 but not 10.0.1.0/24

podSelector Examples

# Allow ingress ONLY from Pods labeled "role=frontend" in same namespace:
ingress:
  - from:
      - podSelector:
          matchLabels:
            role: frontend
    ports:
      - port: 8080

namespaceSelector Examples

# Allow ingress from ANY Pod in namespaces labeled "team=platform":
ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            team: platform

# Allow from ALL namespaces (empty namespaceSelector):
ingress:
  - from:
      - namespaceSelector: {}       # All namespaces

ipBlock — CIDR Ranges

# Allow ingress from corporate network, excluding a specific subnet:
ingress:
  - from:
      - ipBlock:
          cidr: 10.0.0.0/8          # Allow this range
          except:
            - 10.0.1.0/24           # Except this subnet
    ports:
      - port: 443

# Allow egress to external API:
egress:
  - to:
      - ipBlock:
          cidr: 203.0.113.0/24      # External API IP range
    ports:
      - port: 443
ipBlock is for traffic entering/leaving the cluster. Pod-to-Pod traffic within the cluster uses Pod IPs which are dynamic — use podSelector and namespaceSelector for internal traffic. ipBlock is for external clients, external APIs, or known infrastructure IPs.

Namespace Labels (Important!)

For namespaceSelector to work, namespaces must have labels. K8s 1.21+ automatically adds kubernetes.io/metadata.name: <namespace-name> to every namespace. For custom labels:

# Label a namespace for policy selection:
kubectl label namespace monitoring purpose=observability

# Now you can target it:
namespaceSelector:
  matchLabels:
    purpose: observability

4. Practical Patterns

Pattern: Three-Tier Application

Frontend role=frontend API role=api Database role=db ✓ :8080 ✓ :5432 ✗ Frontend cannot reach DB directly
# Policy for API tier: only accept from frontend, only connect to DB
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      role: api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              role: frontend
      ports:
        - port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              role: db
      ports:
        - port: 5432
    - to:                          # Allow DNS
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - port: 53
          protocol: UDP
# Policy for DB: only accept from API
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      role: db
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              role: api
      ports:
        - port: 5432

Pattern: Allow Monitoring from Prometheus

# Allow Prometheus (in monitoring namespace) to scrape metrics:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-prometheus-scrape
  namespace: production
spec:
  podSelector: {}              # All Pods in this namespace
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoring
          podSelector:
            matchLabels:
              app: prometheus
      ports:
        - port: 9090           # Metrics port

Pattern: Allow Egress to External API Only

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-external-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      role: api
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 203.0.113.50/32    # Specific external API IP
      ports:
        - port: 443
    - to:                            # DNS (always needed)
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - port: 53
          protocol: UDP
In production zero-trust clusters, every namespace starts with default-deny (ingress + egress), then adds surgical allow rules. This is the "allowlist" model — nothing works until explicitly permitted. Combine with service mesh mTLS for defense in depth.

5. Gotchas & Common Mistakes

GotchaProblemFix
CNI doesn't enforceFlannel ignores NetworkPolicies — no enforcementUse Calico, Cilium, or Weave
Forgot DNS egressPods can't resolve Service names → connectivity appears brokenAlways add DNS egress rule when using egress deny
OR vs AND confusionPolicy is too open or too restrictiveSeparate - items = OR; combined selectors = AND
Namespace not labelednamespaceSelector doesn't matchLabel the namespace: kubectl label ns monitoring purpose=obs
Policy in wrong namespaceNetworkPolicy is namespaced — must be in same ns as target PodsVerify metadata.namespace matches target Pods
No policyTypes specifiedK8s infers from which rules exist — may not restrict what you expectAlways explicitly set policyTypes
Policies are additiveCan't write a "deny specific traffic" rule — only allowStart with deny-all, then add allow rules

6. Testing NetworkPolicies

# Deploy test Pods:
kubectl run test-web --image=busybox --labels="role=frontend" -- sleep 3600
kubectl run test-api --image=busybox --labels="role=api" -- sleep 3600

# Test connectivity (should succeed if policy allows):
kubectl exec test-web -- wget -qO- --timeout=2 http://api-svc:8080
# If blocked: wget: download timed out

# Test from a Pod that should be denied:
kubectl run test-attacker --image=busybox -- sleep 3600
kubectl exec test-attacker -- wget -qO- --timeout=2 http://api-svc:8080
# Should timeout (denied by policy)

# Use netshoot for more detailed testing:
kubectl run netshoot --image=nicolaka/netshoot -it --rm -- \
  curl -v --connect-timeout 3 http://10.244.1.5:8080
CKS exam workflow: (1) Apply default-deny. (2) Create allow rules. (3) Verify with kubectl exec ... -- wget --timeout=2 from both allowed and denied Pods. The --timeout=2 is crucial — without it, blocked requests hang for 30+ seconds wasting exam time.

Summary

ConceptKey Point
Default behaviorNo policies = all traffic allowed (flat open network)
Selection effectOnce selected by ANY policy, non-allowed traffic is denied
Default-denypodSelector: {} with empty rules = deny all (start here)
Additive onlyCan't deny specific traffic — only add allow rules
OR vs ANDSeparate array items = OR; combined selectors = AND
podSelectorTargets Pods in same namespace
namespaceSelectorTargets all Pods in matching namespaces
ipBlockCIDR ranges for external traffic
DNS ruleAlways allow UDP/TCP 53 to kube-dns when egress is denied
CNI requirementMust use a CNI that enforces policies (Calico, Cilium, NOT Flannel)

📝 Quiz: NetworkPolicies

Q1: You create a NetworkPolicy with podSelector: {}, policyTypes: [Ingress], and no ingress rules. What happens?

All ingress to all Pods in the namespace is denied. The empty podSelector matches all Pods. policyTypes: [Ingress] means ingress is controlled. No ingress rules = no traffic allowed in. Egress is unaffected (not listed in policyTypes).

Q2: What's the difference between these two from blocks?

  # Version A:            # Version B:
  from:                   from:
    - podSelector:          - podSelector:
        matchLabels:            matchLabels:
          app: web                app: web
    - namespaceSelector:      namespaceSelector:
        matchLabels:            matchLabels:
          env: prod               env: prod
Version A (two items, OR): Allow from Pods labeled app=web in the same namespace OR from any Pod in namespaces labeled env=prod.
Version B (one item, AND): Allow from Pods labeled app=web that are also in namespaces labeled env=prod. The difference is one YAML dash (-).

Q3: You applied default-deny-egress. Now Pods can't resolve DNS names. What's wrong and how do you fix it?

Egress deny blocks ALL outbound traffic, including DNS queries to CoreDNS (UDP/TCP port 53). Fix: add an egress rule allowing traffic to the kube-dns Pods in kube-system namespace on port 53. This is required whenever you deny egress.

Q4: You're using Flannel as your CNI. You create NetworkPolicies but they don't seem to block anything. Why?

Flannel doesn't implement NetworkPolicy enforcement. The API server happily accepts NetworkPolicy objects, but Flannel has no policy engine to enforce them. You need a CNI that supports NetworkPolicies: Calico, Cilium, or Weave Net. You can also run Calico as a policy-only add-on alongside Flannel.

Q5: A NetworkPolicy has podSelector: {matchLabels: {app: api}}. A Pod without any labels — is it affected by this policy?

No. The policy only applies to Pods matching its podSelector (Pods with app=api). Pods without that label are not selected, so the policy doesn't affect them — they remain in the default-open state (unless selected by another policy).

Q6: Can you write a NetworkPolicy that denies traffic from a specific Pod while allowing everything else?

Not directly. NetworkPolicies are additive (allow-only). You can't write a "deny from X" rule. The approach is: (1) Apply a default-deny (denies everything). (2) Add allow rules for everything except the Pod you want to block. This effectively denies only that Pod. It's the allowlist model, not a denylist.