Lesson  in  DevSecOps in Practice: Container & Kubernetes Security

Kubernetes Audit Logging

Enable audit logging on a kubeadm cluster by editing the kube-apiserver static pod manifest, write an audit policy that captures secret access and RBAC changes, generate real API events, and read the structured log entries.

What Kubernetes Audit Logging Captures

Audit logs answer: "Who did what, to which resource, and when?" Without them, a security incident is impossible to investigate.

LevelWhat is recorded
NoneNothing
MetadataUser, resource, verb — no body
RequestMetadata + request body
RequestResponseMetadata + request + response body

Step 1: Write the Audit Policy

Create the policy file the API server will load. Log secrets and RBAC changes at full detail (RequestResponse), other writes at Request, and skip noise entirely.

cat > /etc/kubernetes/audit-policy.yaml << 'EOF'
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: RequestResponse
    resources:
    - group: ""
      resources: ["secrets"]

  - level: Request
    verbs: ["create", "update", "patch", "delete"]
    resources:
    - group: ""
      resources: ["pods"]

  - level: RequestResponse
    resources:
    - group: "rbac.authorization.k8s.io"
      resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]

  - level: None
    nonResourceURLs: ["/healthz", "/readyz", "/livez", "/metrics"]

  - level: None
    users: ["system:kube-proxy"]
    verbs: ["watch"]

  - level: Metadata
    omitStages:
    - RequestReceived
EOF

echo "Policy written."

Step 2: Enable Audit Logging in the API Server

On a kubeadm cluster, the API server runs as a static pod — its manifest lives at /etc/kubernetes/manifests/kube-apiserver.yaml. Edit that file and kubelet automatically restarts the API server.

Add three flags and two volume mounts:

python3 - << 'EOF'
import re

path = "/etc/kubernetes/manifests/kube-apiserver.yaml"
with open(path) as f:
    content = f.read()

# Add audit flags after the kube-apiserver command line
audit_flags = """    - --audit-log-path=/var/log/kubernetes/audit.log
    - --audit-log-maxage=7
    - --audit-log-maxbackup=3
    - --audit-log-maxsize=100
    - --audit-policy-file=/etc/kubernetes/audit-policy.yaml"""

# Insert after the first 'command:' block line that has 'kube-apiserver'
content = re.sub(
    r'(    - kube-apiserver\n)',
    r'\1' + audit_flags + '\n',
    content
)

# Add volumeMounts for the audit files
volume_mount = """    - mountPath: /var/log/kubernetes/audit.log
      name: audit-log
      readOnly: false
    - mountPath: /etc/kubernetes/audit-policy.yaml
      name: audit-policy
      readOnly: true"""

content = re.sub(
    r'(    volumeMounts:\n)',
    r'\1' + volume_mount + '\n',
    content
)

# Add hostPath volumes
volume_def = """  - hostPath:
      path: /var/log/kubernetes/audit.log
      type: FileOrCreate
    name: audit-log
  - hostPath:
      path: /etc/kubernetes/audit-policy.yaml
      type: File
    name: audit-policy"""

content = re.sub(
    r'(  volumes:\n)',
    r'\1' + volume_def + '\n',
    content
)

with open(path, "w") as f:
    f.write(content)

print("Manifest updated. Kubelet will restart the API server.")
EOF

Wait for the API server to come back (30–60 seconds):

echo "Waiting for API server to restart..."
sleep 10
until kubectl get nodes > /dev/null 2>&1; do
  echo "  still restarting..."
  sleep 5
done
echo "API server is back."

# Confirm the flag is active
node=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
kubectl get pod "kube-apiserver-${node}" -n kube-system \
  -o jsonpath='{.spec.containers[0].command}' | tr ',' '\n' | grep audit

Expected:

--audit-log-path=/var/log/kubernetes/audit.log
--audit-policy-file=/etc/kubernetes/audit-policy.yaml

Step 3: Generate Audit Events

Every API call you make is recorded. Create some high-value events:

kubectl create namespace audit-test

kubectl create secret generic db-password \
  --from-literal=password=SuperSecret123 \
  -n audit-test

kubectl get secret db-password -n audit-test -o yaml

kubectl create serviceaccount audit-sa -n audit-test

kubectl create rolebinding audit-test-binding \
  --clusterrole=view \
  --serviceaccount=audit-test:audit-sa \
  -n audit-test

Step 4: Read the Audit Log

# Confirm the file exists and has content
ls -lh /var/log/kubernetes/audit.log

# Show recent secret and RBAC events
grep '"resource":"secrets"\|"resource":"rolebindings"' \
  /var/log/kubernetes/audit.log | tail -5 | \
  python3 -c "
import sys, json
for line in sys.stdin:
    try:
        e = json.loads(line.strip())
        verb  = e.get('verb', '')
        res   = e.get('objectRef', {}).get('resource', '')
        name  = e.get('objectRef', {}).get('name', '')
        ns    = e.get('objectRef', {}).get('namespace', '')
        user  = e.get('user', {}).get('username', '')
        level = e.get('level', '')
        code  = e.get('responseStatus', {}).get('code', '')
        print(f'[{level}] {user} | {verb} {res}/{name} (ns:{ns}) -> HTTP {code}')
    except:
        pass
"

Expected output:

[RequestResponse] kubernetes-admin | create secrets/db-password (ns:audit-test) -> HTTP 201
[RequestResponse] kubernetes-admin | get secrets/db-password (ns:audit-test) -> HTTP 200
[RequestResponse] kubernetes-admin | create rolebindings/audit-test-binding (ns:audit-test) -> HTTP 201

Why This Matters

Without audit loggingWith audit logging
"Someone deleted our secret" — no traceWho deleted it, when, from which IP
RBAC misconfiguration discovered after damageAlert fires the moment cluster-admin is bound
Compliance audit failsEvidence trail satisfies SOC 2 / PCI-DSS

Cleanup

kubectl delete namespace audit-test
Previous lesson
RBAC — Least Privilege