Back

K05: Inadequate Logging and Monitoring - Code Examples

Each pair below shows an insecure (blind) configuration and the secure version that captures, ships, and alerts on the signal. The three areas mirror the layers of a working detection stack: API-server audit logging, runtime detection with Falco, and centralisation and alerting (Fluent Bit → SIEM, Prometheus/Alertmanager).

1. Kubernetes API Audit Logging

Insecure

# kube-apiserver started with NO audit configuration.
# There is no --audit-policy-file and no --audit-log-path.
kube-apiserver \
  --etcd-servers=https://127.0.0.1:2379 \
  --authorization-mode=Node,RBAC
  # (no audit flags at all)

# Result:
#   - No record of who exec'd into which pod
#   - No record of secret reads or RBAC changes
#   - No record of privileged-pod creation
#   - Nothing to investigate with after an incident

An equally common variant "has audit logging" but with a policy that captures nothing useful:

# audit-policy.yaml  (INSECURE: logs nothing meaningful)
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: None          # everything is dropped
# The flag is present, a file is configured, and the log is empty of signal.

Secure

# audit-policy.yaml  (SECURE: tiered by sensitivity)
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages: ["RequestReceived"]
rules:
  # Full request+response for crown-jewel resources
  - level: RequestResponse
    resources:
      - group: ""
        resources: ["secrets", "configmaps", "serviceaccounts"]
      - group: "rbac.authorization.k8s.io"
        resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
  # Interactive access to pods, in full
  - level: RequestResponse
    resources:
      - group: ""
        resources: ["pods/exec", "pods/attach", "pods/portforward"]
  # Deliberately drop high-volume, low-value noise to control cost
  - level: None
    users: ["system:kube-scheduler", "system:kube-proxy"]
    verbs: ["watch", "get"]
  - level: None
    resources:
      - group: ""
        resources: ["events"]
  # Everything else still gets cheap, useful metadata
  - level: Metadata
# Wire it into the apiserver (self-managed)
kube-apiserver \
  --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
# Managed clusters: enable the provider's audit/control-plane logging
# feature (off or minimal by default) and export it to your log sink.

Why it matters: The secure policy makes exec, secret access, and RBAC changes expensive to hide—each produces a full, attributable record—while keeping routine, high-volume traffic cheap so the log stays affordable and readable.

2. Runtime Threat Detection (Falco)

Insecure

# No runtime sensor is deployed anywhere in the cluster.
$ kubectl get daemonset -A | grep -iE 'falco|tetragon'
# (no output)

# Consequence: a reverse shell inside a container, a read of the
# service-account token by curl, a sensitive host mount, and an
# outbound connection to a mining pool all execute with ZERO
# process-level telemetry. The control plane never sees them.

Secure

# Deploy Falco as a DaemonSet on every node (Helm)
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco \
  --namespace falco --create-namespace \
  --set falcosidekick.enabled=true \
  --set falcosidekick.config.slack.webhookurl=$SLACK_WEBHOOK
# custom-rules.yaml - high-signal runtime rules
- macro: container
  condition: container.id != host

- macro: shell_procs
  condition: proc.name in (bash, sh, zsh, ash, dash, ksh)

# 1) A shell spawned inside a container (classic post-exploitation)
- rule: Shell Spawned In Container
  desc: An interactive shell started inside a running container
  condition: >
    spawned_process and container and shell_procs
    and proc.tty != 0
  output: >
    Shell in container (user=%user.name container=%container.name
    image=%container.image.repository proc=%proc.cmdline
    pod=%k8s.pod.name ns=%k8s.ns.name)
  priority: WARNING
  tags: [container, shell, mitre_execution]

# 2) The service-account token read by an unexpected process
- rule: SA Token Read By Unexpected Process
  desc: Something other than the app read the mounted SA token
  condition: >
    open_read and container
    and fd.name contains "/var/run/secrets/kubernetes.io/serviceaccount/token"
    and not proc.name in (java, python, node, app)
  output: >
    SA token read (proc=%proc.cmdline file=%fd.name
    pod=%k8s.pod.name ns=%k8s.ns.name)
  priority: CRITICAL
  tags: [container, secrets, mitre_credential_access]

# 3) A sensitive host path mounted / accessed (escape primitive)
- rule: Sensitive Host Path Accessed
  desc: Container touched a sensitive host directory
  condition: >
    open_read and container
    and (fd.name startswith /proc/1/ or
         fd.name startswith /etc/kubernetes or
         fd.name startswith /var/lib/kubelet)
  output: >
    Sensitive host path accessed (file=%fd.name
    container=%container.name pod=%k8s.pod.name)
  priority: CRITICAL
  tags: [container, escape, mitre_privilege_escalation]

