CKS: prove the security boundary
Security practice starts with a concrete question: who should be able to do what, against which resource, and through which path? Make the intended operation work, then prove that an unintended operation fails. Configuration alone is incomplete evidence.
Choose where to begin
Domains and scopeBuild the command-to-evidence habitCluster setup: draw the allowed pathsCluster hardening: identity and permission are separateSystem hardening: name the kernel boundaryWorkload hardening: admission and runtimeSupply chain: compare evidence, not image namesRuntime evidence: explain who did whatFurther applied practiceDomains and scope
| Domain | Weight | Practice focus |
|---|---|---|
| Cluster Setup | 15% | network boundaries, component configuration and CIS findings |
| Cluster Hardening | 15% | scoped permissions and credentials |
| System Hardening | 10% | kernel restrictions and host exposure |
| Minimize Microservice Vulnerabilities | 20% | admission, isolation and Secret protection |
| Supply Chain Security | 20% | image and workload risk before deployment |
| Monitoring, Logging and Runtime Security | 20% | audit evidence and restricted runtime behavior |
Weights follow the official CKS objectives ↗. The 25 drills develop selected skills across those domains. They do not complete every objective: add registry-backed signature enforcement, Falco rule writing, broader OS hardening and upgrade practice. Match documentation and tools to the cluster version supplied by the lab.
Build the command-to-evidence habit
Coach Caz: A security setting earns its place by stopping the wrong action while allowing the right one. Test both reps.
Use each Practice loop below as a short rehearsal. Read its starting state, predict the result of the first command, then compare that prediction with the evidence. The outputs are illustrative: names, timestamps, addresses, and counts vary. Commands use the example's names; substitute the actual task's namespace, resource, host, and file names when transferring the pattern to a drill.
- Recognize: say which symptom puts you on this path.
- Inspect: run the smallest check that separates two plausible causes.
- Act: change the field or configuration supported by that evidence.
- Prove: repeat the failed operation and check a constraint that must still hold.
- Repeat: hide the commands, change one input, and rebuild the sequence from memory.
Use a disposable practice environment for changes. A case that assumes an installed controller, tool, or prepared resource says so; it does not install those prerequisites for you. Linux host commands belong inside a practice Linux VM or the specified lab node. These examples teach the investigation pattern; use the CLI's assigned task and grader for recorded reps.
Cluster setup: draw the allowed paths
NetworkPolicy selects pods and adds allowed traffic. Multiple policies combine their allowances; they do not run as an ordered deny list. A default deny policy provides a baseline, and another policy can deliberately allow the frontend. Enforcement depends on the CNI. Test from an allowed source and a denied source, with the right namespace and labels: Restrict pod traffic with NetworkPolicy (kubefit drill cks-01-default-deny-netpol). Kubernetes NetworkPolicy ↗.
A CIS benchmark is a structured assessment. Check its version and applicability, retain initial findings, repair the requested controls and rerun them. Passing file-permission checks does not establish that every benchmark control passed or that the cluster is secure: Assess and repair CIS file controls (kubefit drill cks-09-cis-benchmark).
An Ingress encrypts the edge only when it names a TLS Secret whose certificate matches the requested host; otherwise the controller serves a placeholder certificate and the object still looks correct. The cue is a tls block without a matching hosts entry, or a Secret that is not kubernetes.io/tls. Proof is a client that resolves the host to the controller and shows the served certificate subject with a 200, not a re-read of the Ingress: Terminate TLS at the Ingress (kubefit drill cks-11-ingress-tls). Ingress TLS ↗.
An unauthenticated API request is not rejected by default; it is authenticated as system:anonymous, and the system:public-info-viewer binding lets that identity read /version and the health endpoints. CIS asks for anonymous auth to be off, but the kubelet probes the API server's /livez and /readyz without credentials, so the bare --anonymous-auth=false flag puts a kubeadm API server into a restart loop. The structured AuthenticationConfiguration (--authentication-config) keeps anonymous access for a listed set of paths and closes everything else. Recognize the task by the CIS control 1.2.1 and by the static-pod manifest you must edit and mount a file into. Proof is a 401 on an unauthenticated /version from the node while /readyz still answers 200 and the mirror pod stays ready: Disable anonymous API access (kubefit drill cks-20-apiserver-anonymous-auth). Authenticating ↗.
Verify binaries and protect node endpoints
A checksum compares downloaded bytes with an expected digest. A signature additionally ties an artifact to a trusted signer under a stated verification policy. Obtain both from the official release channel, match version and architecture, verify before executing, and stop on a mismatch. A checksum fetched from the same compromised mirror is not independent proof of provenance. Verify Kubernetes artifacts ↗
Cloud metadata, kubelet APIs, and host listeners are separate surfaces. A Pod NetworkPolicy is not a general node firewall, and enforcement for host-network traffic depends on the implementation. Use the platform's metadata protections, scoped workload identity, and appropriate node/network controls. Prove it: an unprivileged workload cannot obtain node credentials or call a protected endpoint, while required node operations still work. Do not expose credential contents as test evidence. Security checklist ↗
Practice loop: default-deny broke DNS before it reached the app
Starting state: namespace concepts-security has Pod client, an egress-isolating NetworkPolicy, and an enforcing CNI. The client image contains nslookup. CoreDNS Pods in kube-system carry k8s-app=kube-dns; confirm that before using the selector below. NodeLocal DNS requires inspecting a different path.
kubectl -n concepts-security exec client -- nslookup kubernetes.default.svc.cluster.local
kubectl -n concepts-security get networkpolicy -o yaml
kubectl -n kube-system get pods -l k8s-app=kube-dns --show-labels
Look for: DNS timeout rather than an application connection error. If egress policies allow only a database port, the client cannot resolve its destination first. Inspect ingress policy on the DNS destination too; both directions must permit a flow when isolated.
Act: on the stated ordinary CoreDNS path, allow DNS for this client label while preserving application restrictions:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: client-dns
namespace: concepts-security
spec:
podSelector:
matchLabels:
app: client
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- {protocol: UDP, port: 53}
- {protocol: TCP, port: 53}
Save as client-dns.yaml, check the actual client label, and apply it. The namespace and Pod selectors share one destination item, so both must match. Putting them in separate items would permit their union.
Prove: repeat the lookup, test the permitted application, and test a disallowed destination known to be healthy. A timeout against an already broken server is not evidence of policy enforcement. Inspect every applicable policy because allows add together.
Repeat: point to the exact indentation that makes the selectors AND together.
Coach Caz: DNS is the gym's address. A very secure taxi that cannot find it is still not a workout. NetworkPolicy semantics ↗
Cluster hardening: identity and permission are separate
A ServiceAccount identifies a workload. A Role grants namespaced permissions; a binding attaches those permissions to an identity. Removing an automatically mounted token reduces credential exposure, but it does not remove the account's RBAC bindings. Likewise, a projected token's audience and expiry do not by themselves restrict the account's authorized verbs.
Read the pod's actual identity and volume mounts. Use kubectl auth can-i with the intended identity to check both a required action and a forbidden one. Avoid broad permissions simply to make a failing request pass: Limit ServiceAccount credentials and permissions (kubefit drill cks-06-serviceaccount-hardening). RBAC good practices ↗, ServiceAccounts ↗.
A ClusterRoleBinding to cluster-admin is the widest grant there is, and a workload inherits it silently through its ServiceAccount. Least privilege means a namespaced Role that names only the verbs and resources the workload uses, bound with a RoleBinding to that ServiceAccount, and the cluster-wide binding deleted rather than narrowed. The cue is an unnecessarily broad grant such as cluster-admin; a ServiceAccount subject alone is normal and is not a finding. Proof is kubectl auth can-i --as=system:serviceaccount:<ns>:<name> answering yes for the required actions and no for secrets, other namespaces and cluster-scoped resources, plus a search for any other ClusterRoleBinding that still lists the account: Replace cluster-admin with least privilege (kubefit drill cks-10-rbac-least-privilege). Using RBAC authorization ↗.
Four settings can expose host namespaces, privileges, or files: hostNetwork, hostPID, securityContext.privileged, and a hostPath volume at /. Hardening a cluster starts with finding every workload that sets one, and the fast way is a field-selecting query (kubectl get pods -A -o custom-columns=... or a JSONPath filter), not reading manifests. Remediate the controller's template, keep the image and the replica count, and read the live pods afterwards: a corrected Deployment whose old pod is still running proves nothing. These are the settings the baseline Pod Security Standard rejects, so the sweep normally ends with that label on the namespace: Sweep dangerous workload settings (kubefit drill cks-19-dangerous-workloads-sweep). Pod Security Standards ↗.
Upgrade a vulnerable component without losing the cluster
Identify the vulnerable component, running version, fixed release, and supported upgrade path. API server, kubelet, runtime, and host packages have different lifecycles. A new manifest or downloaded binary is not evidence that the running process changed. Follow version-skew rules, preserve recovery access, and drain workers where the procedure requires it.
Prove it: verify the effective version, node and API readiness, and application traffic after each stage. Recheck the original finding. The CKS pack does not provide a full security-upgrade rehearsal; practise this on a disposable topology. Version skew ↗ · Kubeadm upgrades ↗
Practice loop: remove one grant and retest the identity
Starting state: ServiceAccount reporter in concepts-security must list Pods but must not read Secrets. Your identity can impersonate it. A prepared excessive binding exists; discover its name rather than deleting anything labeled “admin” on sight.
kubectl auth can-i list pods -n concepts-security --as=system:serviceaccount:concepts-security:reporter
kubectl auth can-i get secrets -n concepts-security --as=system:serviceaccount:concepts-security:reporter
kubectl -n concepts-security get rolebindings -o yaml
kubectl get clusterrolebindings -o yaml
Look for: yes to both questions. Follow each matching subject to its roleRef, then inspect that Role or ClusterRole. Names are clues, not authority: viewer can contain wildcard permissions.
Act: create the intended narrow grant, then remove the identified excessive binding after checking its other subjects:
kubectl -n concepts-security create role pod-report --verb=get,list --resource=pods
kubectl -n concepts-security create rolebinding pod-report --role=pod-report --serviceaccount=concepts-security:reporter
Use kubectl delete rolebinding NAME -n concepts-security or kubectl delete clusterrolebinding NAME only for the discovered practice grant. A shared binding may need a subject edit instead of deletion.
Prove: repeat both can-i checks and a cross-namespace check. Expected: required reads still work, Secret access returns no, and no unintended scope remains. A second binding can preserve the excessive permission even after the first is removed.
Repeat: add an extra read grant in a fresh rehearsal and predict whether narrowing the first Role revokes it.
Coach Caz: Removing one weight plate does not empty the bar. RBAC grants stack. RBAC good practices ↗
System hardening: name the kernel boundary
seccomp filters system calls; it is not a filesystem permission model. RuntimeDefault selects the runtime's profile; Localhost references a profile that must exist on the scheduling node. A missing profile can prevent startup. A running pod does not prove a specific syscall was denied—exercise that syscall and inspect its result: Apply runtime and custom seccomp profiles (kubefit drill cks-03-seccomp-hardening). seccomp tutorial ↗.
Reduce the host's exposed surface
Inventory listeners, running services, installed packages, privileged access, and remote-login paths before removing anything. Disable unnecessary services and uninstall unnecessary packages only after checking dependencies; an inactive service and an absent package are different facts. Keep required kubelet, runtime, networking, and recovery paths working. Prove it: the unwanted listener is gone after restart, required administration still works, and workloads remain healthy. Node access here uses disposable kind node containers; that shell mechanism is not a general host-hardening procedure. Security checklist ↗
The kubelet is an API server for one node. With authentication.anonymous.enabled: true and authorization.mode: AlwaysAllow anyone who reaches port 10250 can list pods and exec into them, and an enabled read-only port 10255 exposes selected unauthenticated information over plain HTTP; it is not the same API as the authenticated port. The settings live in /var/lib/kubelet/config.yaml (flags in kubeadm-flags.env override the file) and take effect only after systemctl restart kubelet. Proof is the running process, not the file: an anonymous request to 10250 answers 401 (403 means anonymous is still an identity), 10255 refuses the connection, and the node returns to Ready: Harden the kubelet API (kubefit drill cks-12-kubelet-hardening). Kubelet authentication and authorization ↗.
AppArmor, seccomp, and sandbox runtimes
AppArmor constrains process actions using a loaded profile; seccomp filters system calls. They complement ordinary ownership, capabilities, and non-root execution. An AppArmor Localhost profile must be loaded on every eligible node. complain records would-be denials; enforcement blocks them. A profile name in YAML is not evidence that the kernel is enforcing it. AppArmor ↗
A RuntimeClass selects a runtime handler already configured on the node. Creating the object does not install gVisor, Kata, or another sandbox. Account for supported nodes, overhead, and application compatibility. Prove it: the Pod uses the requested handler, starts on a compatible node, performs its required operation, and fails an explicitly prohibited one. These runtime capabilities require separate practice where the local kind fixture cannot supply them. RuntimeClass ↗
Practice loop: a security profile can stop startup
Starting state: a prepared Pod in concepts-security references seccomp Localhost profile profiles/web.json. It cannot start. The target node is disposable and accessible through the lab's documented node-access method.
kubectl -n concepts-security get pods -o wide
kubectl -n concepts-security describe pod WEB_POD
kubectl -n concepts-security get pod WEB_POD -o yaml
Look for: a runtime error naming a missing or invalid seccomp profile, plus the selected node. A Localhost profile is a node file, not a ConfigMap that Kubernetes automatically distributes. Inspect the kubelet's configured root directory; with its default, the profile belongs under /var/lib/kubelet/seccomp/profiles/web.json.
Act: if a custom profile is required, install the approved profile on every eligible node and validate its JSON. If the requirement is instead RuntimeDefault and Localhost was the mistake, update the controller's security context to seccompProfile: {type: RuntimeDefault}. Do not solve a missing profile by silently switching to Unconfined.
Prove: the replacement starts with the intended profile. Then perform the required application operation and a controlled operation the profile should reject. A Ready Pod proves startup; it does not prove the syscall restriction.
Repeat: identify the distinct evidence for “profile missing,” “syscall denied,” and “application lacks a writable directory.”
Coach Caz: A helmet in the YAML is not a helmet on the process. Check what the runtime actually fitted. Seccomp tutorial ↗
Workload hardening: admission and runtime
Pod Security Admission evaluates new pod requests against a standard. Enforcing restricted does not evict existing pods. Update the Deployment template and verify a rollout actually succeeds. Check non-root execution, capabilities, privilege escalation and allowed seccomp settings together. A secure-looking manifest that cannot serve its workload is an incomplete solution: Enforce restricted Pod Security (kubefit drill cks-02-pod-security-admission). Pod Security Standards ↗.
A read-only root filesystem still permits writes to explicitly writable volumes. Limit those to the paths the application needs, then test the intended service and an unwanted write. This reduces writable surface; it does not establish complete container immutability: Make a container root filesystem read-only (kubefit drill cks-07-runtime-immutability).
Secret API values use base64 representation, which is encoding. At-rest encryption is configured separately in the API server. Provider order affects new writes; old records must be rewritten, and every API server needs compatible configuration. Verify both readable API results and encrypted raw storage: Encrypt Secret data at rest (kubefit drill cks-08-secrets-encryption). Encryption at rest ↗.
How a pod consumes a Secret matters as much as whether it is encrypted. Literal environment values can appear in Pod descriptions; values sourced through secretKeyRef are normally shown as references there. The resolved secret may still leak through process inspection, dumps, child processes, or application logs; a mounted ServiceAccount token the application never uses is a free API credential for anyone who gets a shell. This drill requires a read-only volume mount of the Secret, automountServiceAccountToken: false, and immutable: true on the Secret so it cannot be changed in place. That requires replacement and workload updates for rotation; it does not prevent an authorized process from reading the value. Proof is an exec into the pod: the file is readable, the token path is absent, and the value is not in the environment: Harden how a pod consumes a Secret (kubefit drill cks-13-secret-hygiene). Secrets ↗.
An egress NetworkPolicy is a whitelist: once a Pod is selected for egress isolation, allowed traffic is the union of all applicable egress policies. With an enforcing CNI, ordinary connections outside that union are blocked; DNS needs an allowance too. Kubernetes documents exceptions such as traffic to the local node. Each rule is the cross product of its to entries and its ports, so keep the database rule and the DNS rule separate or the database becomes reachable on port 53. Select CoreDNS with a namespaceSelector on kubernetes.io/metadata.name: kube-system and a podSelector on k8s-app=kube-dns in the same list item, and allow 53 over UDP and TCP. This drill checks configuration only. Behavioral isolation still needs an enforcing CNI and positive/negative traffic tests. The graded evidence is the spec read back field by field: exact podSelector, exact policyTypes, no rule without to or without ports, no ipBlock: Restrict egress with NetworkPolicy (kubefit drill cks-16-egress-network-policy). NetworkPolicy ↗.
Pod encryption and authorization are independent
A NetworkPolicy limits allowed paths; it does not encrypt them. CNI encryption can protect a configured node-to-node path, while a mesh can authenticate workload identities and apply request policy. Neither implies that every host, gateway, or off-cluster hop is protected.
Map the actual path, check enrollment and keys or certificates, then test traffic from allowed and denied identities. Capture only an appropriate test flow on the relevant underlay interface when proving encryption. Prove it: the permitted operation works, unauthorized access fails, and the stated hop is protected. A successful TLS handshake alone does not prove application authorization. Cilium encryption ↗ · Istio security ↗
Practice loop: admission refused the Pod before it existed
Starting state: concepts-restricted is a disposable namespace enforcing the Restricted Pod Security Standard. Create and label it for this example. No application needs to start to demonstrate admission.
kubectl create namespace concepts-restricted
kubectl label namespace concepts-restricted pod-security.kubernetes.io/enforce=restricted
kubectl -n concepts-restricted run sample --image=busybox:1.36 --dry-run=server -o yaml --command -- sleep 60
Look for: an admission error naming missing controls such as allowPrivilegeEscalation=false, dropped capabilities, non-root execution, and seccomp. This is not a scheduler failure; no Pod was created by the dry-run.
Act: save this complete Pod as restricted-pod.yaml and use server-side dry-run again:
apiVersion: v1
kind: Pod
metadata:
name: sample
namespace: concepts-restricted
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
seccompProfile: {type: RuntimeDefault}
containers:
- name: sample
image: busybox:1.36
command: [sleep, "60"]
securityContext:
allowPrivilegeEscalation: false
capabilities: {drop: [ALL]}
kubectl apply --dry-run=server -f restricted-pod.yaml
Prove: admission accepts the complete object. Actually starting it is a separate runtime test; this dry-run grants no readiness claim. Keep the namespace policy in place throughout. Cleanup: delete only the example namespace when finished.
Repeat: remove one required field at a time and predict which message appears.
Coach Caz: The bouncer checks your form at the door. Arguing with the scheduler will not get you inside. Pod Security Standards ↗
Supply chain: compare evidence, not image names
Use the same scanner database and options when comparing images. A vulnerability count varies over time and says nothing about whether the image source is trusted. Retain the scan output and chosen image identity. A workload hardening score is another signal, not a guarantee. Verify the application still starts after changing its image and security context: Reduce image risk and harden a workload (kubefit drill cks-04-supply-chain). Trivy image scanning ↗.
Manifests can be scanned before they reach the cluster. trivy config reports misconfigurations with an ID and a severity, and --severity HIGH,CRITICAL --exit-code 1 turns it into a gate. The fix is in the workload spec, never in a .trivyignore file: remove privileged and hostNetwork, pin tags from the same repository, add a pod-level securityContext with runAsNonRoot, and give containers a read-only root filesystem with a writable emptyDir where the process needs one. Proof is the scan exiting 0 and the Deployments 1/1 ready afterwards: Fix manifest misconfigurations found by Trivy (kubefit drill cks-15-trivy-config-scan). Trivy misconfiguration scanning ↗.
An SBOM (trivy image --format cyclonedx) is the inventory of what an image contains; a vulnerability report (--format json) is what that inventory is exposed to on the day of the scan. Keep both as files, reduce the report to one number with jq over every Results[] entry, and expect to reproduce that number on demand with the same database (--skip-db-update). --ignore-unfixed and counting only the first result are the usual ways the number drifts: Generate an SBOM and scan an image (kubefit drill cks-17-sbom-and-scan). Trivy SBOM ↗.
A tag can move; a digest identifies immutable image content. Distinguish a multi-platform image-index digest from its platform-specific manifest digests. A runtime's Pod imageID is not guaranteed to be the multi-platform index, so do not copy it while claiming portability to every architecture. Resolve the approved artifact through its registry metadata, pin the intended digest, and verify the running platform's resolved image belongs to it. imagePullPolicy: Always does not verify signatures. Pin an image to its digest (kubefit drill cks-18-image-digest-pinning). Image identity ↗ · Multi-platform images ↗
Explore this section
Practice loop: pinning and scanning answer different questionsPractice loop: pinning and scanning answer different questions
Starting state: a practice workload web exists in concepts-security; Trivy is installed. Inspect the actual image before scanning a familiar-looking name:
kubectl -n concepts-security get deployment web -o jsonpath='{.spec.template.spec.containers[*].image}{"\n"}'
Use the resolved, approved immutable reference as IMAGE_REF below. Obtain it from the registry/release metadata; do not invent a digest or assume a multi-platform index equals a Pod's platform imageID.
IMAGE_REF='registry.example.com/team/web@sha256:REPLACE_WITH_VERIFIED_DIGEST'
trivy image --severity HIGH,CRITICAL --exit-code 1 "$IMAGE_REF"
Look for: the package, installed version, vulnerability ID, and fixed version. Exit 1 from this command can indicate policy findings; tool/network/database errors also need attention. Read the report and diagnostics rather than calling every nonzero exit an application vulnerability.
Act: evaluate a rebuilt, fixed candidate by its own digest, using a consistent scanner database and policy. Verify its signature under the release's trusted key or identity policy. A digest establishes content identity, not publisher trust; a low vulnerability count proves neither.
Prove: the deployed spec references the approved digest, the release signature verifies, the scan meets policy, and the application remains healthy. Keep those pieces of evidence attached to the same artifact.
Repeat: explain which check detects a moved tag, an untrusted publisher, and a vulnerable dependency.
Coach Caz: “Low fat” on the container is not a nutrition label. Read the contents and check who packed it. Trivy scanning ↗
Runtime evidence: explain who did what
Audit policy selects which API activity to record and at what detail. First-match ordering matters. Logging Secret bodies can disclose the very data you are protecting; use the requested metadata-only rule. A policy file on the node is insufficient unless the API server can mount and read it. Retain a recovery copy outside the static-manifest directory, verify API health, and generate a known event: Configure API audit logging (kubefit drill cks-05-audit-logging). Kubernetes auditing ↗.
Reading the log is the other half. Resource-request audit events can carry user.username, verb, objectRef and sourceIPs; available fields depend on the request and event. Filter on the object and the verb with jq 'select(.verb=="delete" and .objectRef.resource=="secrets" and .objectRef.name==...)' instead of paging through the file. Act on the identity the event names, not the one you suspect: record it, revoke the RoleBinding that grants it the verb, and keep the ServiceAccount and the log as evidence. Proof is kubectl auth can-i delete secrets --as=<that identity> answering no: Trace a deletion through the audit log (kubefit drill cks-14-audit-forensics).
Runtime detection: write a rule, trigger it, investigate it
Falco evaluates events against rule conditions. A rule needs an event source, matching condition, descriptive output, and priority. Macros and lists make conditions reusable; they do not eliminate the need to check which events and fields the installed driver supports. Alerting is detection, not automatic prevention. Falco rule elements ↗
Start with a controlled behavior, such as launching a shell in a test container. Validate the rule, trigger the event, and confirm its output identifies the workload, process, and time. Run a legitimate control action to detect an overly broad condition. Correlate the alert with API audit events and workload logs before attributing an attack. Preserve evidence before isolating the affected workload and rotating exposed credentials. Prove it: reproduce both the alert and the benign control; an empty alert log alone proves nothing. Falco rule execution remains supplemental to the current CKS drills.
For each attempt, record the protected resource, required access, rejected access and evidence source. Repeat from a fresh lab state before using the target time as a measure of fluency.
Practice loop: turn an audit event into a scoped response
Starting state: the practice task supplies an API audit JSON-lines file, copied to audit.jsonl in your working directory. jq is installed. You are investigating a Secret deletion, not printing Secret contents.
jq -c 'select(.verb=="delete" and .objectRef.resource=="secrets") | {time:.requestReceivedTimestamp,user:.user.username,namespace:.objectRef.namespace,name:.objectRef.name,code:.responseStatus.code}' audit.jsonl
Illustrative evidence:
{"user":"system:serviceaccount:concepts-security:reporter","namespace":"concepts-security","name":"dummy-key","code":200}
Look for: an event for the target object, the authenticated identity, response code, and time. A denied request and a successful deletion are different findings. Audit stages can produce multiple records for one request; correlate auditID when needed. A service account identifies credentials used, not automatically the human responsible.
Act: inspect how that identity obtained delete permission, preserve the evidence, and remove the inappropriate grant under the task's constraints. Avoid deleting the identity or logs as your first move. For a runtime-shell alert, correlate process/container evidence from the detector with these API records; API audit logs are not a complete syscall history.
Prove: kubectl auth can-i delete secrets -n concepts-security --as=system:serviceaccount:concepts-security:reporter returns no, required work still succeeds, and the original record remains available.
Repeat: change the query to a denied Pod creation. State which fields establish action, target, identity, and result.
Coach Caz: Investigate the rep, not the person standing nearest the rack. Evidence first. Audit event fields ↗
Further applied practice
Grant access to one named Secret
resourceNames narrows a permitted resource type to specific names. A get-only rule supports a known credential lookup without revealing the namespace’s Secret inventory. Test the effective authorization because grants elsewhere are additive.
Reject unapproved application images at admission
A policy must be bound before it enforces anything. Namespace matching limits its blast radius. Test with server-side dry-run: a client-side render never reaches admission. Prefix matching is this lab’s explicit rule, not proof of signature verification or safe image contents.
Turn a scan report into a precise release decision
No fixed version does not mean no vulnerability. Deduplication prevents repeated package findings from inflating the decision summary. Apply the stated release policy, preserve the original report, and separate analysis of a supplied report from a fresh scan.
Trivy vulnerability scanning ↗ application security checklist ↗
Revoke a leaked Pod-bound API token
A bound token depends on the identity and lifetime of its bound object. Recreating a Pod with the same name gives it a different UID and does not revive a token bound to the deleted Pod.
Verify a signed image release statement
A signature binds exact bytes to a trusted key. Verify the signature before trusting the image digest, and use an altered document as a negative control.