Introduction
In previous articles, we explored Namespaces (view isolation), Filesystem, and Cgroups / Intel RDT (resource limits) from SPEC.md. Wrapped in these layers, we seem to have a complete container.
However, a critical question remains: If the application inside the container runs as root, can a hacker control the host machine if the application is compromised?
The answer is: usually, no. This is where container security mechanisms come into play. In runc’s SPEC.md, there is a dedicated ### Security section defining the Defense in Depth architecture of containers. This post will break down these defense lines one by one.
Defense Line 1: Linux Capabilities (Fine-grained Privileges)
In traditional Linux systems, privileges are binary: either you are a normal user (restricted), or you are root (UID=0, full power). To achieve finer-grained control, Linux introduced the Capabilities mechanism, which slices the omnipotent root privilege into dozens of distinct, manageable privileges.
SPEC.md provides a default list of Capabilities for a container:
The standard set of Linux capabilities that are set in a container provide a good default for security and flexibility for the applications.
The specification explicitly defines which capabilities are enabled by default (1) and which are disabled (0):
| Capability | Enabled | Brief Description |
|---|---|---|
CAP_CHOWN | 1 | Allow arbitrary changes to file UIDs and GIDs |
CAP_NET_BIND_SERVICE | 1 | Allow binding to privileged ports (< 1024) |
CAP_SETUID / CAP_SETGID | 1 | Allow making arbitrary manipulations of process UIDs/GIDs |
CAP_KILL | 1 | Allow sending signals to processes |
CAP_SYS_ADMIN | 0 | The ultimate Linux privilege; allows numerous high-risk system admin operations |
CAP_NET_ADMIN | 0 | Allow network interface and routing table configuration |
CAP_SYS_MODULE | 0 | Allow loading/unloading kernel modules |
CAP_SYS_TIME | 0 | Allow manipulation of system clock |
| Capability | Enabled | Brief Description |
| ---------- | ------- | ----------------- |
CAP_NET_RAW | 1 | Allow use of RAW and PACKET sockets (e.g., ping) |
CAP_AUDIT_READ | 1 | Allow reading the audit log via a multicast netlink socket |
CAP_AUDIT_WRITE | 1 | Allow writing records to kernel auditing log |
CAP_DAC_OVERRIDE | 1 | Bypass file read, write, and execute permission checks |
CAP_SETFCAP | 1 | Allow setting file capabilities |
CAP_SETPCAP | 1 | Allow modifying process capabilities |
CAP_MKNOD | 1 | Allow creation of special files using mknod |
CAP_FOWNER | 1 | Bypass permission checks on operations that normally require the process UID to match the file UID |
CAP_FSETID | 1 | Don’t clear set-user-ID and set-group-ID mode bits when a file is modified |
CAP_SYS_CHROOT | 1 | Allow use of chroot() |
CAP_NET_BROADCAST | 0 | Allow network broadcasting and multicasting |
CAP_SYS_RAWIO | 0 | Allow raw I/O port operations |
CAP_SYS_PACCT | 0 | Allow configuration of process accounting |
CAP_SYS_NICE | 0 | Allow raising priority and setting scheduling policies |
CAP_SYS_RESOURCE | 0 | Allow overriding resource limits and configure ext2 reserved space |
CAP_SYS_TTY_CONFIG | 0 | Allow configuration of tty devices |
CAP_AUDIT_CONTROL | 0 | Allow configuration of the audit system |
CAP_MAC_OVERRIDE | 0 | Allow overriding Mandatory Access Control (MAC) |
CAP_MAC_ADMIN | 0 | Allow MAC configuration or state changes |
CAP_SYSLOG | 0 | Allow privileged syslog operations |
CAP_DAC_READ_SEARCH | 0 | Bypass file read permission checks and directory read and execute permission checks |
CAP_LINUX_IMMUTABLE | 0 | Allow modifying the immutable and append-only file attributes |
CAP_IPC_LOCK | 0 | Allow locking of memory |
CAP_IPC_OWNER | 0 | Bypass permission checks for operations on System V IPC objects |
CAP_SYS_PTRACE | 0 | Allow tracing of arbitrary processes using ptrace |
CAP_SYS_BOOT | 0 | Allow rebooting and loading a new kernel |
CAP_LEASE | 0 | Allow establishing leases on arbitrary files |
CAP_WAKE_ALARM | 0 | Allow triggering something that will wake up the system |
CAP_BLOCK_SUSPEND | 0 | Allow blocking system suspends |
Analysis: You will notice that the capabilities enabled by runc are the minimum privileges required for a normal application (like Nginx or a database) to function properly. Those that are disabled are operations that could directly impact the host. For instance, without CAP_SYS_ADMIN, you cannot mount file systems inside the container; without CAP_SYS_TIME, you cannot change the host’s time (time is global until newer kernels supported Time Namespace); without CAP_SYS_MODULE, hackers cannot inject malicious kernel rootkits.
This is why even if you are root inside a container, you are actually running a "castrated" root.
Defense Line 2: AppArmor and SELinux (Mandatory Access Control)
Even stripped of dangerous Capabilities, a container root still has the ability to read or modify a vast number of files within the system. To further reduce the attack surface, SPEC.md mentions MAC (Mandatory Access Control):
Additional security layers like apparmor and selinux can be used with the containers. A container should support setting an apparmor profile or selinux process and mount labels if provided in the configuration.
The document even provides a standard AppArmor Profile example. Though short, it is lethal:
profile <profile_name> flags=(attach_disconnected,mediate_deleted) {
...
deny @{PROC}/sys/fs/** wklx,
deny @{PROC}/sysrq-trigger rwklx,
deny @{PROC}/mem rwklx,
...
deny mount,
...
deny /sys/kernel/security/** rwklx,
}The core purpose of this rule set is singular: protect the host’s kernel state from being tampered with.
deny mount: Blocks file system mounting again from the MAC level.deny @{PROC}/sys/fs/** wklxanddeny /sys/...: Forbids write operations to sensitive kernel parameters under/procand/sys. Even if a process inside the container somehow escalated its Capabilities, AppArmor would still intercept the malicious write operation at the last moment (Permission denied).
Defense Line 3: Seccomp (System Call Filtering)
If Capabilities filter "business actions" and AppArmor filters "file paths", then Seccomp (Secure Computing Mode) is the hardcore filter for Linux kernel "System Calls (Syscalls)".
seccomp can be used to apply filters to the system calls used in a container… The primary use-case is to provide an explicit allow-list of syscalls for a container, to reduce the kernel API attack surface exposed to the container.
Containers inherently share the same kernel with the host. If a 0-day vulnerability exists in a specific kernel syscall, a program inside the container could trigger it to escalate privileges and break out.
Seccomp intercepts syscalls using BPF (Berkeley Packet Filter) technology. runc makes a very smart decision here:
libcontainer does not provide a default filter, but higher-level runtimes tend to define their own filters for use with runc…
runc/libcontainer itself does not provide a default Seccomp filter; instead, it delegates the definition to higher-level runtimes (like Docker, containerd). In practice, Docker’s default Seccomp profile blocks over 40 unsafe syscalls out of the 300+ available in Linux (e.g., kexec_load, open_by_handle_at). Historically, Seccomp has successfully defended against numerous kernel privilege escalation 0-day vulnerabilities.
Hands-on Demo: Probing the "Castrated" Root
We can intuitively feel this defense-in-depth system through a simple experiment. Let’s start a standard container and try to do something "unauthorized" inside it.
1. Try to Modify System Time
$ docker run --rm -it ubuntu bash
root@container_id:/# id
uid=0(root) gid=0(root) groups=0(root)
root@container_id:/# date -s "2030-01-01"
date: 2026-03-19T10:00:00+08:00Failure Reason: We are root, but we lack the CAP_SYS_TIME capability.
2. Try to Mount a File System
root@container_id:/# mount -t tmpfs tmpfs /mnt
mount: /mnt: permission denied.Failure Reason: Lacks the CAP_SYS_ADMIN capability, and is simultaneously blocked by AppArmor’s deny mount rule.
3. Inspect the Current Capabilities
We can use the capsh tool to clearly see exactly what capabilities we have been granted.
root@container_id:/# apt-get update && apt-get install -y libcap2-bin
root@container_id:/# capsh --print
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eipComparing the printed Current list with the table in SPEC.md earlier, you will find they match astonishingly well! The high-risk privileged capabilities have been thoroughly stripped away.
Conclusion
From the "deception" of Namespaces (making you think you have a whole machine), to the "stinginess" of Cgroups (limiting your resources), and finally the "defense" of Security (Capabilities + AppArmor + Seccomp) (confiscating all dangerous weapons), runc squeezes Linux kernel features to the extreme to assemble a lightweight yet powerful sandbox system.
With this, our source code and specification reading of runc’s SPEC.md comes to a close. Deeply understanding these underlying specifications provides immense guidance for troubleshooting container permission errors (Permission denied), writing highly secure Dockerfiles, and architecting cloud-native security defenses in our daily work.