CertGrid CertGrid
Hands-on Lab·Certified Kubernetes Security Specialist

The CKS Practice Clusters

This path runs on three clusters rather than one, because CKS work is meant to damage things and a cluster you are about to break should not be the one another path depends on. This guide reads all three, shows what is installed on them - nothing - and finds the one setting that defines the whole path.

Orientation Guide 3 of 40 Beginner

Written against the versions above. All three were rebuilt from scratch with kubeadm and Cilium 1.18.1, and verified afterwards rather than taken from a build sheet. Every command here is read-only.

The three CKS clusters. Destructive work gets its own hardware, which is why KCSA's read-mostly cluster is not on this list.
Server NameIP AddressOSRolesCPURAMHDD
CKA6001192.168.0.46Ubuntu 26.04 LTSControl Plane Node (tainted NoSchedule)2 Core4 GB50 GB
CKA6001-NODE01192.168.0.47Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA6001-NODE02192.168.0.48Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA7001192.168.0.51Ubuntu 26.04 LTSControl Plane Node (tainted NoSchedule)2 Core4 GB50 GB
CKA7001-NODE01192.168.0.52Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA8001192.168.0.56Ubuntu 26.04 LTSSingle Node (control plane, untainted)2 Core4 GB50 GB

Before you start

  1. The cluster most of this path runs on

    Nodes, roles and the taint, then the version skew.

    bash Example session
    kubectl get nodes -o custom-columns='NAME:.metadata.name,STATUS:.status.conditions[-1].type,ROLES:.metadata.labels.node-role\.kubernetes\.io/control-plane,KUBELET:.status.nodeInfo.kubeletVersion,TAINT:.spec.taints[0].key' --no-headers; echo "--- one control plane and two schedulable workers. Two is the minimum for anything that has to prove traffic crossed a node boundary"cka6001          Ready            v1.36.3   node-role.kubernetes.io/control-planecka6001-node01   Ready   <none>   v1.36.3   <none>cka6001-node02   Ready   <none>   v1.36.3   <none>--- one control plane and two schedulable workers. Two is the minimum for anything that has to prove traffic crossed a node boundaryecho -n "apiserver "; kubectl get --raw /version | tr -d ' ' | grep -o 'gitVersion":"[^"]*' | cut -d'"' -f3; echo -n "kubelet   "; kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.kubeletVersion}'; echo; echo "--- the kubelet is one patch BEHIND the apiserver, which is the supported direction. The reverse is not"apiserver v1.36.4kubelet   v1.36.3--- the kubelet is one patch BEHIND the apiserver, which is the supported direction. The reverse is not

    Expected resultcka6001 tainted node-role.kubernetes.io/control-plane with two schedulable workers, then apiserver v1.36.4 against kubelet v1.36.3.

    Success conditionYou know the shape of the cluster the examples run on.

  2. The other two, and why there are three

    Two smaller clusters, and one field worth noticing.

    bash Example session
    kubectl get nodes -o custom-columns='NAME:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion,TAINT:.spec.taints[0].key' --no-headers; echo "--- one control plane and exactly one worker, for anything that needs a second node and no ambiguity about which one"cka7001          v1.36.3   node-role.kubernetes.io/control-planecka7001-node01   v1.36.3   <none>--- one control plane and exactly one worker, for anything that needs a second node and no ambiguity about which one

    Expected resultcka7001 with one control plane and exactly one worker.

    Success conditionYou know which cluster an exercise will name and why.

  3. The single node, and the taint it does not have

    One machine, and the field that makes it usable.

    bash Example session
    kubectl get nodes -o custom-columns='NAME:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion,TAINT:.spec.taints[0].key' --no-headers; echo "--- a SINGLE node, and note the taint column. A one-node cluster that refuses to schedule is no use, so this control plane is deliberately untainted"cka8001   v1.36.3   <none>--- a SINGLE node, and note the taint column. A one-node cluster that refuses to schedule is no use, so this control plane is deliberately untainted

    Expected resultcka8001 v1.36.3 <none> - a single node whose control plane carries no taint.

    Success conditionYou can explain why one control plane in three is untainted.

  4. What is installed on them, which is nothing

    Four counts, and an inventory.

    bash Example session
    echo -n "namespaces:        "; kubectl get ns --no-headers | wc -l; echo -n "network policies:  "; kubectl get netpol,cnp -A --no-headers 2>/dev/null | wc -l; echo -n "admission webhooks:"; kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations --no-headers 2>/dev/null | wc -l; echo -n "storage classes:   "; kubectl get storageclass --no-headers 2>/dev/null | wc -l; echo "--- nothing has been installed on top. Every control this path teaches is one it puts there"namespaces:        5network policies:  0admission webhooks:0storage classes:   0--- nothing has been installed on top. Every control this path teaches is one it puts thereecho -n "pods running: "; kubectl get pods -A --no-headers | wc -l; echo "static control-plane pods:"; kubectl get pods -n kube-system --no-headers | grep -E 'etcd|kube-apiserver|kube-controller|kube-scheduler' | awk '{printf "  %s", $1}'; echo; echo "everything else is one of these controllers:"; kubectl get ds,deploy -A --no-headers | awk '{printf "  %s", $2}'; echopods running: 18static control-plane pods:  etcd-cka6001  kube-apiserver-cka6001  kube-controller-manager-cka6001  kube-scheduler-cka6001everything else is one of these controllers:  daemonset.apps/cilium  daemonset.apps/cilium-envoy  daemonset.apps/kube-proxy  deployment.apps/cilium-operator  deployment.apps/coredns  deployment.apps/hubble-relay  deployment.apps/hubble-ui

    Expected result5 namespaces, 0 network policies, 0 admission webhooks, 0 storage classes. Then 18 pods: four static control-plane pods and seven controllers.

    Success conditionYou can say exactly what a control demonstrated later was added by.

  5. The CNI, and the one setting that defines this path

    Four lines of status, and the resource kinds the CNI adds.

    bash Example session
    kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg status 2>/dev/null | grep -E 'KubeProxyReplacement|Encryption|Hubble|Host firewall'; echo "--- Cilium, with Hubble on and encryption OFF. CKS domain 4 names pod-to-pod encryption explicitly, so that last line is a task this path has to complete rather than describe"KubeProxyReplacement:    FalseHost firewall:           DisabledHubble:                  Ok              Current/Max Flows: 4095/4095 (100.00%), Flows/s: 3.84   Metrics: DisabledEncryption:              Disabled--- Cilium, with Hubble on and encryption OFF. CKS domain 4 names pod-to-pod encryption explicitly, so that last line is a task this path has to complete rather than describekubectl api-resources --api-group=cilium.io --no-headers 2>/dev/null | awk '{printf "%s ", $1}'; echo; echo "--- policy kinds that exist only because the CNI is Cilium. CiliumNetworkPolicy is what makes L7 rules and pod-to-pod encryption possible here"ciliumcidrgroups ciliumclusterwidenetworkpolicies ciliumendpoints ciliumidentities ciliuml2announcementpolicies ciliumloadbalancerippools ciliumnetworkpolicies ciliumnodeconfigs ciliumnodes ciliumpodippools--- policy kinds that exist only because the CNI is Cilium. CiliumNetworkPolicy is what makes L7 rules and pod-to-pod encryption possible here

    Expected resultKubeProxyReplacement: False, Host firewall: Disabled, Hubble: Ok, and Encryption: Disabled. Then ten cilium.io resource kinds.

    Success conditionYou know what the CNI can do here, and what it is not yet doing.

Troubleshooting

Official sources