CKS — NetworkPolicies for Cluster Access
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 namespacepolicyTypes: [Ingress]declares this policy governs ingress traffic- The absence of an
ingressfield 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:
default-deny-ingressblocks all incoming trafficdefault-deny-egressblocks all outgoing trafficallow-dns-egressadds an exception for DNS (port 53)- 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
IngressandEgressinpolicyTypes - No
ingressoregressrules = 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:
podSelectortargets only Pods withrole: frontend- The
ingressrule allows traffic on TCP/443 from any source (nofromfield 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
webnamespace without therole: frontendlabel 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-systemhosts 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:
- DNS queries (port 53 UDP/TCP) from any namespace
- 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 → |