# 4) Outbound connection to an unexpected port (miner / C2 heuristic)
- rule: Unexpected Outbound Connection
  desc: Container initiated egress on a non-standard port
  condition: >
    outbound and container
    and not fd.sport in (53, 80, 443)
  output: >
    Unexpected egress (connection=%fd.name proc=%proc.cmdline
    pod=%k8s.pod.name ns=%k8s.ns.name)
  priority: NOTICE
  tags: [network, mitre_exfiltration]

Why it matters: These rules turn the invisible half of the attack—what runs inside the container and on the node—into concrete, routable events. Falco ships bundled defaults; the custom rules above add the highest-signal Kubernetes-specific detections.

3. Centralisation and Alerting

Insecure

# Logs live only on the node, read ad hoc:
$ kubectl logs payments-api-6d4 -n prod
# Backing files rotate and vanish on pod restart:
#   /var/log/pods/prod_payments-api-6d4_<uid>/app/0.log
# Nothing is shipped off-cluster. An attacker with node access runs:
$ rm -f /var/log/pods/*/*/*.log /var/log/containers/*.log
# ...and the only copy of the evidence is gone.
# There is no alerting: dashboards exist but no rule pages anyone.

Secure — ship everything off-cluster with Fluent Bit

# fluent-bit.conf - DaemonSet tails node-local logs + the audit log
[SERVICE]
    Flush        5
    Log_Level    info

[INPUT]
    Name              tail
    Path              /var/log/containers/*.log
    Tag               kube.*
    Refresh_Interval  5

[INPUT]
    Name              tail
    Path              /var/log/kubernetes/audit.log
    Tag               audit.*

[FILTER]
    Name                kubernetes
    Match               kube.*
    Merge_Log           On
    Keep_Log            Off

[OUTPUT]
    Name    es                     # or loki / splunk / http -> SIEM
    Match   *
    Host    logs.internal.example  # sink OUTSIDE the cluster account
    Port    9200
    tls     On
    Retry_Limit  5
    # Target index/bucket configured as append-only / object-locked (WORM)

Secure — alert on high-risk events with Prometheus/Alertmanager

# An exporter converts audit + Falco events into metrics; alert on them.
# prometheus-rules.yaml
groups:
- name: k8s-security
  rules:
  - alert: ExecIntoProdPod
    expr: increase(k8s_audit_pod_exec_total{namespace="prod"}[5m]) > 0
    for: 0m
    labels: { severity: high }
    annotations:
      summary: "kubectl exec into prod pod {{ $labels.pod }}"
      runbook: "https://runbooks.example/k8s-exec"

  - alert: PrivilegedPodCreated
    expr: increase(k8s_audit_privileged_pod_created_total[5m]) > 0
    labels: { severity: critical }
    annotations:
      summary: "Privileged pod created in {{ $labels.namespace }}"

  - alert: ClusterAdminBindingCreated
    expr: increase(k8s_audit_clusteradmin_binding_total[5m]) > 0
    labels: { severity: critical }

  - alert: FalcoCriticalRuntimeEvent
    expr: increase(falco_events_total{priority="Critical"}[5m]) > 0
    labels: { severity: critical }
    annotations:
      summary: "Critical Falco event: {{ $labels.rule }}"
# alertmanager.yaml - critical events page on-call, not a silent channel
route:
  receiver: soc-default
  routes:
    - matchers: [ severity="critical" ]
      receiver: soc-pager
receivers:
  - name: soc-default
    slack_configs: [{ channel: "#k8s-alerts", api_url: "$SLACK_WEBHOOK" }]
  - name: soc-pager
    pagerduty_configs: [{ routing_key: "$PD_ROUTING_KEY" }]

Why it matters: Fluent Bit moves evidence off the compromised asset in near-real-time to tamper-resistant storage, so cleanup cannot erase it. Prometheus/Alertmanager closes the loop—the high-risk events become pages a human receives, with a runbook attached.

What Changed, and Why

ControlInsecure (blind)Secure (sees itself)
API auditNo policy, or level: NoneTiered policy; RequestResponse for exec/secrets/RBAC
RuntimeNo sensor deployedFalco DaemonSet + high-signal custom rules
Log storageNode-local, rotates, attacker-deletableShipped off-cluster, append-only, retained
AlertingDashboards nobody watchesPrometheus rules page on-call with runbooks
OutcomeMonths of undetected dwell timeMinutes-to-detect on high-risk events

Next Steps