Capabilities & Seccomp
Splitting root into pieces and filtering dangerous syscalls
Linux Capabilities
The Traditional Unix Model
Binary privilege model:
- UID 0 (root) — can do everything
- Non-root — heavily restricted
A process that needs to bind port 80 gets the same powers as one that manages kernel modules. All or nothing.
Root privilege split into ~40 discrete units:
- A web server gets
CAP_NET_BIND_SERVICEonly - A network tool gets
CAP_NET_ADMINonly - No process needs full root
Principle of least privilege — each process gets exactly the permissions it needs.
Capability Sets (per thread)
Each thread in Linux carries five capability sets. The kernel checks these at every privileged operation:
| Set | Purpose | Analogy |
|---|---|---|
| Effective | Capabilities the kernel actually checks — the "active" set | Keys currently on your keyring |
| Permitted | Upper bound of what can be in Effective — can be added/dropped | Keys you own (some may be in your pocket) |
| Inheritable | Capabilities preserved across execve() |
Keys you can pass to a child process |
| Bounding | Hard ceiling — cannot be regained once dropped | Keys that exist — removed ones are gone forever |
| Ambient | Automatically inherited by non-SUID child processes (Linux 4.3+) | Keys that follow you into any room |
Note
Note: The Effective set is what matters at runtime. A capability in Permitted but not in Effective has no power — it must be explicitly raised into Effective via prctl() or cap_set_proc().
Key Capabilities Reference
| Capability | What It Allows | Risk Level |
|---|---|---|
CAP_NET_ADMIN |
Configure networking — iptables rules, routes, interfaces, bridge settings | High |
CAP_NET_BIND_SERVICE |
Bind to privileged ports (< 1024) — needed for web servers on port 80/443 | Low |
CAP_SYS_ADMIN |
Mount filesystems, configure namespaces, many operations — the "new root" | Critical |
CAP_NET_RAW |
Use raw/packet sockets — needed for ping, network sniffing |
Medium |
CAP_CHOWN |
Change file ownership arbitrarily | Medium |
CAP_DAC_OVERRIDE |
Bypass read/write/execute permission checks on files | High |
CAP_FOWNER |
Bypass permission checks where file owner == process UID | Medium |
CAP_SETUID / CAP_SETGID |
Change process UID/GID — impersonate any user | High |
CAP_SYS_PTRACE |
Trace/debug any process — read memory, inject code | Critical |
CAP_MKNOD |
Create device files (block/character special files) | Medium |
CAP_KILL |
Send signals to any process (bypass permission checks) | Medium |
CAP_SYS_MODULE |
Load/unload kernel modules — arbitrary kernel code execution | Critical |
Warning
Warning: CAP_SYS_ADMIN is often called "the new root" because it gates so many operations (mount, BPF, namespaces, device-mapper, etc.). Granting it to a container effectively negates much of the isolation.
What Docker Drops by Default
Docker starts containers with ~14 capabilities out of the ~40 available. The rest are dropped. This is a critical part of container security.
| Capability | Status | What It Allows |
|---|---|---|
CAP_CHOWN |
Kept | Change file ownership |
CAP_DAC_OVERRIDE |
Kept | Bypass file read/write/execute checks |
CAP_FSETID |
Kept | Don't clear set-user/group-ID bits on file modification |
CAP_FOWNER |
Kept | Bypass ownership checks |
CAP_MKNOD |
Kept | Create special files |
CAP_NET_RAW |
Kept | Use raw sockets |
CAP_SETGID |
Kept | Set group ID |
CAP_SETUID |
Kept | Set user ID |
CAP_SETFCAP |
Kept | Set file capabilities |
CAP_SETPCAP |
Kept | Modify process capabilities |
CAP_NET_BIND_SERVICE |
Kept | Bind to ports < 1024 |
CAP_SYS_CHROOT |
Kept | Use chroot() |
CAP_KILL |
Kept | Send signals |
CAP_AUDIT_WRITE |
Kept | Write to kernel audit log |
CAP_SYS_ADMIN |
Dropped | Mount, namespaces, many things |
CAP_NET_ADMIN |
Dropped | Network configuration |
CAP_SYS_MODULE |
Dropped | Load kernel modules |
CAP_SYS_PTRACE |
Dropped | Trace/debug processes |
CAP_SYS_RAWIO |
Dropped | Raw I/O port access |
CAP_SYS_TIME |
Dropped | Set system clock |
CAP_SYS_BOOT |
Dropped | Reboot the system |
CAP_LINUX_IMMUTABLE |
Dropped | Set immutable file flags |
CAP_MAC_ADMIN |
Dropped | Override MAC (Smack) |
CAP_MAC_OVERRIDE |
Dropped | Override MAC policies |
Managing Capabilities in Docker
Adding a specific capability
# Add CAP_NET_ADMIN to allow iptables inside the container
docker run --cap-add NET_ADMIN nginxMinimal approach — drop ALL, add only what you need
# Best practice: start with nothing, add back selectively
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE nginxThis container can only bind to privileged ports. No file ownership changes, no raw sockets, no signal sending to other processes.
Warning
# NEVER do this in production
docker run --privileged nginx--privileged does ALL of the following:
- Grants ALL ~40 capabilities
- Gives access to all host devices (
/dev/*) - Disables seccomp profile
- Disables AppArmor/SELinux confinement
- Allows mounting filesystems
A privileged container has essentially the same power as root on the host. Container escape is trivial.
Kubernetes securityContext
Kubernetes exposes capability management through the securityContext field at both the Pod level and container level:
apiVersion: v1
kind: Pod
spec:
containers:
- name: web
image: nginx
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
readOnlyRootFilesystem: true
allowPrivilegeEscalation: falseapiVersion: v1
kind: Pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: nginxTip
Best practice: Pod-level settings apply to all containers. Container-level settings override pod-level. Always set runAsNonRoot: true and readOnlyRootFilesystem: true as defaults, then grant exceptions per-container only where needed.
| Field | Level | What It Does |
|---|---|---|
capabilities.drop / add |
Container | Linux capabilities to drop/add |
runAsNonRoot |
Pod or Container | Fail to start if process would run as UID 0 |
runAsUser / runAsGroup |
Pod or Container | Force specific UID/GID |
readOnlyRootFilesystem |
Container | Mount root filesystem as read-only |
allowPrivilegeEscalation |
Container | Prevent gaining more privileges than parent (no SUID binaries) |
seccompProfile |
Pod or Container | Apply a seccomp profile (see below) |
Seccomp-BPF
Seccomp (Secure Computing mode) limits which syscalls a process can make. It operates at a lower level than capabilities — even if you have the capability, seccomp can still block the syscall.
Introduced in Linux 2.6.12 (2005)
Only 4 syscalls allowed:
read()write()exit()sigreturn()
Way too restrictive for anything beyond pure computation. Used by cpushare sandboxing — basically nothing else.
Introduced in Linux 3.5 (2012)
A BPF program filters each syscall:
SCMP_ACT_ALLOW— permit the syscallSCMP_ACT_ERRNO— deny with error codeSCMP_ACT_TRAP— send SIGSYS signalSCMP_ACT_LOG— allow but logSCMP_ACT_KILL— kill the thread
Can filter on syscall number AND arguments. Flexible enough for real-world use.
How Seccomp-BPF Works
Process makes syscall
e.g., mount(), reboot()
BPF filter runs
Checks syscall number + args against rules
Decision
ALLOW / ERRNO / TRAP / KILL
Kernel action
Execute syscall or return error
Docker's Default Seccomp Profile
Docker applies a default seccomp profile that blocks ~60 syscalls out of 300+. This profile is carefully tuned to block dangerous operations while allowing normal application behavior.
Blocked Syscalls (examples)
reboot()— reboot the hostmount()/umount2()— mount filesystemskexec_load()— load a new kernelclock_settime()— change system timeswapon()/swapoff()— manage swapinit_module()— load kernel modulescreate_module()— create loadable moduleacct()— process accountingsettimeofday()— set time of daysethostname()— change hostnamekeyctl()— kernel keyring operations
Allowed Syscalls (categories)
- File I/O:
read,write,open,close,stat,fstat - Process:
fork,clone,execve,wait4,exit - Memory:
mmap,mprotect,brk,munmap - Network:
socket,connect,bind,listen,accept - Signals:
kill,rt_sigaction,rt_sigprocmask - Time:
clock_gettime,gettimeofday(read, not set) - IPC:
pipe,shmget,semget
Note
Capabilities vs. Seccomp — what's the difference? Capabilities control who can perform an operation (privilege checks). Seccomp controls what syscalls are available at all. Even root with CAP_SYS_ADMIN cannot call mount() if seccomp blocks it. They're complementary layers.
Custom Seccomp Profiles
Profile Structure (JSON)
Seccomp Profile — whitelist approach
{
"defaultAction": "SCMP_ACT_ERRNO", // deny everything by default
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_AARCH64"
],
"syscalls": [
{
"names": ["read", "write", "open", "close", "stat",
"fstat", "mmap", "mprotect", "brk",
"exit_group", "execve"],
"action": "SCMP_ACT_ALLOW" // whitelist these syscalls
},
{
"names": ["clone"],
"action": "SCMP_ACT_ALLOW",
"args": [ // filter on arguments too
{
"index": 0,
"value": 2114060288,
"op": "SCMP_CMP_MASKED_EQ" // only allow specific clone flags
}
]
}
]
}| Field | Purpose |
|---|---|
defaultAction |
What to do for any syscall not explicitly listed (SCMP_ACT_ERRNO = deny, SCMP_ACT_ALLOW = allow) |
architectures |
CPU architectures this profile applies to (syscall numbers differ per arch) |
syscalls[].names |
List of syscall names to match |
syscalls[].action |
What to do when matched: ALLOW, ERRNO, TRAP, KILL, LOG |
syscalls[].args |
Optional — filter on specific argument values (argument index, value, comparison operator) |
Applying Custom Profiles
Docker
# Use a custom seccomp profile
docker run \
--security-opt seccomp=profile.json \
nginx
# Disable seccomp entirely (dangerous!)
docker run \
--security-opt seccomp=unconfined \
nginxKubernetes
securityContext:
seccompProfile:
# Use the container runtime's default
type: RuntimeDefault
# OR use a custom profile from the node
securityContext:
seccompProfile:
type: Localhost
localhostProfile: profiles/custom.json
# File lives at:
# /var/lib/kubelet/seccomp/profiles/custom.jsonTip
Tip: Use strace or tools like oci-seccomp-bpf-hook to profile which syscalls your application actually uses, then build a minimal whitelist profile from that.
Terminal Examples
Inspecting capabilities on a host
$ 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=ep
Bounding set: 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
Ambient set:
Securebits: 00/0x0/1'b0 (no-hierarchical)
secure-noroot: no (unlocked)
secure-no-suid-fixup: no (unlocked)
secure-keep-caps: no (unlocked)
secure-no-ambient-raise: no (unlocked)
uid=0(root) euid=0(root)
gid=0(root)
Checking capabilities of a running container
$ docker run --rm alpine sh -c 'cat /proc/1/status | grep Cap'
CapInh: 0000000000000000
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
$ # Decode the hex bitmask
$ capsh --decode=00000000a80425fb
0x00000000a80425fb=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
Minimal container vs. privileged container
$ # Minimal container — only NET_BIND_SERVICE
$ docker run --rm --cap-drop ALL --cap-add NET_BIND_SERVICE alpine sh -c 'cat /proc/1/status | grep CapEff'
CapEff: 0000000000000400
$ capsh --decode=0000000000000400
0x0000000000000400=cap_net_bind_service
$ # Privileged container — ALL capabilities
$ docker run --rm --privileged alpine sh -c 'cat /proc/1/status | grep CapEff'
CapEff: 000001ffffffffff
$ capsh --decode=000001ffffffffff
0x000001ffffffffff=cap_chown,cap_dac_override,...,cap_sys_admin,...(ALL 41 capabilities)
Getting capabilities of a specific process
$ # Find the container's PID on the host
$ docker inspect --format '{{.State.Pid}}' my_container
12345
$ getpcaps 12345
12345: 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=ep
Testing seccomp — blocked syscall
$ # mount() is blocked by default seccomp profile
$ docker run --rm alpine mount -t tmpfs tmpfs /mnt
mount: permission denied (are you root?)
$ # With seccomp disabled, mount is allowed (if CAP_SYS_ADMIN is also added)
$ docker run --rm --security-opt seccomp=unconfined --cap-add SYS_ADMIN alpine mount -t tmpfs tmpfs /mnt
(succeeds — both seccomp AND capability gate removed)