Audit Logs ‐ Demo - ralvares/openshift-security-framework GitHub Wiki
Kubernetes Tier 1 Threat Hunting Demo with Audit Logs
This is a full step-by-step demo for hunting Kubernetes threats using audit logs on an OpenShift cluster with access to /var/log/kube-apiserver/audit.log
. It simulates a realistic attacker scenario where a pod is compromised and its ServiceAccount is used to escalate privileges and perform malicious actions.
All actions happen in the default
namespace, and detections rely only on verbs, resources, and subresources — no assumptions about usernames, pod names, or namespaces.
Step 0: Create Pod with kubectl and curl (Attacker foothold)
oc run testpod \
--image=bitnami/kubectl:latest \
-n default \
--restart=Never \
--command -- sleep infinity
Show logs (pod creation)
grep '"resource":"pods"' /var/log/kube-apiserver/audit.log | \
grep '"verb":"create"' | jq 'select(.objectRef.namespace=="default") | {
timestamp: .requestReceivedTimestamp,
user: .user.username,
verb: .verb,
resource: .objectRef.resource,
namespace: .objectRef.namespace,
name: .objectRef.name,
uri: .requestURI
} | with_entries(select(.value != null))'
Step 1: Exec into the pod (initial access)
oc exec -it testpod -n default -- sh
Show logs (exec access)
grep '"subresource":"exec"' /var/log/kube-apiserver/audit.log | \
grep '"verb":"get"' | jq 'select(.objectRef.namespace=="default") | {
timestamp: .requestReceivedTimestamp,
user: .user.username,
verb: .verb,
subresource: .objectRef.subresource,
resource: .objectRef.resource,
namespace: .objectRef.namespace,
name: .objectRef.name,
uri: .requestURI
} | with_entries(select(.value != null))'
Step 2: Escalate privileges (bind SA to cluster-admin)
oc adm policy add-cluster-role-to-user cluster-admin system:serviceaccount:default:default
⚠️ Real-World Scenario
This isn’t always a deliberate attack — it often happens because of: • Developer shortcuts: “It worked on my laptop” syndrome • SCC issues in OpenShift: When deploying a web server (e.g., httpd) that binds to privileged ports (like 80), it may fail due to SecurityContextConstraints • Temporary fixes: Admins or DevOps give cluster-admin to a pod’s SA to “just make it work” — and forget to remove it • CI/CD misconfigurations: Pipelines deploy jobs with elevated privileges as a workaround
🎬 In this demo, imagine someone deploying a httpd container, it fails to bind port 80 due to SCC restrictions, and instead of fixing the Dockerfile, someone grants cluster-admin to the pod’s ServiceAccount.
Show logs (role binding escalation)
grep '"resource":"clusterrolebindings"' /var/log/kube-apiserver/audit.log | \
grep -E '"verb":"(create|patch|update)"' | jq '{
timestamp: .requestReceivedTimestamp,
user: .user.username,
verb: .verb,
resource: .objectRef.resource,
uri: .requestURI,
response_code: .responseStatus.code,
decision: .annotations["authorization.k8s.io/decision"]
} | with_entries(select(.value != null))'
Step 3: From inside the pod, list nodes and secrets using kubectl
Inside the pod:
kubectl get nodes --token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) \
--server=https://kubernetes.default.svc --insecure-skip-tls-verify
Show logs (API access with SA)
grep '"user":{"username":"system:serviceaccount:' /var/log/kube-apiserver/audit.log | \
grep -v '"user":{"username":"system:serviceaccount:openshift-' | \
grep -E '"verb":"(get|list|create|patch|update)"' | \
jq '{
timestamp: .requestReceivedTimestamp,
user: .user.username,
verb: .verb,
resource: .objectRef.resource,
subresource: .objectRef.subresource,
uri: .requestURI,
response_code: .responseStatus.code
} | with_entries(select(.value != null))'
kubectl get secrets -A \
--token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) \
--server=https://kubernetes.default.svc \
--insecure-skip-tls-verify
🔍 Show logs (secrets listing by a ServiceAccount)
grep '"resource":"secrets"' /var/log/kube-apiserver/audit.log | \
grep '"verb":"list"' | \
grep '"user":{"username":"system:serviceaccount:' | \
grep -v '"user":{"username":"system:serviceaccount:openshift-' | \
grep -v '"user":{"username":"system:serviceaccount:kube-system:' | \
jq '{
timestamp: .requestReceivedTimestamp,
user: .user.username,
verb: .verb,
resource: .objectRef.resource,
uri: .requestURI,
response_code: .responseStatus.code,
decision: .annotations["authorization.k8s.io/decision"],
reason: .annotations["authorization.k8s.io/reason"]
} | with_entries(select(.value != null))'
Step 4: Create a CronJob from inside the pod
kubectl create cronjob eviljob --image=busybox --schedule="*/1 * * * *" -- echo pwned
🔍 Show logs (CronJob creation)
grep '"resource":"cronjobs"' /var/log/kube-apiserver/audit.log | \
grep -E '"verb":"(create|patch|update)"' | \
grep -v '"user":{"username":"system:serviceaccount:openshift-' | \
grep -v '"user":{"username":"system:serviceaccount:kube-system:' | \
jq '{
timestamp: .requestReceivedTimestamp,
user: .user.username,
verb: .verb,
resource: .objectRef.resource,
namespace: .objectRef.namespace,
name: .objectRef.name,
uri: .requestURI,
response_code: .responseStatus.code
} | with_entries(select(.value != null))'
Step 5: Port-forward to internal service
From within the pod:
kubectl port-forward testpod 9000:80 --token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) \
--server=https://kubernetes.default.svc --insecure-skip-tls-verify
Show logs (portforward)
grep '"subresource":"portforward"' /var/log/kube-apiserver/audit.log | \
grep '"verb":"get"' | \
grep -v '"user":{"username":"system:serviceaccount:openshift-' | \
grep -v '"user":{"username":"system:serviceaccount:kube-system:' | \
jq '{
timestamp: .requestReceivedTimestamp,
user: .user.username,
verb: .verb,
subresource: .objectRef.subresource,
resource: .objectRef.resource,
namespace: .objectRef.namespace,
name: .objectRef.name,
uri: .requestURI,
response_code: .responseStatus.code
} | with_entries(select(.value != null))'
This sequence simulates:
- Attacker entry into a pod
- Privilege escalation
- API abuse from inside the pod
- Persistence via CronJob
- Lateral movement or tunneling with port-forward
All tracked and confirmed via audit logs — using only verbs and resource types, and scoped to the default namespace for clarity and reproducibility.