DEV Community

Cover image for Solved: Pwning Santa before the bad guys do: A hybrid bug bounty / CTF for container isolation
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Pwning Santa before the bad guys do: A hybrid bug bounty / CTF for container isolation

🚀 Executive Summary

TL;DR: Default container settings are inherently insecure, posing a significant risk of container escapes and host compromise. The article details a layered approach to container isolation, from basic privilege dropping to advanced kernel-level sandboxing, to prevent attackers from exploiting these vulnerabilities.

🎯 Key Takeaways

  • Containers are not virtual machines; they share the host kernel, making process-level isolation primitives like namespaces and cgroups susceptible to kernel exploits or misconfigurations.
  • The most critical and easiest first step in container security is to run containers as a non-root user and explicitly drop all unnecessary Linux capabilities, adding back only what is absolutely required.
  • Kernel-level guardrails like Seccomp and AppArmor provide a stronger defense by restricting the specific system calls a container can make to the host kernel, acting as a bouncer for syscalls.
  • For untrusted or highly sensitive workloads, gVisor and Kata Containers offer the strongest isolation by intercepting syscalls in a user-space kernel or a lightweight VM, effectively abstracting the host kernel interaction.

A recent Reddit thread about a container security CTF highlights a critical truth: default container settings are not secure enough. This guide breaks down the real-world risks of container escapes and provides three practical, layered solutions for hardening your environment, from quick fixes to kernel-level isolation.

Pwning Santa Before the Bad Guys Do: A DevOps War Story on Container Isolation

I remember a 3 AM PagerDuty alert that nearly gave me a heart attack. We had a critical vulnerability scanner screaming about one of our Kubernetes nodes, kube-worker-prod-07. The weird part? The alert wasn’t for the node itself, but for a process originating from a supposedly “harmless” container running a third-party marketing analytics tool. It was trying to read /proc/version on the host. It turns out the vendor’s container was running as root with way too many privileges, and a newly discovered exploit in their code gave an attacker a direct line to the underlying node. We dodged a bullet that night, but it taught me a lesson I’ll never forget: “container isolation” is a myth until you build it yourself.

The “Why”: Containers Aren’t Magic Boxes

Let’s get one thing straight: a container is not a virtual machine. It doesn’t have its own kernel. It’s just a set of processes running on the host’s kernel, wrapped in some clever isolation primitives called namespaces and cgroups. Think of it like this: you’ve put your app in a “room” (the container) inside a “house” (the host node). You’ve locked the door, but it’s still sharing the same foundation, plumbing, and electrical systems as every other room. A kernel exploit is like finding a master key to the whole house, and a misconfigured container is like leaving the window wide open.

The core problem in that Reddit CTF, and in my 3 AM war story, is that if an attacker can break out of the process-level isolation, they can potentially interact with the shared host kernel and, from there, pivot to other containers on the same node. Our job is to make that breakout as difficult as humanly possible.

The Fixes: From Finger Plugs to Fortress Walls

Alright, enough with the horror stories. How do we fix this? There isn’t a single magic button. Security is about layers. Here are three approaches I use, ranging from the quick-and-dirty to the architecturally robust.

1. The Quick Fix: Drop Privileges You Don’t Need

This is your first line of defense and the easiest to implement. By default, Docker and Kubernetes grant containers a set of Linux “capabilities” that are often unnecessary. The most common mistake is running containers as the root user. Don’t do it. Ever.

First, run as a non-root user. Second, explicitly drop all capabilities and only add back the ones your application absolutely needs. For 90% of web apps, that number is zero.

Here’s a Kubernetes securityContext example for your pod spec that does just this:

apiVersion: v1
kind: Pod
metadata:
  name: secure-app-pod
spec:
  containers:
  - name: my-secure-app
    image: my-app:1.0
    securityContext:
      runAsUser: 1001
      runAsGroup: 1001
      runAsNonRoot: true
      allowPrivilegeEscalation: false
      capabilities:
        drop:
          - ALL
Enter fullscreen mode Exit fullscreen mode

Pro Tip: This is low-hanging fruit. Enforce this with a policy engine like OPA Gatekeeper or Kyverno. Don’t let a single container running as root into your production cluster. It’s just not worth the risk.

2. The Permanent Fix: Kernel-Level Guardrails with Seccomp or AppArmor

Dropping capabilities is great, but what if the attacker can leverage a kernel vulnerability directly? This is where you need to restrict the specific system calls (syscalls) a container is allowed to make. This is what tools like Seccomp and AppArmor are for. They act as a bouncer between your container and the host kernel, checking an allow/deny list before letting any request through.

Docker provides a default seccomp profile that’s pretty good, but you can and should create custom, more restrictive profiles for sensitive applications. A custom profile is just a JSON file that lists allowed syscalls.

For example, a profile that blocks the unshare syscall (often used in exploits) would look like this:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": [
        "SCMP_ARCH_X86_64",
        "SCMP_ARCH_X86",
        "SCMP_ARCH_AARCH64"
    ],
    "syscalls": [
        {
            "names": [
                "unshare"
            ],
            "action": "SCMP_ACT_KILL"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

This is more work, for sure. You’ll need to profile your application to figure out what syscalls it actually needs, but for mission-critical services on prod-db-01, it’s a non-negotiable layer of security.

3. The “Nuclear” Option: Swap the Kernel with gVisor or Kata Containers

Sometimes you have to run code you just don’t trust. That third-party marketing tool, a legacy application, or anything that requires root for some ancient, unknowable reason. In these cases, you don’t want that container’s processes even touching your host kernel.

This is where projects like Google’s gVisor or Kata Containers come in. They create a “sandbox” that intercepts all the syscalls from the container and handles them in a user-space kernel (gVisor) or a lightweight VM (Kata). The container thinks it’s talking to a Linux kernel, but it’s really talking to a highly secure proxy.

This provides an incredible level of isolation, far stronger than standard containers. The trade-off? Performance. You’re adding an extra layer of abstraction, which introduces latency. You wouldn’t run your high-performance database this way, but for that untrusted “santa-tracker-2024” container? It’s the perfect solution.

Solution Best For Effort Level Security Gain
Drop Privileges All workloads, baseline security Low Medium
Seccomp/AppArmor Sensitive apps, regulated data Medium High
gVisor/Kata Untrusted, multi-tenant, or legacy code High Very High

At the end of the day, there’s no silver bullet. But by layering these approaches, you can move from the default “hope it’s okay” posture to a deliberate, hardened security model. Don’t wait for that 3 AM page to find out which window you left open.


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)