avatar🌌
TangentTangent Blog

技术博客 | Go & Kubernetes & Linux

技术博客 | Go & Kubernetes & Linux

Reading runc Source Code (Part 5): Container Security and Privileges in SPEC.md

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):

CapabilityEnabledBrief Description
CAP_CHOWN1Allow arbitrary changes to file UIDs and GIDs
CAP_NET_BIND_SERVICE1Allow binding to privileged ports (< 1024)
CAP_SETUID / CAP_SETGID1Allow making arbitrary manipulations of process UIDs/GIDs
CAP_KILL1Allow sending signals to processes
CAP_SYS_ADMIN0The ultimate Linux privilege; allows numerous high-risk system admin operations
CAP_NET_ADMIN0Allow network interface and routing table configuration
CAP_SYS_MODULE0Allow loading/unloading kernel modules
CAP_SYS_TIME0Allow manipulation of system clock
CapabilityEnabledBrief Description
----------------------------------
CAP_NET_RAW1Allow use of RAW and PACKET sockets (e.g., ping)
CAP_AUDIT_READ1Allow reading the audit log via a multicast netlink socket
CAP_AUDIT_WRITE1Allow writing records to kernel auditing log
CAP_DAC_OVERRIDE1Bypass file read, write, and execute permission checks
CAP_SETFCAP1Allow setting file capabilities
CAP_SETPCAP1Allow modifying process capabilities
CAP_MKNOD1Allow creation of special files using mknod
CAP_FOWNER1Bypass permission checks on operations that normally require the process UID to match the file UID
CAP_FSETID1Don’t clear set-user-ID and set-group-ID mode bits when a file is modified
CAP_SYS_CHROOT1Allow use of chroot()
CAP_NET_BROADCAST0Allow network broadcasting and multicasting
CAP_SYS_RAWIO0Allow raw I/O port operations
CAP_SYS_PACCT0Allow configuration of process accounting
CAP_SYS_NICE0Allow raising priority and setting scheduling policies
CAP_SYS_RESOURCE0Allow overriding resource limits and configure ext2 reserved space
CAP_SYS_TTY_CONFIG0Allow configuration of tty devices
CAP_AUDIT_CONTROL0Allow configuration of the audit system
CAP_MAC_OVERRIDE0Allow overriding Mandatory Access Control (MAC)
CAP_MAC_ADMIN0Allow MAC configuration or state changes
CAP_SYSLOG0Allow privileged syslog operations
CAP_DAC_READ_SEARCH0Bypass file read permission checks and directory read and execute permission checks
CAP_LINUX_IMMUTABLE0Allow modifying the immutable and append-only file attributes
CAP_IPC_LOCK0Allow locking of memory
CAP_IPC_OWNER0Bypass permission checks for operations on System V IPC objects
CAP_SYS_PTRACE0Allow tracing of arbitrary processes using ptrace
CAP_SYS_BOOT0Allow rebooting and loading a new kernel
CAP_LEASE0Allow establishing leases on arbitrary files
CAP_WAKE_ALARM0Allow triggering something that will wake up the system
CAP_BLOCK_SUSPEND0Allow 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:

c
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/** wklx and deny /sys/...: Forbids write operations to sensitive kernel parameters under /proc and /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 ​

bash
$ 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:00

Failure Reason: We are root, but we lack the CAP_SYS_TIME capability.

2. Try to Mount a File System ​

bash
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.

bash
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+eip

Comparing 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.

runc 源码解读(七):OCI Spec 核心数据结构解析 (上) —— 基础配置与 Process
runc 源码解读(五):从 SPEC.md 看容器安全与权限控制
由 Valaxy v1.2.2 驱动|主题-Yunv1.2.2