CKS — Cluster Setup & Hardening Lab
Lab Overview
This hands-on lab brings together all Cluster Setup topics into a cohesive exercise. You’ll implement the security controls that defend a Kubernetes cluster at the network and platform level — the same controls tested in Domain 1 of the CKS exam.
Estimated time: 45–60 minutes
Prerequisites
| Requirement | Details |
|---|---|
| Kubernetes cluster | kubeadm, minikube (--cni=calico), or kind with Calico |
| CNI with NetworkPolicy | Calico, Cilium, or Weave Net (Flannel will NOT work) |
| kubectl | Configured and connected to the cluster |
| openssl | Installed on your workstation |
| curl/wget | Available for download verification |
Verify Lab Readiness
# Verify cluster is running
kubectl cluster-info
# Verify CNI supports NetworkPolicy
kubectl get pods -n kube-system | grep -E "calico|cilium|weave"
# Verify kubectl is working
kubectl get nodes
What You’ll Practice
- Create default deny NetworkPolicy for a namespace
- Create allow-dns-egress policy
- Block cloud metadata access with NetworkPolicy
- Create TLS Secret and Ingress with TLS
- Verify kubectl binary integrity with sha512sum
- Verify Dashboard access restrictions
Exercise 1: Default Deny NetworkPolicy
Objective: Create a namespace called secure-app and apply a default deny policy for both ingress and egress.
Step 1.1: Create the Namespace
kubectl create namespace secure-app
kubectl label namespace secure-app purpose=lab
Step 1.2: Deploy Test Workloads
# Deploy a web server
kubectl run web --image=nginx --namespace=secure-app --port=80 \
--labels="app=web,tier=frontend"
# Deploy a backend
kubectl run api --image=nginx --namespace=secure-app --port=8080 \
--labels="app=api,tier=backend"
# Wait for Pods to be ready
kubectl wait --for=condition=ready pod/web -n secure-app --timeout=60s
kubectl wait --for=condition=ready pod/api -n secure-app --timeout=60s
# Verify Pods are running
kubectl get pods -n secure-app -o wide
Expected output:
NAME READY STATUS RESTARTS AGE IP NODE
api 1/1 Running 0 15s 10.244.1.12 node01
web 1/1 Running 0 20s 10.244.1.11 node01
Step 1.3: Verify Unrestricted Communication (Before Policy)
# Get the web Pod IP
WEB_IP=$(kubectl get pod web -n secure-app -o jsonpath='{.status.podIP}')
# Test: api can reach web (should succeed — no policies yet)
kubectl exec -n secure-app api -- wget --timeout=3 -q -O- http://${WEB_IP}
You should see the NGINX welcome page HTML.
Step 1.4: Apply Default Deny Ingress and Egress
Create a file default-deny-all.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: secure-app
spec:
podSelector: {}
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: secure-app
spec:
podSelector: {}
policyTypes:
- Egress
kubectl apply -f default-deny-all.yaml
Step 1.5: Verify Policies Are Enforced
# Test: api can NO LONGER reach web (ingress denied)
kubectl exec -n secure-app api -- wget --timeout=3 -q -O- http://${WEB_IP}
# Expected: wget: download timed out (command exits with error)
# Test: web can NO LONGER reach external sites (egress denied)
kubectl exec -n secure-app web -- wget --timeout=3 -q -O- http://example.com
# Expected: wget: bad address 'example.com' (DNS blocked)
Expected Result
NAME POD-SELECTOR AGE
default-deny-egress <none> 10s
default-deny-ingress <none> 10s
Both ingress and egress are denied for all Pods in secure-app.
Exercise 2: Allow DNS Egress
Objective: Re-enable DNS resolution while keeping all other egress blocked.
Step 2.1: Apply DNS Egress Policy
Create a file allow-dns.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: secure-app
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
kubectl apply -f allow-dns.yaml
Step 2.2: Verify DNS Works
# DNS should now resolve
kubectl exec -n secure-app web -- nslookup kubernetes.default
Expected output:
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name: kubernetes.default
Address 1: 10.96.0.1 kubernetes.default.svc.cluster.local
Step 2.3: Verify Other Egress Still Blocked
# HTTP to external should still fail (DNS resolves, but connection blocked)
kubectl exec -n secure-app web -- wget --timeout=3 -q -O- http://example.com
# Expected: wget: download timed out
Expected Result
DNS resolution works. All other egress remains blocked. The Pods can resolve names but cannot connect to those IPs.
Exercise 3: Block Cloud Metadata Access
Objective: Create a NetworkPolicy that explicitly blocks the cloud metadata endpoint while allowing other egress.
Step 3.1: Apply Metadata Blocking Policy
Create a file block-metadata.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-cloud-metadata
namespace: secure-app
spec:
podSelector: {}
policyTypes:
- Egress
egress:
# DNS
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# All egress EXCEPT metadata
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
kubectl apply -f block-metadata.yaml
Step 3.2: Verify Metadata Is Blocked
# Attempt to reach the metadata endpoint (should timeout)
kubectl exec -n secure-app web -- wget --timeout=3 -q -O- http://169.254.169.254/latest/meta-data/
# Expected: wget: download timed out
Step 3.3: Verify Other Egress Works
# Regular external access should now work (because block-metadata allows 0.0.0.0/0 except metadata)
kubectl exec -n secure-app web -- wget --timeout=3 -q -O- http://example.com
You should see the HTML content from example.com. The block-cloud-metadata policy adds an egress rule allowing all IPs except the metadata endpoint.
Step 3.4: Verify All Policies
kubectl get networkpolicy -n secure-app
Expected output:
NAME POD-SELECTOR AGE
allow-dns-egress <none> 5m
block-cloud-metadata <none> 30s
default-deny-egress <none> 8m
default-deny-ingress <none> 8m
Exercise 4: Create TLS Secret and Ingress
Objective: Generate a TLS certificate, create a Kubernetes Secret, and configure an Ingress with TLS termination.
Step 4.1: Generate a Self-Signed Certificate
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout tls.key \
-out tls.crt \
-subj "/CN=secure-app.example.com/O=lab"
Step 4.2: Create the TLS Secret
kubectl create secret tls secure-app-tls \
--cert=tls.crt \
--key=tls.key \
--namespace=secure-app
Step 4.3: Verify the Secret
kubectl get secret secure-app-tls -n secure-app
Expected output:
NAME TYPE DATA AGE
secure-app-tls kubernetes.io/tls 2 5s
# Check the Secret details
kubectl describe secret secure-app-tls -n secure-app
Expected output:
Name: secure-app-tls
Namespace: secure-app
Type: kubernetes.io/tls
Data
====
tls.crt: 1164 bytes
tls.key: 1704 bytes
Step 4.4: Create a Service for the Web Pod
kubectl expose pod web --port=80 --name=web-service --namespace=secure-app
Step 4.5: Create the Ingress with TLS
Create a file ingress-tls.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: secure-app-ingress
namespace: secure-app
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- secure-app.example.com
secretName: secure-app-tls
rules:
- host: secure-app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
kubectl apply -f ingress-tls.yaml
Step 4.6: Verify the Ingress
kubectl get ingress -n secure-app
Expected output:
NAME CLASS HOSTS ADDRESS PORTS AGE
secure-app-ingress nginx secure-app.example.com 80, 443 5s
kubectl describe ingress secure-app-ingress -n secure-app
Expected output includes:
TLS:
secure-app-tls terminates secure-app.example.com
Rules:
Host Path Backends
---- ---- --------
secure-app.example.com / web-service:80 (10.244.1.11:80)
Expected Result
The Ingress is configured with TLS termination using the self-signed certificate. HTTPS traffic to secure-app.example.com will be decrypted at the Ingress controller and forwarded to the backend as HTTP.
Exercise 5: Verify kubectl Binary Integrity
Objective: Verify that the kubectl binary on your workstation hasn’t been tampered with.
Step 5.1: Get the Installed Version
KUBECTL_VERSION=$(kubectl version --client -o json 2>/dev/null | grep gitVersion | awk -F'"' '{print $4}')
echo "kubectl version: $KUBECTL_VERSION"
Step 5.2: Download the Official Checksum
# Download the sha256 checksum for the installed version
curl -sLO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256"
echo "Official sha256: $(cat kubectl.sha256)"
Step 5.3: Compute the Hash of the Installed Binary
# Find kubectl location
KUBECTL_PATH=$(which kubectl)
echo "kubectl path: $KUBECTL_PATH"
# Compute its sha256
sha256sum "$KUBECTL_PATH"
Step 5.4: Compare Hashes
INSTALLED_HASH=$(sha256sum "$KUBECTL_PATH" | awk '{print $1}')
OFFICIAL_HASH=$(cat kubectl.sha256)
echo "Installed: $INSTALLED_HASH"
echo "Official: $OFFICIAL_HASH"
if [[ "$INSTALLED_HASH" == "$OFFICIAL_HASH" ]]; then
echo "PASS: kubectl binary is authentic"
else
echo "FAIL: kubectl binary hash mismatch — possible tampering!"
fi
Step 5.5: Verify Using sha512sum
# Compute sha512 of the installed binary
sha512sum "$KUBECTL_PATH"
# The exam may give you a sha512 hash and ask you to compare
# The process is identical to sha256 verification
Expected Result
The hashes match, confirming the binary is authentic. If they don’t match, the binary has been modified.
Note: On macOS, use shasum -a 256 instead of sha256sum. On the CKS exam (Linux), sha256sum is available.
Exercise 6: Verify Dashboard Access Restrictions
Objective: If the Kubernetes Dashboard is installed, verify it has proper access controls. If not installed, create the RBAC resources that would restrict it.
Step 6.1: Check if Dashboard Is Installed
kubectl get namespace kubernetes-dashboard 2>/dev/null
kubectl get pods -n kubernetes-dashboard 2>/dev/null
Step 6.2: If Dashboard Is Installed — Audit It
# Check the Service type (should be ClusterIP)
kubectl get svc -n kubernetes-dashboard
# If NodePort or LoadBalancer → SECURITY RISK
# Check RBAC bindings
kubectl get clusterrolebinding | grep dashboard
# Check if cluster-admin is bound
kubectl get clusterrolebinding -o json | \
jq '.items[] | select(.roleRef.name=="cluster-admin") |
select(.subjects[]?.namespace=="kubernetes-dashboard") | .metadata.name'
Step 6.3: If Dashboard Is NOT Installed — Create Restricted RBAC
Create a file dashboard-rbac.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: dashboard-readonly
namespace: secure-app
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: dashboard-viewer-lab
rules:
- apiGroups: [""]
resources: ["pods", "services", "namespaces", "events"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: dashboard-viewer-lab-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: dashboard-viewer-lab
subjects:
- kind: ServiceAccount
name: dashboard-readonly
namespace: secure-app
kubectl apply -f dashboard-rbac.yaml
Step 6.4: Verify RBAC Restrictions
# Should be able to list Pods (read-only)
kubectl auth can-i list pods \
--as=system:serviceaccount:secure-app:dashboard-readonly
# Expected: yes
# Should NOT be able to delete Pods
kubectl auth can-i delete pods \
--as=system:serviceaccount:secure-app:dashboard-readonly
# Expected: no
# Should NOT be able to get Secrets
kubectl auth can-i get secrets \
--as=system:serviceaccount:secure-app:dashboard-readonly
# Expected: no
# Should NOT be able to exec into Pods
kubectl auth can-i create pods/exec \
--as=system:serviceaccount:secure-app:dashboard-readonly
# Expected: no
# Should NOT be able to create Deployments
kubectl auth can-i create deployments \
--as=system:serviceaccount:secure-app:dashboard-readonly
# Expected: no
Expected Result
The ServiceAccount has read-only access. It can list and watch resources but cannot create, modify, delete, or access sensitive resources like Secrets.
Lab Summary
| Exercise | Control | Status |
|---|---|---|
| 1 | Default deny ingress & egress | Verified — all traffic blocked |
| 2 | Allow DNS egress | Verified — DNS works, other egress blocked |
| 3 | Block metadata endpoint | Verified — 169.254.169.254 blocked |
| 4 | Ingress TLS | Verified — TLS Secret and Ingress configured |
| 5 | Binary verification | Verified — sha256 hash matches official checksum |
| 6 | Dashboard RBAC | Verified — read-only access, no secrets/exec |
Lab Cleanup
# Delete all lab resources
kubectl delete namespace secure-app
# Delete cluster-scoped resources
kubectl delete clusterrole dashboard-viewer-lab 2>/dev/null
kubectl delete clusterrolebinding dashboard-viewer-lab-binding 2>/dev/null
# Clean up local files
rm -f tls.key tls.crt kubectl.sha256
# Verify cleanup
kubectl get namespace secure-app 2>/dev/null
# Expected: Error from server (NotFound)
Practice Questions
Q1: You enter a CKS exam question that says: “Create a NetworkPolicy in the payments namespace that denies all ingress and egress, then allow only DNS egress.” Write the complete solution.
Show Answer
# Step 1: Default deny all
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Step 2: Allow DNS egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: payments
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# Apply
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: payments
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector: {}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
EOF
# Verify
kubectl get networkpolicy -n payments
Exam speed tip: Use heredoc with kubectl apply -f - to create both policies in one command. This saves time compared to creating separate files.
Q2: During a lab exercise, you applied a default-deny-egress policy and an allow-dns policy, but DNS still doesn’t work. What should you check?
Show Answer
Troubleshooting checklist:
- Verify the CNI supports NetworkPolicy:
kubectl get pods -n kube-system | grep -E "calico|cilium|weave"If using Flannel or a non-policy CNI, policies have no effect.
- Check the policy is in the correct namespace:
kubectl get networkpolicy -n <your-namespace> - Verify the allow-dns policy YAML:
- Port is
53(not5353) - Protocols include both
UDPandTCP namespaceSelector: {}is present (not a specific label selector)
- Port is
- Check CoreDNS is running:
kubectl get pods -n kube-system -l k8s-app=kube-dns - Check CoreDNS Service:
kubectl get svc -n kube-system kube-dns - Test DNS directly by IP (bypass name resolution):
kubectl exec -n <namespace> <pod> -- wget --timeout=3 -q -O- http://10.96.0.10:53If this also fails, the egress policy isn’t allowing port 53.
- Check for conflicting policies that might override:
kubectl get networkpolicy -n <namespace> -o yaml
Q3: Write the commands to verify that kubeadm on your CKS exam node is untampered, given that the version is v1.29.2.
Show Answer
# Step 1: Verify the installed version matches expectations
kubeadm version -o short
# Expected: v1.29.2
# Step 2: Compute the hash of the installed binary
sha256sum $(which kubeadm)
# Example: e4f5a6b7c8d9... /usr/bin/kubeadm
# Step 3: Download the official checksum
curl -sL "https://dl.k8s.io/release/v1.29.2/bin/linux/amd64/kubeadm.sha256" -o kubeadm-official.sha256
# Step 4: Compare
echo "$(cat kubeadm-official.sha256) $(which kubeadm)" | sha256sum --check
# Expected: /usr/bin/kubeadm: OK
If the check fails: The binary has been modified. Replace it:
curl -LO "https://dl.k8s.io/release/v1.29.2/bin/linux/amd64/kubeadm"
echo "$(cat kubeadm-official.sha256) kubeadm" | sha256sum --check
chmod +x kubeadm
sudo mv kubeadm /usr/bin/kubeadm
Q4: Create a TLS Secret named api-tls in namespace production for the domain api.company.com, then write an Ingress that uses it. Use one-liners where possible.
Show Answer
# Create namespace
kubectl create namespace production
# Generate cert + key + create Secret in one pipeline
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /tmp/tls.key -out /tmp/tls.crt \
-subj "/CN=api.company.com" && \
kubectl create secret tls api-tls \
--cert=/tmp/tls.crt --key=/tmp/tls.key \
-n production
# Create Ingress
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- api.company.com
secretName: api-tls
rules:
- host: api.company.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 8080
EOF
# Verify
kubectl get secret api-tls -n production
kubectl get ingress api-ingress -n production
kubectl describe ingress api-ingress -n production | grep -A2 "TLS"
Q5: What is the complete list of Security controls you implemented in this lab, and which CKS exam objectives do they map to?
Show Answer
| Lab Exercise | Security Control | CKS Exam Objective |
|---|---|---|
| Exercise 1 | Default deny ingress & egress | Use NetworkPolicy to restrict cluster-level access |
| Exercise 2 | DNS egress allowance | Use NetworkPolicy to restrict cluster-level access |
| Exercise 3 | Metadata endpoint blocking | Protect node metadata and endpoints |
| Exercise 4 | TLS Secret + Ingress with TLS | Properly set up Ingress objects with security control |
| Exercise 5 | Binary hash verification | Verify platform binaries before deploying |
| Exercise 6 | Dashboard RBAC restriction | Minimize use of, and access to, GUI elements |
All six exercises map to Domain 1 — Cluster Setup (10%).
Defense-in-depth layers implemented:
- Network layer — Default deny prevents lateral movement (Exercise 1)
- Service discovery — DNS is explicitly allowed so workloads function (Exercise 2)
- Cloud integration — Metadata endpoint blocked to prevent credential theft (Exercise 3)
- Transport encryption — TLS on Ingress prevents traffic interception (Exercise 4)
- Supply chain — Binary verification prevents tampered components (Exercise 5)
- Access control — GUI restricted to prevent unauthorized cluster management (Exercise 6)
Together, these controls implement a zero-trust network model with verified platform integrity and least-privilege access.
| ← Binary Verification | Cluster Hardening → |