CKS — API Server Audit Configuration
API Server Audit Configuration
Kubernetes audit logging records every request to the API server — who did what, to which resource, when, and with what result. It is essential for security monitoring, incident response, and compliance. The CKS exam frequently tests audit policy configuration.
Why Audit Logging Matters
Without audit logs, you cannot:
- Detect unauthorized API access
- Investigate security incidents (who deleted that Pod? who read those Secrets?)
- Satisfy compliance requirements (SOC2, PCI-DSS, HIPAA)
- Monitor for suspicious patterns (repeated failed auth attempts, privilege escalation)
Audit Policy Levels
The audit policy defines what to record for each API request. There are four levels, each capturing progressively more data:
| Level | What Is Recorded | Use Case |
|---|---|---|
None |
Nothing — request is not logged | Exclude noisy, low-value endpoints (e.g., health checks) |
Metadata |
Request metadata: user, timestamp, resource, verb, response code | General monitoring — lightweight, low storage |
Request |
Metadata + request body | Record what was submitted (e.g., Pod spec, Secret data) |
RequestResponse |
Metadata + request body + response body | Full forensic detail — high storage cost |
Storage Impact
None → 0 bytes per request
Metadata → ~500 bytes per request
Request → ~2-10 KB per request (depends on body size)
RequestResponse → ~5-50 KB per request (doubles with response)
CKS Exam Tip: Use
RequestResponseonly for security-critical resources (Secrets, RBAC). UseMetadatafor general coverage. UseNonefor noisy endpoints.
Audit Policy Structure
The audit policy is a YAML file that defines an ordered list of rules. The first matching rule wins — order matters.
Basic Structure
apiVersion: audit.k8s.io/v1
kind: Policy
# Exclude requests to certain non-resource URLs
omitStages:
- "RequestReceived" # Optional: skip logging at this stage
rules:
# Rule 1: Most specific rules first
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
# Rule 2: Less specific
- level: Metadata
resources:
- group: ""
resources: ["pods"]
# Rule 3: Catch-all (last rule)
- level: Metadata
Rule Fields
| Field | Description | Example |
|---|---|---|
level |
Audit level for matching requests | None, Metadata, Request, RequestResponse |
resources |
API resources to match | [{group: "", resources: ["secrets"]}] |
namespaces |
Restrict to specific namespaces | ["kube-system", "production"] |
verbs |
HTTP verbs to match | ["create", "update", "delete"] |
users |
User identities to match | ["system:anonymous"] |
userGroups |
User groups to match | ["system:authenticated"] |
nonResourceURLs |
Non-resource URLs (/healthz, /metrics) |
["/healthz*", "/version"] |
omitStages |
Stages to skip for this rule | ["RequestReceived"] |
Audit Stages
Each API request passes through up to four stages. The audit policy can log at each stage:
| Stage | When | Description |
|---|---|---|
RequestReceived |
Request arrives | Before it’s delegated to handler. Logged immediately but may not have response info. |
ResponseStarted |
Response headers sent | Only for long-running requests (watch). Body not yet fully sent. |
ResponseComplete |
Response body sent | Full response available. Most commonly used. |
Panic |
A panic occurred | Internal server error. Always log. |
Recommended omitStages
# Skip RequestReceived to reduce duplicate entries
# (most events will be logged at ResponseComplete anyway)
omitStages:
- "RequestReceived"
Complete Audit Policy Example
This is a production-grade audit policy suitable for the CKS exam:
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
rules:
# 1. Do NOT log requests to the following non-resource URLs
- level: None
nonResourceURLs:
- "/healthz*"
- "/readyz*"
- "/livez*"
- "/metrics"
# 2. Do NOT log watch requests (very noisy)
- level: None
verbs: ["watch"]
# 3. Do NOT log kube-system service account token fetches
- level: None
users:
- "system:kube-proxy"
- "system:kube-scheduler"
- "system:kube-controller-manager"
verbs: ["get"]
resources:
- group: ""
resources: ["endpoints", "services", "services/status"]
# 4. Log Secret operations at RequestResponse level (full forensics)
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
# 5. Log RBAC changes at RequestResponse level
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
# 6. Log authentication-related events
- level: RequestResponse
resources:
- group: ""
resources: ["serviceaccounts", "serviceaccounts/token"]
- group: "authentication.k8s.io"
resources: ["tokenreviews"]
# 7. Log Pod creation and deletion at Request level
- level: Request
verbs: ["create", "update", "patch", "delete"]
resources:
- group: ""
resources: ["pods"]
- group: "apps"
resources: ["deployments", "daemonsets", "statefulsets", "replicasets"]
# 8. Log exec and attach (container access) at Metadata level
- level: Metadata
resources:
- group: ""
resources: ["pods/exec", "pods/attach", "pods/portforward"]
# 9. Log namespace operations
- level: Metadata
resources:
- group: ""
resources: ["namespaces"]
# 10. Catch-all: log everything else at Metadata level
- level: Metadata
Configuring the API Server for Audit Logging
Log Backend
The log backend writes audit events to a file on the control plane node.
# /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- command:
- kube-apiserver
# ... existing flags ...
- --audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit/audit.log
- --audit-log-maxage=30 # Retain logs for 30 days
- --audit-log-maxbackup=10 # Keep 10 rotated log files
- --audit-log-maxsize=100 # Rotate after 100 MB
volumeMounts:
- name: audit-policy
mountPath: /etc/kubernetes/audit
readOnly: true
- name: audit-logs
mountPath: /var/log/kubernetes/audit
volumes:
- name: audit-policy
hostPath:
path: /etc/kubernetes/audit
type: DirectoryOrCreate
- name: audit-logs
hostPath:
path: /var/log/kubernetes/audit
type: DirectoryOrCreate
API Server Audit Flags Reference
| Flag | Description | Default |
|---|---|---|
--audit-policy-file |
Path to the audit policy YAML | None (audit disabled) |
--audit-log-path |
Path for the audit log file | None |
--audit-log-maxage |
Max days to retain old log files | 0 (no limit) |
--audit-log-maxbackup |
Max number of old log files to retain | 0 (no limit) |
--audit-log-maxsize |
Max size in MB before rotation | 0 (no limit) |
--audit-log-format |
Format of audit logs (json or legacy) |
json |
--audit-webhook-config-file |
Path to webhook backend config | None |
--audit-webhook-batch-max-wait |
Max time before flushing webhook batch | 30s |
Webhook Backend
Instead of (or in addition to) writing to a log file, you can send audit events to an external webhook.
Webhook Configuration
# /etc/kubernetes/audit/webhook-config.yaml
apiVersion: v1
kind: Config
clusters:
- name: audit-webhook
cluster:
server: https://audit-collector.example.com/audit
certificate-authority: /etc/kubernetes/audit/ca.crt
contexts:
- name: audit-webhook
context:
cluster: audit-webhook
current-context: audit-webhook
API Server Flag
- --audit-webhook-config-file=/etc/kubernetes/audit/webhook-config.yaml
- --audit-webhook-batch-max-wait=5s
Log Backend vs. Webhook Backend
| Feature | Log Backend | Webhook Backend |
|---|---|---|
| Output | Local file | External HTTP endpoint |
| Reliability | High (local filesystem) | Depends on network/webhook availability |
| Centralization | Requires log shipping | Built-in centralization |
| Latency | Low | Higher (network round-trip) |
| Typical use | Simple setups, exam scenarios | Production with SIEM integration |
CKS Exam Tip: The exam almost always uses the log backend. Know the audit policy file path, log path, and volume mount configuration by heart.
Reading Audit Logs
Audit log entries are JSON objects. Here’s how to read and filter them:
Sample Audit Log Entry
{
"kind": "Event",
"apiVersion": "audit.k8s.io/v1",
"level": "RequestResponse",
"auditID": "abc-123-def",
"stage": "ResponseComplete",
"requestURI": "/api/v1/namespaces/default/secrets",
"verb": "list",
"user": {
"username": "system:serviceaccount:default:suspicious-sa",
"groups": ["system:serviceaccounts", "system:authenticated"]
},
"sourceIPs": ["10.244.1.5"],
"objectRef": {
"resource": "secrets",
"namespace": "default",
"apiVersion": "v1"
},
"responseStatus": {
"code": 200
},
"requestReceivedTimestamp": "2025-02-09T10:00:00.000000Z",
"stageTimestamp": "2025-02-09T10:00:00.050000Z"
}
Filtering Audit Logs
# Find all Secret access events
cat /var/log/kubernetes/audit/audit.log | \
jq 'select(.objectRef.resource == "secrets")'
# Find events by a specific user
cat /var/log/kubernetes/audit/audit.log | \
jq 'select(.user.username == "system:serviceaccount:default:suspicious-sa")'
# Find failed requests (non-2xx status codes)
cat /var/log/kubernetes/audit/audit.log | \
jq 'select(.responseStatus.code >= 400)'
# Find delete operations on critical resources
cat /var/log/kubernetes/audit/audit.log | \
jq 'select(.verb == "delete" and (.objectRef.resource == "deployments" or .objectRef.resource == "namespaces"))'
# Count events per user
cat /var/log/kubernetes/audit/audit.log | \
jq -r '.user.username' | sort | uniq -c | sort -rn | head -20
Step-by-Step: Enabling Audit Logging on a kubeadm Cluster
This is the most common CKS exam task for this topic:
Step 1: Create the Audit Policy File
# Create the directory
mkdir -p /etc/kubernetes/audit
# Create the audit policy
cat <<'EOF' > /etc/kubernetes/audit/audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
rules:
- level: None
nonResourceURLs:
- "/healthz*"
- "/readyz*"
- "/livez*"
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
- level: Metadata
EOF
Step 2: Create the Log Directory
mkdir -p /var/log/kubernetes/audit
Step 3: Update the API Server Manifest
Add the flags and volume mounts to /etc/kubernetes/manifests/kube-apiserver.yaml:
# Add to spec.containers[0].command:
- --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=3
- --audit-log-maxsize=100
# Add to spec.containers[0].volumeMounts:
- name: audit-policy
mountPath: /etc/kubernetes/audit
readOnly: true
- name: audit-logs
mountPath: /var/log/kubernetes/audit
# Add to spec.volumes:
- name: audit-policy
hostPath:
path: /etc/kubernetes/audit
type: DirectoryOrCreate
- name: audit-logs
hostPath:
path: /var/log/kubernetes/audit
type: DirectoryOrCreate
Step 4: Wait for API Server to Restart
# The kubelet detects the manifest change and restarts the API server
# Watch for the Pod to come back
watch kubectl get pods -n kube-system -l component=kube-apiserver
# If the API server doesn't come back, check:
# 1. YAML syntax in the audit policy
# 2. Volume mount paths exist
# 3. File permissions
crictl ps -a | grep apiserver
crictl logs <container-id>
Step 5: Verify
# Generate some API activity
kubectl get secrets -n default
# Check the audit log
tail -5 /var/log/kubernetes/audit/audit.log | jq .
# Verify Secret access is logged at RequestResponse level
cat /var/log/kubernetes/audit/audit.log | \
jq 'select(.objectRef.resource == "secrets") | {verb, user: .user.username, level}'
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Missing volume mount for audit policy | API server fails to start | Add hostPath volume + volumeMount |
| Missing volume mount for audit log path | API server fails to start | Add hostPath volume + volumeMount |
| YAML syntax error in audit policy | API server fails to start | Validate with python3 -c "import yaml; yaml.safe_load(open('policy.yaml'))" |
| Rules in wrong order | Catch-all matches before specific rules | Most specific rules FIRST, catch-all LAST |
| Logging everything at RequestResponse | Disk fills up quickly | Use None for health checks, Metadata as default |
Forgetting omitStages |
Duplicate entries for each request | Add omitStages: ["RequestReceived"] |
Practice Questions
Q1. Write an audit policy that logs all Secret access at RequestResponse level, all Pod creation/deletion at Request level, and everything else at Metadata level. Exclude health check endpoints entirely.
Show Answer
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- "RequestReceived"
rules:
# Exclude health checks
- level: None
nonResourceURLs:
- "/healthz*"
- "/readyz*"
- "/livez*"
# Log Secret access with full request and response
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
# Log Pod create/delete with request body
- level: Request
verbs: ["create", "delete"]
resources:
- group: ""
resources: ["pods"]
# Everything else at Metadata
- level: Metadata
Q2. What API server flags are required to enable audit logging with a log backend? List them with appropriate values.
Show Answer
Minimum required:
--audit-policy-file=/etc/kubernetes/audit/audit-policy.yaml
--audit-log-path=/var/log/kubernetes/audit/audit.log
Recommended additional flags:
--audit-log-maxage=30 # Retain logs for 30 days
--audit-log-maxbackup=10 # Keep 10 rotated files
--audit-log-maxsize=100 # Rotate at 100 MB
You must also add the corresponding volume mounts and hostPath volumes to the API server Pod spec.
Q3. The API server fails to start after you added audit logging configuration. How do you troubleshoot?
Show Answer
# 1. Check if the API server container is crashing
crictl ps -a | grep apiserver
# 2. Check container logs
crictl logs <container-id>
# 3. Common causes:
# a) Audit policy file doesn't exist at the specified path
ls -la /etc/kubernetes/audit/audit-policy.yaml
# b) YAML syntax error in audit policy
python3 -c "import yaml; yaml.safe_load(open('/etc/kubernetes/audit/audit-policy.yaml'))"
# c) Volume mount paths don't match flag paths
grep -A5 'audit' /etc/kubernetes/manifests/kube-apiserver.yaml
# d) Log directory doesn't exist
mkdir -p /var/log/kubernetes/audit
# e) Permission issues
ls -la /var/log/kubernetes/audit/
Q4. In an audit policy, rules are evaluated in order. What happens if the first rule is - level: None with no filters?
Show Answer
A rule with level: None and no resource, verb, or user filters matches every request. Since it’s the first rule and the first match wins, nothing will be logged at all.
# BAD — this disables all auditing
rules:
- level: None # Matches everything — catch-all at position 1
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"] # Never reached
Always put the most specific rules first and the catch-all last.
Q5. What are the four audit stages, and which one is most commonly used for logging?
Show Answer
| Stage | Description |
|---|---|
RequestReceived |
Event generated as soon as the request arrives, before processing |
ResponseStarted |
Response headers sent but body not complete (only for long-running requests like watch) |
ResponseComplete |
Response body has been fully sent — most commonly used |
Panic |
An internal server error (panic) occurred during request handling |
ResponseComplete is the most useful because it includes the final response status code. Most policies use omitStages: ["RequestReceived"] to avoid duplicate entries, since every request logged at RequestReceived will also be logged at ResponseComplete.
| Previous | Up | Next |
|---|---|---|
| Upgrade Frequency | CKS Home | Cluster Hardening Lab |