KCSA security review cheat sheet
The command that answers each question a security review asks of a Kubernetes cluster - who can do what, what the control plane is configured to do, what a workload is allowed, where the secrets leak, what the network permits, what gets admitted, and what evidence exists afterwards. Every output below is a real run on the path's cluster, not an illustration.
- Kubernetesapiserver v1.36.4, kubelet v1.36.3
- Runtimecontainerd 2.2.6
- CNICilium 1.18.1 - tunnel/VXLAN, with Hubble relay and UI
- Host OSUbuntu 26.04 LTS, kernel 7.0.0-29
- Built withkubeadm v1.36.3 - podSubnet 10.244.0.0/16, serviceSubnet 10.96.0.0/12
- Commands33
- Reviewed25 August 2026
Who can do what
-
kubectl auth whoamiYour own identity and groups. There is no user object behind it - the certificate in your kubeconfig IS the credential.
bash Example session kubectl auth whoami 2>&1 | head -6; echo "--- the admin is a client certificate in kubeconfig, in the cluster-admins group. No password, no server-side account"ATTRIBUTE VALUEUsername kubernetes-adminGroups [kubeadm:cluster-admins system:authenticated]Extra: authentication.kubernetes.io/credential-id [X509SHA256=da024493843d4cf0b1166a0d2defc96e9ea89084fc6d817e1a2bb955b1a28503]--- the admin is a client certificate in kubeconfig, in the cluster-admins group. No password, no server-side account -
kubectl auth can-i --list -n NS --as=system:serviceaccount:NS:SAEverything one identity may do, without holding it. The only honest answer to "what does this ServiceAccount have".
bash Example session kubectl auth can-i --list -n kcsa-cc --as=system:serviceaccount:kcsa-cc:appsa 2>/dev/null | grep -iE 'secrets|resources' | head -4; echo "--- what THIS app's ServiceAccount may do. A get,list on secrets is a common grant for an app that reads its own config"Resources Non-Resource URLs Resource Names Verbssecrets [] [] [get list]--- what THIS app's ServiceAccount may do. A get,list on secrets is a common grant for an app that reads its own config -
kubectl get clusterrolebindings -o jsonpath='{range .items[?(@.roleRef.name=="cluster-admin")]}{.metadata.name}{" -> "}{.subjects[*].name}{"\n"}{end}' | grep -vE 'system:|^$'Every cluster-admin subject. On a fresh kubeadm cluster this is one line - so anything else is an addition somebody made.
bash Example session kubectl get clusterrolebindings -o jsonpath='{range .items[?(@.roleRef.name=="cluster-admin")]}{.metadata.name}{" -> "}{.subjects[*].name}{"\n"}{end}' | grep -vE 'system:|^$' | head; echo "--- every cluster-admin binding and its subjects. kcsa-backdoor-admin is the one that does not belong"kcsa-backdoor-admin -> backdoorkubeadm:cluster-admins -> kubeadm:cluster-admins--- every cluster-admin binding and its subjects. kcsa-backdoor-admin is the one that does not belong -
kubectl get clusterroles -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.rules[*].verbs}{"\n"}{end}' | grep -E 'escalate|bind|impersonate'The three verbs that turn a small grant into a large one. None of them looks dangerous in a YAML review.
bash Example session kubectl get clusterroles -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.rules[*].verbs}{"\n"}{end}' | grep -E 'escalate|bind|impersonate' | grep -vc '^system:'; echo "non-system ClusterRoles carrying escalate, bind or impersonate"; echo "--- these three verbs are how a limited grant becomes a larger one. They belong on every review checklist"2non-system ClusterRoles carrying escalate, bind or impersonate--- these three verbs are how a limited grant becomes a larger one. They belong on every review checklist -
kubectl auth can-i get secrets -n NS --as=system:serviceaccount:NS:SAThe view-against-edit difference, settled. edit reads Secrets and view does not, which makes edit far closer to admin than its name suggests.
bash Example session kubectl create ns kcsa-trap >/dev/null 2>&1; kubectl -n kcsa-trap create sa viewer >/dev/null; kubectl -n kcsa-trap create sa editor >/dev/null; kubectl -n kcsa-trap create rolebinding v --clusterrole=view --serviceaccount=kcsa-trap:viewer >/dev/null; kubectl -n kcsa-trap create rolebinding e --clusterrole=edit --serviceaccount=kcsa-trap:editor >/dev/null; echo -n 'view can get secrets: '; kubectl auth can-i get secrets -n kcsa-trap --as=system:serviceaccount:kcsa-trap:viewer; echo -n 'edit can get secrets: '; kubectl auth can-i get secrets -n kcsa-trap --as=system:serviceaccount:kcsa-trap:editor; echo "--- the difference between the two roles people hand out casually"view can get secrets: noedit can get secrets: yes--- the difference between the two roles people hand out casually
What the control plane is set to do
-
sudo grep -E 'admission-plugins' /etc/kubernetes/manifests/kube-apiserver.yamlThe only admission flag kubeadm sets. Everything else - PodSecurity, LimitRanger, ResourceQuota - is one of 27 defaults.
bash Example session sudo grep -E 'admission-plugins' /etc/kubernetes/manifests/kube-apiserver.yaml; echo "--- the ONLY admission flag kubeadm sets. Everything else is a default" - --enable-admission-plugins=NodeRestriction--- the ONLY admission flag kubeadm sets. Everything else is a default -
sudo grep -cE 'audit-log-path|audit-policy-file' /etc/kubernetes/manifests/kube-apiserver.yamlWhether anything is recorded about who did what. 0 is the kubeadm default and the largest compliance gap on a fresh cluster.
bash Example session sudo grep -cE 'audit-log-path|audit-policy-file' /etc/kubernetes/manifests/kube-apiserver.yaml; echo "--- occurrences of the audit flags in the manifest. The same 0 this path measured by hand in domain 3, now reported as CIS 1.2.16 to 1.2.19"0--- occurrences of the audit flags in the manifest. The same 0 this path measured by hand in domain 3, now reported as CIS 1.2.16 to 1.2.19 -
sudo find /etc/kubernetes/pki -name '*ca.crt' | sortHow many certificate authorities the cluster has. Three, not one - and etcd having its own is the important one.
bash Example session sudo find /etc/kubernetes/pki -name '*ca.crt' | sort; echo "--- THREE, not one. Client certs are only trusted by the CA that signed them, so these are three separate trust domains"/etc/kubernetes/pki/ca.crt/etc/kubernetes/pki/etcd/ca.crt/etc/kubernetes/pki/front-proxy-ca.crt--- THREE, not one. Client certs are only trusted by the CA that signed them, so these are three separate trust domains -
sudo kubeadm certs check-expirationEvery certificate and when it dies. One year on the leaves, which is why a cluster left un-upgraded for a year stops working.
bash Example session sudo kubeadm certs check-expiration 2>/dev/null | sed -n '4,14p'; echo "--- one year on the leaf certificates. kubeadm renews them on upgrade, which is why a cluster left un-upgraded for a year stops working"CERTIFICATE EXPIRES RESIDUAL TIME CERTIFICATE AUTHORITY EXTERNALLY MANAGEDadmin.conf Aug 25, 2027 11:22 UTC 364d ca noapiserver Aug 25, 2027 11:22 UTC 364d ca noapiserver-etcd-client Aug 25, 2027 11:22 UTC 364d etcd-ca no
What a workload is allowed
-
kubectl label ns NS pod-security.kubernetes.io/enforce=restricted --overwriteThe difference between baseline and restricted, printed by the cluster: the refusal names every field restricted wants.
bash Example session kubectl label ns kcsa-trap pod-security.kubernetes.io/enforce=restricted --overwrite >/dev/null; kubectl -n kcsa-trap run rootpod2 --image=docker.io/library/busybox:1.37 --restart=Never --command -- sh -c 'sleep 300' 2>&1 | head -3; echo "--- restricted refuses the identical Pod, and names every field it wanted. That list IS the difference between the two levels"Warning: existing pods in namespace "kcsa-trap" violate the new PodSecurity enforce level "restricted:latest"Warning: both (and 2 other pods): allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfileError from server (Forbidden): pods "rootpod2" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false (container "rootpod2" must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container "rootpod2" must set securityContext.capabilities.drop=["ALL"]), runAsNonRoot != true (pod or container "rootpod2" must set securityContext.runAsNonRoot=true), seccompProfile (pod or container "rootpod2" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")--- restricted refuses the identical Pod, and names every field it wanted. That list IS the difference between the two levels -
kubectl -n NS exec POD -- grep CapEff /proc/1/statusThe effective capability set. Compare the hex between a hardened Pod and a default one - root in a container is not root on the node.
bash Example session kubectl -n kcsa-cc exec app -- sh -c 'mkdir -p /mnt 2>/dev/null; mount -t proc proc /mnt 2>&1; grep CapEff /proc/1/status'; echo "--- mount is refused although we are root: CAP_SYS_ADMIN is not in the set. CapEff is the bounded default containerd grants, not the full capability set"mount: mounting proc on /mnt failed: Permission deniedCapEff: 00000000a80425fb--- mount is refused although we are root: CAP_SYS_ADMIN is not in the set. CapEff is the bounded default containerd grants, not the full capability set -
kubectl -n NS get pods -o custom-columns='NAME:.metadata.name,QOS:.status.qosClass'QoS class per Pod. Guaranteed needs cpu AND memory equal on EVERY container - memory alone leaves you Burstable.
bash Example session printf 'apiVersion: v1\nkind: Pod\nmetadata:\n name: guaranteed\nspec:\n containers:\n - name: c\n image: docker.io/library/busybox:1.37\n command: ["sh","-c","sleep 900"]\n resources:\n requests:\n cpu: 100m\n memory: 64Mi\n limits:\n cpu: 100m\n memory: 64Mi\n' | kubectl -n kcsa-dos apply -f - >/dev/null && kubectl -n kcsa-dos wait --for=condition=Ready pod/guaranteed --timeout=120s >/dev/null; kubectl -n kcsa-dos get pods -o custom-columns='NAME:.metadata.name,QOS:.status.qosClass,CPU-REQ:.spec.containers[0].resources.requests.cpu,MEM-REQ:.spec.containers[0].resources.requests.memory,MEM-LIM:.spec.containers[0].resources.limits.memory' --no-headers; echo "--- hog set memory request == limit and is still Burstable. Guaranteed needs CPU AND memory equal on EVERY container"greedy BestEffort <none> <none> <none>guaranteed Guaranteed 100m 64Mi 64Mihog Burstable <none> 64Mi 64Mi--- hog set memory request == limit and is still Burstable. Guaranteed needs CPU AND memory equal on EVERY container -
kubectl -n NS exec POD -- sh -c 'nproc; cat /sys/fs/cgroup/cpu.max; cat /sys/fs/cgroup/memory.max'The ceiling at the kernel interface. `max` means there is none, and the container sees every node CPU.
bash Example session kubectl -n kcsa-dos exec greedy -- sh -c 'echo -n "nproc: "; nproc; echo -n "cpu.max: "; cat /sys/fs/cgroup/cpu.max; echo -n "memory.max: "; cat /sys/fs/cgroup/memory.max'; echo "--- the container sees every node CPU, and both cgroup ceilings read max. A busy loop here is a node-wide outage"nproc: 2cpu.max: max 100000memory.max: max--- the container sees every node CPU, and both cgroup ceilings read max. A busy loop here is a node-wide outage
Where the data lives, and what outlives it
-
kubectl get storageclass,csidrivers; kubectl get csinodes -o custom-columns='NODE:.metadata.name,DRIVERS:.spec.drivers[*].name'What the cluster can provision, and which nodes register a driver. A CSI driver is a privileged DaemonSet that mounts filesystems into other people's Pods.
bash Example session echo -n "StorageClasses: "; kubectl get storageclass --no-headers 2>/dev/null | wc -l; echo -n "CSI drivers: "; kubectl get csidrivers --no-headers 2>/dev/null | wc -l; kubectl get csinodes -o custom-columns='NODE:.metadata.name,DRIVERS:.spec.drivers[*].name' --no-headers; echo "--- no StorageClass, no CSI driver, and not one node registers a driver. Dynamic provisioning is something you INSTALL"StorageClasses: 0CSI drivers: 0cka5001 <none>cka5001-node01 <none>cka5001-node02 <none>cka5001-node03 <none>--- no StorageClass, no CSI driver, and not one node registers a driver. Dynamic provisioning is something you INSTALL -
kubectl get pv -o custom-columns='NAME:.metadata.name,RECLAIM:.spec.persistentVolumeReclaimPolicy,STATUS:.status.phase,PATH:.spec.hostPath.path'The reclaim policy and the path. A PV is cluster-scoped and can name a directory on a node - which Pod Security never sees.
bash Example session printf 'apiVersion: v1\nkind: PersistentVolume\nmetadata:\n name: kcsa-static\nspec:\n capacity:\n storage: 128Mi\n accessModes: ["ReadWriteOnce"]\n storageClassName: manual\n hostPath:\n path: /mnt/kcsa-store\n' | kubectl apply -f - 2>&1 | tail -1; kubectl get pv kcsa-static -o custom-columns='NAME:.metadata.name,CAPACITY:.spec.capacity.storage,RECLAIM:.spec.persistentVolumeReclaimPolicy,STATUS:.status.phase,PATH:.spec.hostPath.path' --no-headers; echo "--- a PV is CLUSTER-SCOPED and it names a path on a node. Note the reclaim policy nobody set"persistentvolume/kcsa-static createdkcsa-static 128Mi Retain Available /mnt/kcsa-store--- a PV is CLUSTER-SCOPED and it names a path on a node. Note the reclaim policy nobody set -
sudo cat /PATH/FROM/THE/PV/fileData remanence, proved. Deleting a PVC leaves the volume Released and the bytes on disk - removing them is a separate manual step.
bash Example session sudo cat /mnt/kcsa-store/records.txt; echo "--- the claim is gone and the data is still here. Deleting a PVC is not deleting data"CUSTOMER-RECORDS-2026--- the claim is gone and the data is still here. Deleting a PVC is not deleting data
Where the credentials are
-
kubectl -n NS describe secret NAMEByte counts, never values - the one place Kubernetes withholds data it holds. `get -o yaml` gives you the base64 without hesitation.
bash Example session kubectl create ns kcsa-sec >/dev/null && kubectl -n kcsa-sec create secret generic dbcreds --from-literal=username=svcuser --from-literal=password=Hunter2-VerySecret >/dev/null && kubectl -n kcsa-sec describe secret dbcreds | tail -6; echo "--- describe shows SIZES, never values. That is deliberate"Type: Opaque Data====password: 18 bytes -
kubectl -n NS exec POD -- sh -c 'tr "\0" "\n" < /proc/1/environ'A Secret consumed as an environment variable, readable by any process in the container. A volume-mounted Secret does not appear here.
bash Example session kubectl -n kcsa-sec exec envpod -- sh -c 'tr "\0" "\n" < /proc/1/environ | grep -i password'; echo "--- and in /proc/1/environ, readable by ANY process in that container"password=Hunter2-VerySecret--- and in /proc/1/environ, readable by ANY process in that container -
kubectl -n NS exec POD -- cat /var/run/secrets/kubernetes.io/serviceaccount/token | cut -d. -f2 | base64 -dThe identity inside a mounted token. Decoding needs no permission at all - the payload is signed, not encrypted.
bash Example session kubectl -n kcsa-tm exec probe -- sh -c 'cat /var/run/secrets/kubernetes.io/serviceaccount/token' | cut -d. -f2 | base64 -d 2>/dev/null | tr ',' '\n' | grep -E 'serviceaccount|"sub"|namespace' | head -5; echo "--- the token decodes to system:serviceaccount:kcsa-tm:default. Anyone who reads this file IS that identity""kubernetes.io":{"namespace":"kcsa-tm""serviceaccount":{"name":"default""sub":"system:serviceaccount:kcsa-tm:default"}--- the token decodes to system:serviceaccount:kcsa-tm:default. Anyone who reads this file IS that identity -
automountServiceAccountToken: falseThe credential is simply not there. Most workloads never call the API, so this is free attack surface to remove.
bash Example session kubectl -n kcsa-cc run notoken --image=docker.io/library/busybox:1.37 --restart=Never --overrides='{"spec":{"automountServiceAccountToken":false}}' --command -- sh -c 'sleep 600' >/dev/null && kubectl -n kcsa-cc wait --for=condition=Ready pod/notoken --timeout=120s >/dev/null && kubectl -n kcsa-cc exec notoken -- ls /var/run/secrets/kubernetes.io/serviceaccount/ 2>&1 | head -2; echo "--- automountServiceAccountToken:false and the credential is simply not there. Nothing to steal"ls: /var/run/secrets/kubernetes.io/serviceaccount/: No such file or directorycommand terminated with exit code 1--- automountServiceAccountToken:false and the credential is simply not there. Nothing to steal -
kubectl -n NS get secret NAME -o jsonpath='{.data.\.dockerconfigjson}' | base64 -dA registry login in clear. The inner `auth` field is base64 of username:password - two decodes and you can push.
bash Example session kubectl -n kcsa-reg get secret regcred -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d; echo; echo "--- the registry, the username, the password in clear, and an auth field. Anyone who can read Secrets in this namespace can log in to that registry"{"auths":{"ghcr.io":{"username":"ci-bot","password":"s3cr3t-ci-token","auth":"Y2ktYm90OnMzY3IzdC1jaS10b2tlbg=="}}}--- the registry, the username, the password in clear, and an auth field. Anyone who can read Secrets in this namespace can log in to that registry
What the network permits
-
kubectl -n NS exec POD -- curl -s --max-time 5 http://SVC.OTHER-NS.svc:PORT/A namespace is not a network boundary. Every Pod reaches every Pod by default, and cluster DNS hands over the address.
bash Example session kubectl -n kcsa-attacker exec atk -- curl -s --max-time 5 http://web.kcsa-victim.svc.cluster.local:8080/; echo "--- a Pod in kcsa-attacker reached an internal service in kcsa-victim with no credential and no policy in the way"INTERNAL-ADMIN-PANEL-OK--- a Pod in kcsa-attacker reached an internal service in kcsa-victim with no credential and no policy in the way -
kubectl apply -f - # NetworkPolicy, podSelector {}, policyTypes ["Ingress"]The default-deny idiom, and what a policy drop looks like: exit 28, a timeout, not a connection refused.
bash Example session printf 'apiVersion: networking.k8s.io/v1\nkind: NetworkPolicy\nmetadata:\n name: default-deny-ingress\n namespace: kcsa-victim\nspec:\n podSelector: {}\n policyTypes: ["Ingress"]\n' | kubectl apply -f - 2>&1 | tail -1; kubectl -n kcsa-attacker exec atk -- curl -s --max-time 6 -o /dev/null -w 'exit-then-code %{http_code}\n' http://web.kcsa-victim.svc.cluster.local:8080/ 2>&1; echo "curl exit code: $?"; echo "--- default-deny ingress in kcsa-victim, enforced by Cilium. The connection now times out. Same attacker, same network, one policy"networkpolicy.networking.k8s.io/default-deny-ingress createdexit-then-code 000command terminated with exit code 28curl exit code: 28--- default-deny ingress in kcsa-victim, enforced by Cilium. The connection now times out. Same attacker, same network, one policy -
kubectl -n kube-system exec CILIUM-POD -c cilium-agent -- hubble observe -n NS --verdict DROPPEDWhy the connection timed out, named: `Policy denied`, with both ends as Pod names. The difference between a timeout you can explain and one you cannot.
bash Example session AG=$(kubectl -n kube-system get pod -l k8s-app=cilium --field-selector spec.nodeName=cka5001-node01 -o jsonpath='{.items[0].metadata.name}'); for i in $(seq 20); do D=$(kubectl -n kube-system exec $AG -c cilium-agent -- hubble observe -n kcsa-obs --verdict DROPPED --last 2 2>/dev/null); [ -n "$D" ] && break; sleep 3; done; echo "$D"; echo "--- DROPPED, and the reason is named: Policy denied. This is the difference between a timeout you can explain and one you cannot"Aug 25 15:24:47.901: kcsa-obs/client:48108 (ID:2369) <> kcsa-obs/web:8080 (ID:26486) policy-verdict:none INGRESS DENIED (TCP Flags: SYN)Aug 25 15:24:47.901: kcsa-obs/client:48108 (ID:2369) <> kcsa-obs/web:8080 (ID:26486) Policy denied DROPPED (TCP Flags: SYN)--- DROPPED, and the reason is named: Policy denied. This is the difference between a timeout you can explain and one you cannot -
kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg statusWhether pod-to-pod traffic is encrypted. Encapsulation is not encryption, and the default here is Disabled.
bash Example session kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg status 2>/dev/null | grep -E 'Encryption|KubeProxyReplacement|Host firewall'; echo "--- Encryption DISABLED. Pod-to-pod traffic between nodes crosses the network in clear"KubeProxyReplacement: FalseHost firewall: DisabledEncryption: Disabled--- Encryption DISABLED. Pod-to-pod traffic between nodes crosses the network in clear
What gets admitted, and what it is made of
-
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurationsWhat is currently able to rewrite or refuse objects. Run this first on a cluster you inherit - a mutating webhook sees everything.
bash Example session kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations --no-headers 2>&1 | tail -1; kubectl get validatingadmissionpolicies --no-headers 2>&1 | tail -1; echo "--- no webhooks and no policies. Every admission decision so far has come from a plugin compiled into the apiserver"No resources foundNo resources found--- no webhooks and no policies. Every admission decision so far has come from a plugin compiled into the apiserver -
kubectl -n NS run NAME --image=IMG --restart=NeverWhat a ValidatingAdmissionPolicy refusal looks like: it names the policy, the binding and the message a human wrote.
bash Example session sleep 8; kubectl -n kcsa-adm run nolabel --image=docker.io/library/busybox:1.37 --restart=Never --command -- sh -c 'sleep 300' 2>&1 | tail -2; echo "--- refused at admission. The message names the policy, the binding, and the reason a human wrote"The pods "nolabel" is invalid: : ValidatingAdmissionPolicy 'kcsa-require-owner' with binding 'kcsa-require-owner-binding' denied request: every Pod must carry an owner label naming a team--- refused at admission. The message names the policy, the binding, and the reason a human wrote -
kubectl -n NS get pod NAME -o jsonpath='{.spec.containers[0].image}{"\n"}{.status.containerStatuses[0].imageID}{"\n"}'What you asked for against what ran. The tag is the request; the digest in status is the answer, and only it is stable.
bash Example session kubectl create ns kcsa-img >/dev/null 2>&1; kubectl -n kcsa-img run bytag --image=docker.io/library/busybox:1.37 --restart=Never --command -- sh -c 'sleep 900' >/dev/null && kubectl -n kcsa-img wait --for=condition=Ready pod/bytag --timeout=120s >/dev/null && kubectl -n kcsa-img get pod bytag -o jsonpath='spec.image: {.spec.containers[0].image}{"\n"}status.imageID: {.status.containerStatuses[0].imageID}{"\n"}pullPolicy: {.spec.containers[0].imagePullPolicy}{"\n"}'; echo "--- you asked for a TAG. What ran is a DIGEST, and the two are recorded in different fields"spec.image: docker.io/library/busybox:1.37status.imageID: docker.io/library/busybox@sha256:9db7b59979c38555a39def84a31fb98b5296952f9e3afd4f6f11f05b07adfab0pullPolicy: IfNotPresent--- you asked for a TAG. What ran is a DIGEST, and the two are recorded in different fields -
kubectl -n NS get pods -o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[0].image,PULLPOLICY:.spec.containers[0].imagePullPolicy'The pull policy nobody set. `:latest` defaults to Always, every other tag to IfNotPresent.
bash Example session kubectl -n kcsa-img run bylatest --image=docker.io/library/busybox:latest --restart=Never --command -- sh -c 'sleep 900' >/dev/null; kubectl -n kcsa-img get pods bytag bylatest -o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[0].image,PULLPOLICY:.spec.containers[0].imagePullPolicy' --no-headers; echo "--- neither Pod set imagePullPolicy. The tag decides it: :latest defaults to Always, anything else to IfNotPresent"bytag docker.io/library/busybox:1.37 IfNotPresentbylatest docker.io/library/busybox:latest Always--- neither Pod set imagePullPolicy. The tag decides it: :latest defaults to Always, anything else to IfNotPresent
What the evidence says
-
kubectl logs job/kube-bench-master | grep -A5 '== Summary master =='The CIS Benchmark score for the control plane. Read WARN separately - every one of them means a human must check it.
bash Example session for i in $(seq 60); do S=$(kubectl -n kcsa-cis get job kube-bench-master -o jsonpath='{.status.succeeded}' 2>/dev/null); [ "$S" = "1" ] && break; sleep 3; done; kubectl -n kcsa-cis logs job/kube-bench-master 2>/dev/null | grep -A5 '== Summary master =='; echo "--- a number for each outcome. Note that WARN is nearly as large as FAIL"== Summary master ==38 checks PASS9 checks FAIL12 checks WARN0 checks INFO --- a number for each outcome. Note that WARN is nearly as large as FAIL -
kubectl logs job/kube-bench-policies | grep -E '^\[FAIL\]'The six policy checks a benchmark can automate: cluster-admin, secrets, wildcards, create pods, default ServiceAccounts, mounted tokens.
bash Example session kubectl -n kcsa-cis logs job/kube-bench-policies 2>/dev/null | grep -E '^\[FAIL\]'; echo "--- and the six it CAN automate are every finding this path made by hand, in the benchmark's own words"[FAIL] 5.1.1 Ensure that the cluster-admin role is only used where required (Automated)[FAIL] 5.1.2 Minimize access to secrets (Automated)[FAIL] 5.1.3 Minimize wildcard use in Roles and ClusterRoles (Automated)[FAIL] 5.1.4 Minimize access to create pods (Automated)[FAIL] 5.1.5 Ensure that default service accounts are not actively used (Automated)[FAIL] 5.1.6 Ensure that Service Account Tokens are only mounted where necessary (Automated)--- and the six it CAN automate are every finding this path made by hand, in the benchmark's own words -
trivy image --scanners vuln --format json IMAGEKnown vulnerabilities in an image, by severity. Most are OS packages from the base image, so the first fix is usually a rebuild.
bash Example session for i in $(seq 100); do S=$(kubectl -n kcsa-scan get job trivy -o jsonpath='{.status.succeeded}' 2>/dev/null); [ "$S" = "1" ] && break; sleep 5; done; kubectl -n kcsa-scan logs job/trivy 2>/dev/null | sed -n '/===old-severity===/,/===old-findings===/p' | head -n -1; echo "--- nginx:1.20, a base image a few years old. Every one of these is a KNOWN, PUBLISHED vulnerability in a package the image ships"===old-severity=== 32 CRITICAL 197 HIGH 229 LOW 317 MEDIUM 23 UNKNOWN--- nginx:1.20, a base image a few years old. Every one of these is a KNOWN, PUBLISHED vulnerability in a package the image ships -
kubectl get pods -A -o jsonpath='{range .items[*].spec.containers[*]}{.resources.limits}{"\n"}{end}' | grep -c '^$'How many containers have no ceiling at all, and whether anything exists to default them. Availability is part of the model.
bash Example session echo -n "containers running on this cluster: "; kubectl get pods -A -o jsonpath='{range .items[*].spec.containers[*]}{.name}{"\n"}{end}' | grep -c .; echo -n "with no resource limits at all: "; kubectl get pods -A -o jsonpath='{range .items[*].spec.containers[*]}{.resources.limits}{"\n"}{end}' | grep -c '^$'; echo -n "LimitRanges and ResourceQuotas that could default them: "; kubectl get limitrange,resourcequota -A --no-headers 2>/dev/null | wc -l; echo "--- 20 of 22 containers have no ceiling, and nothing exists to give them one"containers running on this cluster: 22with no resource limits at all: 20LimitRanges and ResourceQuotas that could default them: 0--- 20 of 22 containers have no ceiling, and nothing exists to give them one
No command matches that search.