Three days later: no audit backend, a Falco DaemonSet nobody ships, and a compromised pod
Five nodes, kube-apiserver running as a kubeadm static pod with no --audit-* flags set. Falco runs as a DaemonSet with the modern eBPF driver and its default output to stdout; there is no log shipper on the cluster. Namespace prod holds 60 pods, and the group developer is granted create on pods/exec through an aggregated ClusterRole. During an incident you learn that three days ago someone ran kubectl exec -it -n prod checkout-7c9f -- sh -c 'cat /etc/shadow'. You have no record of who, when, or from where. You are asked to make this class of activity detectable and to handle the next occurrence without destroying evidence.
1. Design the audit configuration to capture interactive access and sensitive reads, and state how it is applied and verified.
Read the worked answer
With no --audit-log-path and no webhook configured, the audit backend is disabled, so nothing was ever persisted - this is not a retention problem, it is an absent control. Note that the default policy (used when no policy file is supplied) logs at Metadata level, but only if a backend exists.
Minimal, cost-aware policy (audit.k8s.io/v1), rules evaluated in order:
rules:
- level: None
nonResourceURLs: [/healthz, /livez, /readyz, /metrics]
- level: Request
resources:
- group: ""
resources: [pods/exec, pods/attach, pods/portforward]
verbs: [create]
- level: Metadata
resources:
- group: ""
resources: [secrets, configmaps, serviceaccounts/token]
verbs: [get, list, watch, create]
- level: Metadata
omitStages: [RequestReceived]
exec is a create on the pods/exec subresource, and the command line appears in the event's requestURI query string, which is present at Metadata and above - so Request is enough and you never need RequestResponse for exec. Never log secrets at Request or RequestResponse: that writes secret bodies to disk in plaintext. omitStages: [RequestReceived] halves the volume.
Apply by adding --audit-policy-file=/etc/kubernetes/audit/policy.yaml, --audit-log-path=/var/log/kubernetes/audit/audit.log, --audit-log-maxsize, --audit-log-maxbackup=10, --audit-log-format=json to the kube-apiserver static pod, plus hostPath volumes and mounts for both the policy file and the log directory; the kubelet restarts the pod. Verify with kubectl exec -n prod <pod> -- true then grep pods/exec /var/log/kubernetes/audit/audit.log. Remember the log lives on the control-plane node's disk: it is lost with the node, so ship it off-box.
2. Falco has been running the whole time and did not alert. What are the plausible reasons, and how would you write and tune a rule for a shell in a container?
Read the worked answer
First, Falco alerts are not the same as Falco detecting. Output to stdout means the event went into the container's log stream, which is lost when the pod restarts and is invisible unless someone runs kubectl logs. A detection pipeline with no shipping is telemetry theatre: add a DaemonSet shipper (fluent-bit/vector) for /var/log/containers or a Falco sidecar, or set program_output/http_output to a collector. Second, verify the driver is actually instrumenting: kubectl logs -n falco ds/falco | grep -i 'driver\|eBPF' - modern-bpf silently degrades on some kernels and managed-node configurations. Third, check the rule fired but was suppressed by a default macro or an exception that is broader than intended.
A rule:
- rule: Interactive Shell in Application Container
desc: A shell was spawned inside a workload container
condition: >
spawned_process and container
and proc.name in (sh, bash, dash, ash, zsh)
and not proc.pname in (runc, containerd-shim, crun)
output: >
Shell spawned in container (user=%user.name pod=%k8s.pod.name ns=%k8s.ns.name
cmd=%proc.cmdline parent=%proc.pname image=%container.image.repository)
priority: Notice
tags: [container, shell]
Tune by namespace rather than by disabling: add and not k8s.ns.name in (kube-system) or a named exception block listing the specific workload images that legitimately start shells (init containers in some base images do). Keep the broad rule and narrow the exception list, so a new shell gets caught by default.
3. A `prod` pod is now suspected of being compromised. Give the containment and evidence steps in order, and say what you deliberately do not do.
Read the worked answer
Do not kubectl delete the pod first. Deleting it destroys the container filesystem and any process state, and the ReplicaSet immediately replaces it, so you lose both evidence and the artefact you were investigating.
- Isolate the network, not the pod.
kubectl label pod <name> -n prod quarantine=trueand apply aNetworkPolicyselectingquarantine=truewithpolicyTypes: [Ingress, Egress]and no rules. Caveats: conntrack entries for existing connections may persist briefly, and ahostNetworkpod is unaffected - checkspec.hostNetworkand, if set, cordon the node instead. - Capture volatile state before anything restarts.
kubectl get pod -o yamland the ReplicaSet/Deployment spec (with the resolved image digest),kubectl logs --previousand current logs,kubectl describe podfor events, andkubectl get events -n prod. - Inspect without trusting the container's own tools. Use
kubectl debug -it <pod> --image=<hardened-tools> --target=<container>so the ephemeral container shares the target's process namespace; an attacker who replacedpsornetstatin the app image should not be the tool you investigate with. - Scope the blast radius. Determine what the pod could reach: its ServiceAccount and
kubectl auth can-i --list --as=system:serviceaccount:prod:<sa>, mounted secrets and ConfigMaps, and - most important - whetherautomountServiceAccountTokengave it API credentials. Treat every reachable secret as compromised. - Remediate. Rotate or delete the ServiceAccount tokens and any credentials the pod could read, roll the workload from a known-good pinned digest, and only then remove the quarantined pod. Finally, check the audit log for
pods/execon that pod to reconstruct who was inside it, and review thedevelopergroup'spods/execgrant - exec into production is itself a privileged operation, not everyday access.