CKS — NetworkPolicies for Cluster Access

10 minute read

Exam Context

Property Value
Domain Domain 1 — Cluster Setup
Weight 10%
Objective Use NetworkPolicy to restrict cluster-level access

NetworkPolicies are your first line of defense for in-cluster network segmentation. In the CKS exam, you’ll be expected to write policies from scratch — not just understand them. This topic goes beyond CKA-level NetworkPolicy knowledge to focus on security-critical patterns like default deny, DNS egress isolation, and kube-system namespace protection.

Why Network Segmentation Matters

Without NetworkPolicies, every Pod in a Kubernetes cluster can communicate with every other Pod — across all namespaces. This flat network model is a catastrophic security flaw.

Threat model:

Attack Vector Without NetworkPolicy With NetworkPolicy
Lateral movement Compromised Pod reaches all services Compromised Pod is isolated to its namespace
Data exfiltration Pod can connect to any external endpoint Egress restricted to known destinations
API server attack Any Pod can reach the API server Only authorized namespaces can reach kube-apiserver
kube-system access Any Pod can probe CoreDNS, etcd proxies kube-system is locked down

CKS perspective: The exam tests whether you can implement the principle of least privilege at the network layer. Start with deny-all, then open specific paths.

Prerequisites

NetworkPolicies require a CNI plugin that supports them. Not all CNI plugins do.

CNI Plugin NetworkPolicy Support
Calico Full support (most common in CKS environments)
Cilium Full support (L3/L4 and L7)
Weave Net Full support
Flannel No support — policies are silently ignored

Critical: If your cluster uses Flannel, NetworkPolicies will be created but have no effect. The CKS exam environment uses a policy-capable CNI.

Verify your CNI supports NetworkPolicy:

# Check which CNI is installed
ls /etc/cni/net.d/

# Look for Calico, Cilium, or Weave
kubectl get pods -n kube-system | grep -E "calico|cilium|weave"

Default Deny Ingress

The most important security pattern: deny all incoming traffic to a namespace by default.

Why Default Deny Ingress?

In a zero-trust model, no Pod should receive traffic unless explicitly allowed. Default deny ingress ensures that newly deployed workloads are isolated until you create a policy permitting specific traffic.

Manifest: default-deny-ingress.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}    # Selects ALL pods in the namespace
  policyTypes:
    - Ingress        # Only affects ingress; egress is untouched
  # No ingress rules = deny all ingress

Key points:

  • podSelector: {} with no match labels selects every Pod in the namespace
  • policyTypes: [Ingress] declares this policy governs ingress traffic
  • The absence of an ingress field means no ingress is allowed
  • Egress is unaffected — Pods can still make outbound connections

Apply and Verify

# Create the namespace
kubectl create namespace production

# Apply the policy
kubectl apply -f default-deny-ingress.yaml

# Verify it exists
kubectl get networkpolicy -n production

Expected output:

NAME                   POD-SELECTOR   AGE
default-deny-ingress   <none>         5s

Test the Policy

# Deploy a test Pod in the production namespace
kubectl run test-server --image=nginx --namespace=production --port=80

# Wait for it to be ready
kubectl wait --for=condition=ready pod/test-server -n production --timeout=60s

# Try to reach it from the default namespace (should fail/timeout)
kubectl run test-client --image=busybox --rm -it --restart=Never \
  -- wget --timeout=3 -q -O- http://test-server.production.svc.cluster.local

Expected output:

wget: download timed out
pod "test-client" deleted
pod default/test-client terminated (Error)

The connection times out because default-deny-ingress blocks all inbound traffic to the production namespace.

Default Deny Egress

Deny all outgoing traffic from a namespace. This prevents data exfiltration and limits blast radius.

Manifest: default-deny-egress.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: production
spec:
  podSelector: {}    # Selects ALL pods in the namespace
  policyTypes:
    - Egress         # Only affects egress; ingress is untouched
  # No egress rules = deny all egress

Apply and Verify

kubectl apply -f default-deny-egress.yaml

# Verify both policies are in place
kubectl get networkpolicy -n production

Expected output:

NAME                   POD-SELECTOR   AGE
default-deny-egress    <none>         3s
default-deny-ingress   <none>         2m

Test Egress Denial

# From a Pod in the production namespace, try to reach the internet
kubectl run egress-test --image=busybox --namespace=production --rm -it --restart=Never \
  -- wget --timeout=3 -q -O- https://kubernetes.io

Expected output:

wget: bad address 'kubernetes.io'

The Pod cannot even resolve DNS because all egress is blocked — including DNS queries to CoreDNS.

