Skip to content
Menu

Virtualization8 min read

Capabilities & Seccomp

Splitting root into pieces and filtering dangerous syscalls

Linux Capabilities

The Traditional Unix Model

Traditional Unix

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.

Capabilities Model

Root privilege split into ~40 discrete units:

  • A web server gets CAP_NET_BIND_SERVICE only
  • A network tool gets CAP_NET_ADMIN only
  • 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

console
# Add CAP_NET_ADMIN to allow iptables inside the container
docker run --cap-add NET_ADMIN nginx

Minimal approach — drop ALL, add only what you need

console
# Best practice: start with nothing, add back selectively
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx

This container can only bind to privileged ports. No file ownership changes, no raw sockets, no signal sending to other processes.

Warning

DANGER: --privileged mode
console
# 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:

Container-level securityContext
yaml
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: web
    image: nginx
    securityContext:
      capabilities:
        drop: ["ALL"]
        add: ["NET_BIND_SERVICE"]
      readOnlyRootFilesystem: true
      allowPrivilegeEscalation: false
Pod-level securityContext
yaml
apiVersion: v1
kind: Pod
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 2000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: web
    image: nginx

Tip

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.

Original Seccomp (Mode 1)

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.

Seccomp-BPF (Mode 2)

Introduced in Linux 3.5 (2012)

A BPF program filters each syscall:

  • SCMP_ACT_ALLOW — permit the syscall
  • SCMP_ACT_ERRNO — deny with error code
  • SCMP_ACT_TRAP — send SIGSYS signal
  • SCMP_ACT_LOG — allow but log
  • SCMP_ACT_KILL — kill the thread

Can filter on syscall number AND arguments. Flexible enough for real-world use.

How Seccomp-BPF Works

  1. Process makes syscall

    e.g., mount(), reboot()

  2. BPF filter runs

    Checks syscall number + args against rules

  3. Decision

    ALLOW / ERRNO / TRAP / KILL

  4. 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 host
  • mount() / umount2() — mount filesystems
  • kexec_load() — load a new kernel
  • clock_settime() — change system time
  • swapon() / swapoff() — manage swap
  • init_module() — load kernel modules
  • create_module() — create loadable module
  • acct() — process accounting
  • settimeofday() — set time of day
  • sethostname() — change hostname
  • keyctl() — 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

bash
{
  "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

console
# Use a custom seccomp profile
docker run \
  --security-opt seccomp=profile.json \
  nginx

# Disable seccomp entirely (dangerous!)
docker run \
  --security-opt seccomp=unconfined \
  nginx

Kubernetes

console
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.json

Tip

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

console
console

    $ 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

console
console

    $ docker run --rm alpine sh -c 'cat /proc/1/status | grep Cap'

    CapInh: 0000000000000000

CapPrm: 00000000a80425fb

CapEff: 00000000a80425fb

CapBnd: 00000000a80425fb

CapAmb: 0000000000000000
  
console
console

    $ # 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

console
console

    $ # 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
  
console
console

    $ # 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

console
console

    $ # 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

console
console

    $ # 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)
  
Solidnines — solidnines.com