CKS Cluster Hardening — the API that still answers
Set Node,RBAC. Set anonymous-auth=false. Enable NodeRestriction. Delete the anonymous binding.
<!-- hal:authoritative:yaml -->
Set Node,RBAC. Set anonymous-auth=false. Enable NodeRestriction. Delete the anonymous binding. The API still answers until all four are true.
§I — Frame
k8s_day_counter reads 11. Odd is CKS. 08-19 closed the store half of Minimize Microservice Vulnerabilities and wrote the reservation in its pairing section: the next CKS-emphasis fire can take Cluster Hardening Q04. That fire is this file.
CKS blueprint leftovers entering today were Cluster Hardening 15% and System Hardening 15%. Official order lists Cluster Hardening first. Q04 is the restrict-API stem. Trust the runbook. System Hardening (AppArmor, seccomp, host OS footprint) stays closed. Do not spend this clock on Q01.
08-04 already taught CKA RBAC as objects you mint. Today is CKS RBAC as objects you remove and flags you restore. 08-01 already spent kube-bench, ingress TLS, and metadata. Those are Cluster Setup. Today's CIS overlap is one line: AlwaysAllow is never a production authorizer. Do not rerun kube-bench.
The day's Ops lesson is AKS: authorized IP ranges, disable local accounts, Azure RBAC. Poulton already told you a hosted cluster hides the apiserver static Pod (Ch. 13, p. 183). The exam is not hosted. The exam is kubeadm. This file edits /etc/kubernetes/manifests/kube-apiserver.yaml and then deletes a binding. The fleet cannot do the first half. The exam cannot do the Azure half. Learn both. Grade this file on the flags.
Coin it: the API that still answers.
§II — Domain foundations
Cluster Hardening is 15%. Four official bullets.
- Restrict access to the Kubernetes API.
- Use Role Based Access Controls to minimize exposure.
- Exercise caution in using service accounts (disable defaults, minimize permissions on newly created ones).
- Update Kubernetes frequently.
Q04 spends bullets 1 and 2 in one stem. Q03 and Q37 spend bullet 3. Q15 spends bullet 4. A pass that only restores flags and leaves default automounting a token is a partial pass. A pass that only patches the ServiceAccount and leaves AlwaysAllow is a fail.
Poulton's request path is the picture to hold (Ch. 13, pp. 176-177). TLS, then authentication, then authorization, then admission. The unsecured port bypasses the first three and still hits admission. Disable it. The exam stem does not always name that port. It names --anonymous-auth, --authorization-mode, and --enable-admission-plugins. Those are the three flags Verify.bash greps.
Authentication fail is HTTP 401 (Ch. 13, p. 177). Kubernetes has no user database. --client-ca-file is how client certs work. SolutionNotes tells you to leave it. Do not delete a line you did not have to touch.
Authorization is pluggable. The first module that says yes wins (Ch. 13, p. 178). AlwaysAllow is a module that always wins. LabSetUp writes --authorization-mode=AlwaysAllow. The fix is --authorization-mode=Node,RBAC. Node authorizes kubelets. RBAC authorizes everyone else. Either alone is incomplete. AlwaysAllow is the API that still answers.
RBAC is deny-by-default (Ch. 13, p. 179). cluster-admin is *.* and * (Ch. 13, p. 185). LabSetUp binds that role to system:anonymous. Restoring Node,RBAC without deleting the binding leaves a user who never authenticated holding every verb. Two edits. One delete. All three required.
Admission runs after authorization, mutating first, and only on writes (Ch. 13, p. 185). NodeRestriction stops a kubelet from writing objects for a different node. LabSetUp swaps it for AlwaysAdmit. AlwaysAdmit is the admission analog of AlwaysAllow. Put NodeRestriction back. Do not invent a second plugin the stem did not ask for.
§III — Q04 by hand
LabSetUp does four hostile things (Questions.bash + LabSetUp.bash).
- Creates
clusterrolebinding system:anonymousbound tocluster-adminfor usersystem:anonymous. - Sets
--authorization-mode=AlwaysAllow. - Deletes the
--anonymous-authline so the default (true) returns. - Replaces
NodeRestrictionwithAlwaysAdmiton--enable-admission-plugins.
Your job is the inverse. SolutionNotes is the order.
Step 1. Edit /etc/kubernetes/manifests/kube-apiserver.yaml.
- --authorization-mode=Node,RBAC
- --enable-admission-plugins=NodeRestriction
- --anonymous-auth=false
- --client-ca-file=/etc/kubernetes/pki/ca.crt
The last line should already be there. Confirm it. Do not rearrange flags for sport. Static Pod recycle is a thirty-second clock you do not spend twice.
--anonymous-auth=false is the line people forget because LabSetUp deleted it rather than setting it true. A missing line is true. You must write false.
If --enable-admission-plugins lists several plugins, put NodeRestriction back in the comma list. Do not replace the whole list with NodeRestriction alone unless the stem shows only that plugin. LabSetUp's sed swaps the token in place. Your edit should too.
Step 2. Delete the leftover binding.
kubectl delete clusterrolebinding system:anonymous
Use /etc/kubernetes/admin.conf if your laptop kubeconfig now 401s. The stem says so. After --anonymous-auth=false, an anonymous get is supposed to fail. That is success, and it will feel like you broke kubectl. You did not. You closed Door 2.
Step 3. Wait. kubectl get pods -n kube-system --kubeconfig /etc/kubernetes/admin.conf. The apiserver static Pod must be Running. Editing the manifest is a delete-and-recreate of that Pod. A Verify.bash run during CrashLoopBackOff is a self-inflicted fail.
Verify.bash greps four facts (Verify.bash).
authorization-mode=Node,RBACin the manifest.anonymous-auth=falsein the manifest.NodeRestrictionin the manifest.kubectl get clusterrolebinding system:anonymousfails.
All four. Three flags and a missing object. Flags without the delete leave the API answering as cluster-admin for anonymous. Delete without the flags leaves AlwaysAllow, which does not care that the binding is gone.
SolutionNotes names the doctrine. AlwaysAllow and AlwaysAdmit never belong in production. Node authorizes kubelet requests. NodeRestriction stops a kubelet from writing another node's objects. --client-ca-file keeps x509 between client and apiserver. After the binding is gone, anonymous has no Role.
If the static Pod crash-loops, stop editing. crictl ps -a on the control plane node, then the apiserver container log. A YAML indent error is more common than a wrong flag. Restore from LabSetUp's backup only if you have no other copy; the stem told you not to peek, and the backup is the insecure file. Your last good edit plus a fixed indent is faster than a restore plus a full redo.
--authorization-mode is a comma list with no spaces in the common form. Node,RBAC matches Verify.bash. Node, RBAC may not. Copy the verify string. The Node module must come with RBAC. Node alone authorizes kubelets and leaves human requests to the next module. If you deleted AlwaysAllow without adding RBAC you can lock yourself out for real. That lockout is different from the 401 after anonymous-auth=false. The 401 is anonymous. A missing RBAC module is everyone.
Poulton's Docker Desktop walk already greps --authorization-mode=Node,RBAC and --enable-admission-plugins=NodeRestriction (Ch. 13, pp. 183, 185). The exam is that grep plus the anonymous line plus the delete. The hosted-cluster sentence on the same page is why Ops exists. Do not try kubectl describe pod kube-apiserver-... on AKS and call it Q04 practice. The Pod is not there.
§IV — Adjacent bullets: Q03, Q37, Q15
Q03 is ServiceAccount caution. nginx-pod in default uses a token by default. Patch default with automountServiceAccountToken: false. Create a Secret of type kubernetes.io/service-account-token annotated for that ServiceAccount. Recreate the Pod with automount false and a volume mount at /var/run/secrets/kubernetes.io/serviceaccount/. Verify.bash checks the SA field, the Secret type, and cat of the token path. The point is not "tokens are bad." The point is default automount is a token in every pod, including pods that never call the API.
Q37 is least privilege. Create ServiceAccount backend-qa in qa. Create Role no-secret-access with get,list on pods and nothing on secrets. Bind it. Point Pod frontend at the ServiceAccount. serviceAccountName is immutable; delete and recreate. Verify with kubectl auth can-i list pods -n qa --as=system:serviceaccount:qa:backend-qa (yes) and list secrets (no). 08-04 already taught can-i. Today the Role is the finding: pods only.
Q15 is update frequently. Worker node01 is one minor behind the control plane. Write the procedure to a file: drain, SSH, upgrade kubeadm, kubeadm upgrade node, upgrade kubelet and kubectl, restart kubelet, uncordon. The lab nodes may already match. The grade is the file. AKS names the same bullet as an upgrade channel. Do not drain an AKS node from this memory. Name the channel in Ops. Write the kubeadm list here if the stem asks.
Do not mix Q03's Secret type with 08-19's encryption-at-rest. A kubernetes.io/service-account-token Secret is a projected credential. AES-CBC in etcd is a store. Different doors.
Q03's manual Secret is the old-style bound token. The annotation kubernetes.io/service-account.name: default is what makes the control plane mint the token data. The type string is exact: kubernetes.io/service-account-token. A generic Opaque Secret with a token key will fail Verify.bash. The mount path is exact. Automount false on both the ServiceAccount and the Pod is not redundant. The SA default applies to new pods that omit the field. The Pod field overrides. Set both so a later editor cannot flip one.
Q37's can-i impersonation is --as=system:serviceaccount:qa:backend-qa. The namespace of the ServiceAccount is in the name. --as=backend-qa is a User, not a ServiceAccount, and the RoleBinding will not match. 08-04 taught that shape for humans and CSRs. Today the subject is a workload.
Q15's file is scored as a procedure, not as a live upgrade. If you drain the wrong node you spend the clock on a recover. Write the seven lines. Do not run them unless the stem says the versions actually differ and the worker is yours.
§V — Pairing
Ops is the AKS doors you cannot set with a static Pod. Authorized IP ranges, disable local accounts, Azure RBAC. Poulton's hosted-cluster sentence is why those knobs exist (Ch. 13, p. 183). A green Q04 on kubeadm is not a green AKS. An empty authorizedIpRanges on a public cluster is the API that still answers after Microsoft already set --anonymous-auth=false for you.
Dev is the census that lists leftover bindings and will not delete them. Q04 Step 2 is a delete of one known name. The fleet has other names. The Python tool prints anonymous_admin, unauthenticated_any, and authenticated_admin. It exits 1 when a row exists. It does not call delete. LabSetUp is a one-object plant. A loop that deletes is a fleet event.
Q08 stays in 08-19. etcd is closed. Q16 / Q05 / Q09 / Q10 stay in 08-13. Q11 / Q18 stay in 08-07. Q02 / Q07 stay in 08-01. Q14 (PSA) stays in 07-26. None of them restore --anonymous-auth=false.
System Hardening 15% remains after this ship. Next CKS-emphasis fire (counter 13, 2026-08-31) can take AppArmor, seccomp, and the host OS footprint. Do not start it today.
CKS first pass on the Cert slot is now Cluster Setup 10% (08-01) + Minimize Microservice Vulnerabilities 20% (07-26 PSS + 08-19 store) + Supply Chain 20% (08-07) + Runtime Security 20% (08-13) + Cluster Hardening 15% (today) = 85%. System Hardening 15% is the last unopened CKS domain. That is the honest leftover for counter 13.
One more pairing trap. 08-04's CKA CSR flow mints a human user and binds a Role. Q24 in this Bootcamp is the CKS form of that flow and is also tagged Cluster Hardening. Do not spend this fire on Q24. 08-04 already walked create, approve, Role, RoleBinding, can-i. Today's delete is the opposite verb. If a stem asks you to mint john, you are in the 08-04 file. If a stem asks you to take system:anonymous off cluster-admin, you are here.
Time box the stem. Q04 is a manifest edit plus one delete plus a wait. Candidates lose the wait. They grep too early, see AlwaysAllow still in the running process, and edit a second time while the first recycle is in flight. Two overlapping static-Pod recreates are how you get a CrashLoop that was not in LabSetUp. Edit once. Wait until Running. Then grep. Then delete. Then Verify.bash. The order in SolutionNotes is not decoration. If Verify.bash is still red after a Running apiserver, read the four greps in order instead of re-opening the manifest. The binding check is last because it is the leftover the flags cannot cancel.
§VI — Exam drill
kubectl get pods works without a kubeconfig. The manifest shows --authorization-mode=AlwaysAllow and no --anonymous-auth line. What is the first edit, and why is a working kubectl not a pass?--authorization-mode=Node,RBAC and --anonymous-auth=false. A missing anonymous-auth line is true. AlwaysAllow authorizes anyone who reaches the port. Working kubectl as anonymous is the API that still answers.system:anonymous still exists. Are the flags wasted?--anonymous-auth=false, your laptop kubeconfig returns 401. Did you break the cluster?--kubeconfig /etc/kubernetes/admin.conf as the stem requires. Wait for the static Pod to be Running before you edit again.--enable-admission-plugins was NodeRestriction,NamespaceLifecycle. LabSetUp sed-replaced NodeRestriction with AlwaysAdmit. You set the value to NodeRestriction only. What did you lose?NodeRestriction,NamespaceLifecycle unless the stem shows a single plugin. Token swap, not list wipe.get,list on pods. You also added secrets because "the app might need them later." can-i list secrets --as=system:serviceaccount:qa:backend-qa returns yes. Pass?no-secret-access. The verify is a no on secrets. Least privilege is the Role as written, not the Role as imagined.default in any namespace. Which Cluster Hardening bullet is still open, and which question spends it?default and mounts a token only where a pod needs one. Q37 binds a Role that cannot list secrets. The API is closed. The pods still carry tokens.§VII — Close
Four bullets, one leftover domain still on the board. Q04 is three flags and a delete. Q03 and Q37 are the ServiceAccount half. Q15 is the upgrade list. AKS is the hosted form of the same door, and it will not let you edit the manifest.
Examine well. Anonymous-auth defaults on when the line is missing. The binding survives the flags. Both are the API that still answers.
Related
- Prior arc: CKS encryption at rest (2026-08-19)
- Domain hub: Cross-References/Certifications-Roadmap
- Grounding tome: Poulton Ch.13, API security and RBAC, pp. 176-186