Allow DNS Egress — Critical for CKS

When you apply default-deny-egress, you break DNS resolution. Almost every workload needs DNS. The standard pattern is to re-enable DNS egress while keeping everything else blocked.

This is one of the most commonly tested patterns on the CKS exam.

Manifest: allow-dns-egress.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector: {}    # Applies to ALL pods in the namespace
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector: {}    # Any namespace (CoreDNS runs in kube-system)
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Why both UDP and TCP port 53?

  • DNS primarily uses UDP/53 for queries under 512 bytes
  • DNS falls back to TCP/53 for large responses (DNSSEC, long TXT records)
  • The CKS exam expects you to include both protocols

Apply and Test

kubectl apply -f allow-dns-egress.yaml

# Now DNS works, but other egress is still blocked
kubectl run dns-test --image=busybox --namespace=production --rm -it --restart=Never \
  -- nslookup kubernetes.default.svc.cluster.local

Expected output:

Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      kubernetes.default.svc.cluster.local
Address 1: 10.96.0.1 kubernetes.default.svc.cluster.local

DNS resolution works. But other egress is still blocked:

kubectl run egress-test2 --image=busybox --namespace=production --rm -it --restart=Never \
  -- wget --timeout=3 -q -O- https://kubernetes.io
wget: download timed out

DNS resolves the name, but the actual HTTP connection is blocked by the default-deny-egress policy.

Restricting API Server Access

The Kubernetes API server is the most critical component in the cluster. By default, any Pod can reach it. You should restrict which namespaces can communicate with the API server.

Manifest: restrict-api-server-access.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: restrict-api-server-access
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    # Allow DNS
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    # Deny access to API server (port 6443)
    # Note: This works by NOT including port 6443 in allowed egress
    # The default-deny-egress policy already blocks it
    # This policy maintains DNS while keeping API server blocked

Important: The default-deny-egress policy already blocks API server access. The allow-dns-egress policy only opens port 53. The API server (typically port 6443) remains blocked.

To explicitly verify:

# Get the API server IP
kubectl get endpoints kubernetes -n default
NAME         ENDPOINTS           AGE
kubernetes   192.168.1.100:6443  45d
# Try to reach the API server from a Pod in production namespace
kubectl run api-test --image=busybox --namespace=production --rm -it --restart=Never \
  -- wget --timeout=3 -q -O- https://192.168.1.100:6443
wget: download timed out

The Pod cannot reach the API server. This prevents a compromised Pod from querying the API for cluster information.

Protecting kube-system Namespace

The kube-system namespace runs critical infrastructure: CoreDNS, kube-proxy, the CNI plugin, and potentially etcd. Unauthorized access to these components can compromise the entire cluster.

Manifest: protect-kube-system.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: protect-kube-system
  namespace: kube-system
spec:
  podSelector: {}    # Protect ALL pods in kube-system
  policyTypes:
    - Ingress
  ingress:
    # Allow DNS queries from all namespaces (CoreDNS must be reachable)
    - from:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    # Allow kube-system internal communication
    - from:
        - podSelector: {}    # Pods within kube-system can talk to each other

What this policy does:

  • Denies all ingress to kube-system except:
    • DNS queries (port 53) from any namespace — CoreDNS must remain accessible
    • Internal kube-system traffic — control plane components need to communicate
  • Blocks direct access to kube-proxy, metrics endpoints, and other kube-system services from application namespaces

Apply and Verify

kubectl apply -f protect-kube-system.yaml

# Verify the policy
kubectl describe networkpolicy protect-kube-system -n kube-system

Testing kube-system Protection

# DNS should still work (port 53 is allowed)
kubectl run dns-check --image=busybox --rm -it --restart=Never \
  -- nslookup kubernetes.default

# Trying to reach kube-system metrics should fail
kubectl run probe-test --image=busybox --rm -it --restart=Never \
  -- wget --timeout=3 -q -O- http://10.96.0.10:9153/metrics

DNS resolves successfully, but the metrics endpoint on CoreDNS (port 9153) is blocked.

Combined Security Pattern

For maximum security, apply all four policies together. Here’s the recommended order:

# Step 1: Default deny all ingress
kubectl apply -f default-deny-ingress.yaml

# Step 2: Default deny all egress
kubectl apply -f default-deny-egress.yaml

# Step 3: Re-enable DNS egress
kubectl apply -f allow-dns-egress.yaml

# Step 4: Protect kube-system
kubectl apply -f protect-kube-system.yaml

# Verify all policies
kubectl get networkpolicy -n production
kubectl get networkpolicy -n kube-system

