CKS — Audit Logging Lab
Audit Logging Lab
CKS Exam Domain: Domain 6 — Monitoring, Logging & Runtime Security (20%)
This lab walks you through the complete audit logging workflow — from writing an audit policy to configuring the API server, generating events, and analyzing the results with jq. These exercises mirror the kind of multi-step tasks you will encounter on the CKS exam.
Prerequisites
- A kubeadm-based Kubernetes cluster (single or multi-node)
- SSH access to the control plane node with root/sudo privileges
jqinstalled on the control plane node- Familiarity with editing kube-apiserver static Pod manifests
Exercise 1: Create an Audit Policy File
Objective: Write an audit policy that logs secrets at Request level, pods at Metadata level, and includes a Metadata catch-all.
Steps
- Create the audit directory:
sudo mkdir -p /etc/kubernetes/audit
- Create the audit policy file:
cat <<'EOF' | sudo tee /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
omitManagedFields: true
rules:
# Don't log health check endpoints
- level: None
nonResourceURLs:
- "/healthz*"
- "/readyz*"
- "/livez*"
# Don't log events resource (noisy)
- level: None
resources:
- group: ""
resources: ["events"]
# Log Secret access at Request level (captures secret values)
- level: Request
resources:
- group: ""
resources: ["secrets"]
# Log Pod operations at Metadata level
- level: Metadata
resources:
- group: ""
resources: ["pods"]
# Catch-all: log everything else at Metadata
- level: Metadata
EOF
- Validate the YAML syntax:
python3 -c "import yaml; yaml.safe_load(open('/etc/kubernetes/audit/audit-policy.yaml'))" && echo "YAML is valid"
Expected Output
YAML is valid
The file should exist at /etc/kubernetes/audit/audit-policy.yaml with correct YAML syntax and the rules in the order specified.
Exercise 2: Configure the API Server with Audit Logging
Objective: Modify the kube-apiserver manifest to enable audit logging with the policy from Exercise 1.
Steps
- Back up the current API server manifest:
sudo cp /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/kube-apiserver.yaml.bak
- Create the audit log directory:
sudo mkdir -p /var/log/kubernetes/audit
- Edit the API server manifest:
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml
- Add the audit flags to the
kube-apiservercommand section (add these lines after the existing flags):
- --audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
- Add the volumeMounts (inside the
containers[0].volumeMountssection):
- mountPath: /etc/kubernetes/audit
name: audit-policy
readOnly: true
- mountPath: /var/log/kubernetes/audit
name: audit-log
- Add the volumes (inside the
spec.volumessection):
- hostPath:
path: /etc/kubernetes/audit
type: DirectoryOrCreate
name: audit-policy
- hostPath:
path: /var/log/kubernetes/audit
type: DirectoryOrCreate
name: audit-log
-
Save and exit. The kubelet will detect the change and restart the API server.
-
Wait for the API server to restart:
# Wait until the API server responds
echo "Waiting for API server..."
until kubectl get nodes &>/dev/null; do
sleep 2
echo -n "."
done
echo " Ready!"
- Verify the API server has audit flags:
ps aux | grep kube-apiserver | grep audit-policy-file
Expected Output
root ... kube-apiserver --advertise-address=... --audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml --audit-log-path=/var/log/kubernetes/audit/audit.log ...
Troubleshooting
If the API server does not come back:
# Check kubelet logs for the error
sudo journalctl -u kubelet --since "3 minutes ago" | grep -i "error\|fail" | tail -20
# Common errors:
# - YAML syntax error in kube-apiserver.yaml → restore from backup
# - Policy file not found → verify the volume mount path
# - Permission denied → verify directory ownership
# Restore from backup if needed
sudo cp /tmp/kube-apiserver.yaml.bak /etc/kubernetes/manifests/kube-apiserver.yaml
Exercise 3: Generate Audit Events
Objective: Perform various API operations to generate audit events, then verify they were logged.
Steps
- Create a secret:
kubectl create secret generic audit-test-secret \
--from-literal=password=SuperSecret123 \
-n default
- Read the secret:
kubectl get secret audit-test-secret -n default -o yaml
- Create a pod and exec into it:
kubectl run audit-test-pod --image=nginx:1.25 --restart=Never -n default
# Wait for the pod to be running
kubectl wait --for=condition=Ready pod/audit-test-pod -n default --timeout=60s
# Exec into the pod
kubectl exec -it audit-test-pod -n default -- whoami
- Create a role and rolebinding:
kubectl create role audit-test-role \
--verb=get,list \
--resource=pods \
-n default
kubectl create rolebinding audit-test-binding \
--role=audit-test-role \
--serviceaccount=default:default \
-n default
- Delete a resource:
kubectl delete secret audit-test-secret -n default
- Verify audit events are being written:
# Check log file exists and has content
ls -la /var/log/kubernetes/audit/audit.log
wc -l /var/log/kubernetes/audit/audit.log
Expected Output
The audit log file should exist and contain multiple lines (JSON events). Each line is one audit event.
Exercise 4: Analyze Audit Logs with jq
Objective: Use jq to find specific events in the audit log generated from Exercise 3.
Steps
- Find all Secret access events:
cat /var/log/kubernetes/audit/audit.log | jq 'select(.objectRef.resource=="secrets") | {
time: .requestReceivedTimestamp,
user: .user.username,
verb: .verb,
name: .objectRef.name,
namespace: .objectRef.namespace,
code: .responseStatus.code
}'
Expected: You should see events for create, get, and delete operations on audit-test-secret.
- Find the secret creation event specifically:
cat /var/log/kubernetes/audit/audit.log | jq 'select(
.objectRef.resource=="secrets" and
.objectRef.name=="audit-test-secret" and
.verb=="create"
) | {
user: .user.username,
verb: .verb,
name: .objectRef.name,
code: .responseStatus.code
}'
- Find pod exec events:
cat /var/log/kubernetes/audit/audit.log | jq 'select(
.objectRef.resource=="pods" and
.objectRef.subresource=="exec"
) | {
time: .requestReceivedTimestamp,
user: .user.username,
pod: .objectRef.name,
namespace: .objectRef.namespace
}'
- Find all failed requests (4xx and 5xx):
cat /var/log/kubernetes/audit/audit.log | jq 'select(.responseStatus.code >= 400) | {
user: .user.username,
verb: .verb,
resource: .objectRef.resource,
name: .objectRef.name,
code: .responseStatus.code
}' | head -20
- Find all delete operations:
cat /var/log/kubernetes/audit/audit.log | jq 'select(.verb=="delete") | {
time: .requestReceivedTimestamp,
user: .user.username,
resource: .objectRef.resource,
name: .objectRef.name,
namespace: .objectRef.namespace
}'
- Count events by verb:
cat /var/log/kubernetes/audit/audit.log | jq -r '.verb' | sort | uniq -c | sort -rn
- Count events by user:
cat /var/log/kubernetes/audit/audit.log | jq -r '.user.username' | sort | uniq -c | sort -rn | head -10
Expected Output
You should see a mix of events from your manual operations (kubernetes-admin user) and system components. The Secret events should appear at Request level (with request body), while Pod events at Metadata level (no body).
Exercise 5: Add a Rule to Audit RBAC Changes at RequestResponse Level
Objective: Modify the audit policy to capture RBAC changes at RequestResponse level and verify the new rule works.
Steps
- Edit the audit policy file:
sudo vi /etc/kubernetes/audit/audit-policy.yaml
- Add the RBAC rule before the catch-all rule. The updated policy should look like:
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
omitManagedFields: true
rules:
# Don't log health check endpoints
- level: None
nonResourceURLs:
- "/healthz*"
- "/readyz*"
- "/livez*"
# Don't log events resource
- level: None
resources:
- group: ""
resources: ["events"]
# Log Secret access at Request level
- level: Request
resources:
- group: ""
resources: ["secrets"]
# NEW: Log RBAC changes at RequestResponse level
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
verbs: ["create", "update", "patch", "delete"]
# Log Pod operations at Metadata level
- level: Metadata
resources:
- group: ""
resources: ["pods"]
# Catch-all
- level: Metadata
- Restart the API server (since the policy file is read at startup):
# Touch the manifest to trigger a restart
# (kubeadm/kubelet watches for file changes)
sudo touch /etc/kubernetes/manifests/kube-apiserver.yaml
If the API server does not restart automatically, force it:
# Find the kube-apiserver container
sudo crictl ps | grep kube-apiserver
# Stop the container (kubelet will restart it)
sudo crictl stop <CONTAINER_ID>
- Wait for the API server to come back:
until kubectl get nodes &>/dev/null; do sleep 2; done
echo "API server is back"
Exercise 6: Verify the New Audit Rule Captures RoleBinding Creation
Objective: Create an RBAC RoleBinding and verify that the audit log captures it at RequestResponse level (including the response body).
Steps
- Create a new RoleBinding:
kubectl create rolebinding rbac-audit-test \
--role=audit-test-role \
--serviceaccount=default:default \
-n default
- Find the audit event for this RoleBinding creation:
cat /var/log/kubernetes/audit/audit.log | jq 'select(
.objectRef.resource=="rolebindings" and
.objectRef.name=="rbac-audit-test" and
.verb=="create"
)'
- Verify the event is at
RequestResponselevel (should includeresponseObject):
cat /var/log/kubernetes/audit/audit.log | jq 'select(
.objectRef.resource=="rolebindings" and
.objectRef.name=="rbac-audit-test" and
.verb=="create"
) | {
level: .level,
user: .user.username,
verb: .verb,
name: .objectRef.name,
hasRequestObject: (.requestObject != null),
hasResponseObject: (.responseObject != null),
responseKind: .responseObject.kind
}'
Expected Output
{
"level": "RequestResponse",
"user": "kubernetes-admin",
"verb": "create",
"name": "rbac-audit-test",
"hasRequestObject": true,
"hasResponseObject": true,
"responseKind": "RoleBinding"
}
Key verification points:
levelisRequestResponse(notMetadata)hasRequestObjectistrue— the request body was loggedhasResponseObjectistrue— the response body was loggedresponseObject.kindisRoleBinding— the full RoleBinding resource is in the log
- Inspect the full response object:
cat /var/log/kubernetes/audit/audit.log | jq 'select(
.objectRef.resource=="rolebindings" and
.objectRef.name=="rbac-audit-test" and
.verb=="create"
) | .responseObject'
This shows the complete RoleBinding resource as returned by the API server, including server-generated fields like metadata.uid, metadata.creationTimestamp, and metadata.resourceVersion.
Cleanup
Remove the test resources created during the lab:
# Delete test resources
kubectl delete rolebinding audit-test-binding rbac-audit-test -n default --ignore-not-found
kubectl delete role audit-test-role -n default --ignore-not-found
kubectl delete pod audit-test-pod -n default --ignore-not-found
kubectl delete secret audit-test-secret -n default --ignore-not-found
To remove audit logging entirely (not recommended — keep it for security):
# Remove audit flags from kube-apiserver.yaml
# Remove the volumeMounts and volumes for audit-policy and audit-log
# The API server will restart without audit logging
# Clean up log files
sudo rm -rf /var/log/kubernetes/audit/
# Optionally remove the policy file
sudo rm -rf /etc/kubernetes/audit/
Lab Summary
| Exercise | What You Practiced | Key Skill |
|---|---|---|
| 1 | Writing an audit policy YAML | Policy structure, rule ordering, audit levels |
| 2 | Configuring kube-apiserver | Manifest editing, volumeMounts, volumes, flag configuration |
| 3 | Generating audit events | Creating secrets, pods, exec, RBAC resources |
| 4 | Analyzing logs with jq | Filtering by resource, verb, user, status code |
| 5 | Adding a new audit rule | Policy modification, API server restart |
| 6 | Verifying RequestResponse level | Confirming request/response bodies are captured |
Practice Questions
Question 1
You configured audit logging but the audit log file is empty. What are the most likely causes?
Show Answer
Most likely causes:
- The API server did not restart with the new flags. Verify with:
ps aux | grep kube-apiserver | grep audit-policy-fileIf the flag is not present, the manifest change was not picked up. Try:
sudo crictl ps | grep kube-apiserver # If the container start time is old, force a restart -
The volumeMount path does not match the flag path. If
--audit-log-path=/var/log/kubernetes/audit/audit.logbut the volumeMount is at/var/log/audit, the API server writes to a path inside the container that is not persisted to the host. -
The policy file has only
Nonelevel rules. If every rule islevel: None, all events are suppressed and nothing is written. - The log directory does not exist or lacks write permissions. Verify:
ls -la /var/log/kubernetes/audit/ - The API server crashed and reverted to the old manifest. Check:
sudo journalctl -u kubelet --since "5 minutes ago" | grep apiserver
Question 2
Write the complete set of volumeMounts and volumes needed in the kube-apiserver manifest for audit logging.
Show Answer
volumeMounts (inside spec.containers[0].volumeMounts):
- mountPath: /etc/kubernetes/audit
name: audit-policy
readOnly: true
- mountPath: /var/log/kubernetes/audit
name: audit-log
volumes (inside spec.volumes):
- hostPath:
path: /etc/kubernetes/audit
type: DirectoryOrCreate
name: audit-policy
- hostPath:
path: /var/log/kubernetes/audit
type: DirectoryOrCreate
name: audit-log
Key details:
- The policy volume is
readOnly: truebecause the API server only reads the policy file - The log volume is NOT readOnly because the API server must write to the audit log file
type: DirectoryOrCreateensures the directory is created if it does not exist- The volume names (
audit-policy,audit-log) must match between the volumeMount and volume definitions
Question 3
After adding a new rule to the audit policy, do you need to restart the API server? How?
Show Answer
Yes, you must restart the API server. The audit policy file is read once at API server startup. Changes to the policy file are not automatically detected.
Methods to restart the API server:
- Touch the manifest file (triggers kubelet to detect a change):
sudo touch /etc/kubernetes/manifests/kube-apiserver.yamlNote: This may not always trigger a restart if the file content has not changed.
- Make a trivial change to the manifest (e.g., add/remove a blank line):
sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml # Add a comment or blank line, save - Stop the API server container directly:
sudo crictl ps | grep kube-apiserver sudo crictl stop <CONTAINER_ID>The kubelet will restart it with the same manifest, but the API server will re-read the policy file.
- Move the manifest out and back:
sudo mv /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/ sleep 5 sudo mv /tmp/kube-apiserver.yaml /etc/kubernetes/manifests/
Wait for the API server to come back:
until kubectl get nodes &>/dev/null; do sleep 2; done
Question 4
How do you verify that a specific audit rule is working correctly after configuration?
Show Answer
Verification steps:
- Identify the target resource and level. For example, if your rule logs RBAC changes at
RequestResponse: ```yaml- level: RequestResponse
resources:
- group: “rbac.authorization.k8s.io” resources: [“rolebindings”] verbs: [“create”] ```
- level: RequestResponse
resources:
- Generate a matching event:
kubectl create rolebinding test-verify --role=edit --serviceaccount=default:default -n default - Find the event in the audit log:
cat /var/log/kubernetes/audit/audit.log | jq 'select( .objectRef.resource=="rolebindings" and .objectRef.name=="test-verify" and .verb=="create" ) | {level: .level, hasRequestObject: (.requestObject != null), hasResponseObject: (.responseObject != null)}' - Verify the level matches your rule:
Metadata→requestObjectandresponseObjectshould benullRequest→requestObjectshould be non-null,responseObjectshould benullRequestResponse→ bothrequestObjectandresponseObjectshould be non-null
- Clean up:
kubectl delete rolebinding test-verify -n default
Question 5
In a CKS exam scenario, you are asked to configure audit logging so that all requests to create or delete Secrets in any namespace are logged at RequestResponse level, and all other requests are logged at Metadata level. Write the complete audit policy and the API server flags needed.
Show Answer
Audit policy (/etc/kubernetes/audit/audit-policy.yaml):
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
omitManagedFields: true
rules:
- level: None
nonResourceURLs:
- "/healthz*"
- "/readyz*"
- "/livez*"
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
verbs: ["create", "delete"]
- level: Metadata
API server flags:
--audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml
--audit-log-path=/var/log/kubernetes/audit/audit.log
--audit-log-maxage=30
--audit-log-maxbackup=10
--audit-log-maxsize=100
Required volumeMounts:
- mountPath: /etc/kubernetes/audit
name: audit-policy
readOnly: true
- mountPath: /var/log/kubernetes/audit
name: audit-log
Required volumes:
- hostPath:
path: /etc/kubernetes/audit
type: DirectoryOrCreate
name: audit-policy
- hostPath:
path: /var/log/kubernetes/audit
type: DirectoryOrCreate
name: audit-log
Key points:
- The
RequestResponserule for secrets must come before theMetadatacatch-all (first match wins) - The
verbsfilter restrictsRequestResponselogging to onlycreateanddelete—get,list, andwatchwill fall through to theMetadatacatch-all - The
Nonerule for health endpoints prevents unnecessary noise
| Previous | Up | Next |
|---|---|---|
| Audit Best Practices | CKS Home | Audit Logging |