Exercise: audit logging, from policy to proof
Host: controlplane (root) · Time: 15 min · Domain: Monitoring, Logging and Runtime Security
Task
- Log Metadata for all namespaces at the top of the policy.
- Log RequestResponse for Secrets and ConfigMaps in namespace
config-data. - Write to
/var/log/kubernetes/audit.log, max age 30 days. - Do not log
pods/logandpods/statusrequest bodies.
Acceptance criteria
- Creating a Secret in
config-dataproduces aRequestResponserecord with a body. - Creating a Pod elsewhere produces a
Metadatarecord. pods/logrequests appear withlevel: Metadataand a redacted body./healthzstill returns ok.
Solution
# /etc/kubernetes/audit/policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages: ["RequestReceived"]
rules:
- level: None
resources:
- {group: "", resources: ["pods/log", "pods/status"]}
verbs: ["get", "list", "watch"]
- level: RequestResponse
namespaces: ["config-data"]
verbs: ["create", "update", "patch", "delete"]
resources:
- {group: "", resources: ["secrets", "configmaps"]}
- level: Metadata
# kube-apiserver.yaml
- --audit-policy-file=/etc/kubernetes/audit/policy.yaml
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=30
- --audit-log-maxbackup=10
- --audit-log-maxsize=100
volumeMounts:
- {name: audit-policy, mountPath: /etc/kubernetes/audit/policy.yaml, readOnly: true}
- {name: audit-log, mountPath: /var/log/kubernetes, readOnly: false}
volumes:
- {name: audit-policy, hostPath: {path: /etc/kubernetes/audit/policy.yaml, type: File}}
- {name: audit-log, hostPath: {path: /var/log/kubernetes, type: DirectoryOrCreate}}
sleep 20 && kubectl get --raw /healthz
kubectl -n config-data create secret generic probe --from-literal=a=b
sudo tail -1 /var/log/kubernetes/audit.log | jq -r '"\(.level) \(.verb) \(.objectRef.resource)"'
Rule order is the whole exercise: the None rule for pods/log must come
first, or the broad Metadata catch-all swallows it.