Expected output for production namespace:

NAME                   POD-SELECTOR   AGE
allow-dns-egress       <none>         10s
default-deny-egress    <none>         15s
default-deny-ingress   <none>         20s

How Policies Combine

NetworkPolicies are additive — they are unioned, not intersected. This means:

  1. default-deny-ingress blocks all incoming traffic
  2. default-deny-egress blocks all outgoing traffic
  3. allow-dns-egress adds an exception for DNS (port 53)
  4. Any additional policy that allows specific traffic adds to what’s permitted

No policy can override a deny. If you have default-deny-ingress and want to allow traffic from a specific source, you create a new policy with an ingress rule that matches that source. The new policy doesn’t cancel the deny — it adds an explicit allow.

Exam Tips

Tip Details
Always start with default deny Apply both ingress and egress deny before writing allow rules
Never forget DNS If egress is denied, you must explicitly allow UDP/53 and TCP/53
Empty podSelector = all pods podSelector: {} selects everything in the namespace
Check CNI support Know that Flannel doesn’t enforce NetworkPolicy
Test your policies Use kubectl run with busybox to verify connectivity
Namespace scope NetworkPolicies are namespaced — you need one per namespace
Read the question carefully CKS questions may ask you to deny or allow — don’t mix them up

Practice Questions


Q1: Write a NetworkPolicy that denies all ingress and egress in the secure namespace.

Show Answer
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
  namespace: secure
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Key points:

  • podSelector: {} selects all Pods
  • Both Ingress and Egress in policyTypes
  • No ingress or egress rules = deny everything
  • This is the most restrictive starting point — nothing gets in or out

Q2: After applying a default-deny-egress policy, Pods can’t resolve DNS. Write a policy to fix this while keeping all other egress blocked.

Show Answer
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: secure
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Why namespaceSelector: {}? CoreDNS runs in the kube-system namespace. Using namespaceSelector: {} allows DNS queries to reach any namespace, which covers CoreDNS regardless of where it runs.

Why both UDP and TCP?

  • UDP/53 handles standard DNS queries
  • TCP/53 handles responses over 512 bytes and zone transfers
  • Including both is required for complete DNS functionality

Q3: What happens if you apply a NetworkPolicy in a cluster using Flannel as the CNI plugin?

Show Answer

The NetworkPolicy resource is created successfully (kubectl shows no errors), but it has no effect. Flannel does not implement the NetworkPolicy API.

This is a critical distinction:

  • The Kubernetes API accepts the resource because it’s a valid API object
  • The CNI plugin is responsible for enforcing the rules
  • Flannel does not read or enforce NetworkPolicy rules
  • Traffic continues flowing as if no policy exists

CKS exam context: The exam uses a CNI that supports NetworkPolicy (typically Calico). But understanding this distinction demonstrates depth of knowledge about how NetworkPolicies work under the hood.

To verify CNI support:

kubectl get pods -n kube-system | grep -E "calico|cilium|weave"

Q4: Write a NetworkPolicy that allows only Pods with the label role: frontend in the web namespace to receive ingress on port 443.

Show Answer
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-443
  namespace: web
spec:
  podSelector:
    matchLabels:
      role: frontend
  policyTypes:
    - Ingress
  ingress:
    - ports:
        - protocol: TCP
          port: 443

Explanation:

  • podSelector targets only Pods with role: frontend
  • The ingress rule allows traffic on TCP/443 from any source (no from field means all sources)
  • If you also had a default-deny-ingress policy, this would be the only allowed inbound path for frontend Pods
  • Other Pods in the web namespace without the role: frontend label would still be governed by the default-deny policy

Q5: Explain why you should protect the kube-system namespace with NetworkPolicies and write a policy that only allows DNS queries into kube-system.

Show Answer

Why protect kube-system:

  • kube-system hosts CoreDNS, kube-proxy, CNI pods, and potentially metrics-server
  • A compromised application Pod could probe kube-system services for vulnerabilities
  • CoreDNS metrics endpoint (port 9153) can leak information about cluster DNS queries
  • kube-proxy configuration can reveal cluster networking details
  • An attacker with access to kube-system can pivot to control plane attacks

Policy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: restrict-kube-system-ingress
  namespace: kube-system
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    - from:
        - podSelector: {}

This allows:

  1. DNS queries (port 53 UDP/TCP) from any namespace
  2. All internal kube-system communication (control plane components)

Everything else — metrics scraping, direct Pod probing, unauthorized service access — is denied.


← Cluster Setup Overview Ingress TLS →

Updated: