Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Understanding Linux capabilities

By updated

Improve this page

A capability is a slice of root's powers: instead of giving a process all of root, the kernel grants it exactly the privilege it needs. A web server that must listen on port 80 receives CAP_NET_BIND_SERVICE and nothing else. If it is compromised, the attacker inherits the right to open a privileged port, not the right to read /etc/shadow or to load a kernel module.

This page shows you how to read the capabilities of a running process, how to set one on an executable, and how to run Nginx on port 80 under an unprivileged account. It also lays the groundwork for namespaces and container security.

What you will learn

  • Read the five capability sets of a process through /proc and getpcaps.
  • Decode the hexadecimal masks shown by the kernel with capsh.
  • Set and remove a file capability on a binary with setcap.
  • Identify the dangerous capabilities never to grant lightly.
  • Configure an unprivileged systemd service that listens on a privileged port.

What a capability is

Historically, a Linux process was either root (everything allowed) or a normal user (nothing privileged). This binary model raised a simple problem: a web server only needs to open port 80, but to do so it had to start as root, hence with the right to do everything, including things it would never use.

Capabilities split this monolithic block into some forty independent privileges. On the machine used to write this page, the kernel exposes 41 of them, from CAP_CHOWN to CAP_CHECKPOINT_RESTORE: /proc/sys/kernel/cap_last_cap is 40 there, the number of the last one. Each one matches a precise check in the kernel: CAP_NET_BIND_SERVICE allows binding a socket to a port below 1024, CAP_SYS_TIME changing the system clock, CAP_SYS_MODULE loading a kernel module.

The benefit is directly measurable in attack surface. A process that only holds CAP_NET_BIND_SERVICE and gets compromised only gives the attacker one extra power: opening a low port. The same process started as root would offer all 41.

First use: see the capabilities already set

Before setting anything, look at what your system already uses. The getcap command reads the capabilities attached to an executable file, and the -r option recursively walks a directory tree:

Terminal window
getcap -r /usr/bin /usr/sbin 2>/dev/null

Actual output on an Ubuntu workstation:

/usr/bin/mtr-packet cap_net_raw=ep

A single binary carries a capability, and it makes sense: mtr needs to forge raw network packets, which CAP_NET_RAW allows. Without a capability, it would have to be made setuid root, which would give it far more than needed. It is exactly the use capabilities were designed for. The 2>/dev/null redirection hides permission errors on directories you cannot read; it hides no useful result.

Remember this reflex: getcap -r / 2>/dev/null on an unknown server tells you in one command which binaries are privileged without being setuid. It is a systematic audit checkpoint.

The five capability sets of a process

This is the point that stops most readers, because many documentation pages only present three. A Linux process actually carries five sets of capabilities, and you must know all five to read the output the kernel shows you.

Look at the raw output for the systemd process (PID 1), which runs as root:

Terminal window
grep -E '^Cap' /proc/1/status
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000

Each line is a bit mask in hexadecimal, one bit per capability. Here is what each one denotes:

/proc lineSetWhat it means
CapPrmPermittedThe ceiling of what the process can turn on. A capability missing from here is permanently out of reach.
CapEffEffectiveWhat the kernel checks right now. It is the set actually used for permission checks.
CapInhInheritableWhat survives an execve(), but only combined with the capabilities of the executed file. Little used in practice.
CapBndBounding setThe absolute ceiling: a capability removed from here can never be regained, neither by the process nor by its descendants.
CapAmbAmbient setThe capabilities preserved across the execution of an unprivileged program, without a file capability on the binary.

Reading becomes immediate. systemd has everything in Permitted and Effective: it is root, it can do anything. Its bounding set is full, so it can pass any capability on to its children. Its ambient set is empty, which is normal for an already privileged process.

Compare with an ordinary user shell, started outside any container: Docker, for its part, lowers the bounding set of its processes to a reduced list.

Terminal window
grep -E '^Cap' /proc/self/status
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000

Everything is zero except the bounding set. It is the signature of an unprivileged process: it holds no capability, but the ceiling stays high, which means a setuid binary or one carrying a file capability could still grant it some. Hardening consists precisely in lowering that bounding set.

Decode the masks with capsh

Hexadecimal masks are unreadable as they are. The capsh tool translates them into capability names, which is essential as soon as you audit a /proc output:

Terminal window
capsh --decode=0x0000000000002000
0x0000000000002000=cap_net_raw

The full mask 0x000001ffffffffff decodes to the kernel's 41 capabilities. Faster still, getpcaps queries a PID directly and shows the result in text notation:

Terminal window
getpcaps 1
1: =ep

The =ep notation reads: all capabilities (=) are present in Effective (e) and in Permitted (p). It is the compact form of what /proc/1/status showed in hexadecimal.

Set a capability on an executable

A file capability is a capability stored in an extended attribute of the file (security.capability), not in the process. When the kernel executes this file, it takes it into account to build the new process's sets. That is what lets mtr-packet obtain CAP_NET_RAW without being setuid.

The setcap command sets this capability:

Terminal window
sudo setcap cap_net_bind_service=+ep /usr/local/bin/mon-service

The +ep suffix combines two flags. The p (permitted) puts the capability in the process's Permitted set at execve() time. The e (effective) is a single bit that asks the kernel to raise this capability in Effective straight away, without the program having to claim it itself. Without the e, a binary unaware of capabilities would start with the capability available but inactive.

Always check after setting it, with getcap:

Terminal window
getcap /usr/local/bin/mon-service
/usr/local/bin/mon-service cap_net_bind_service=ep

To remove the attribute, use -r:

Terminal window
sudo setcap -r /usr/local/bin/mon-service

Mind the subtlety: according to setcap(8), setting an empty set is not equivalent to removing the attribute. A file with an empty attribute remains a "capability" file from the kernel's point of view: after a setcap '=' file, getcap still shows the file followed by =, whereas it no longer shows it at all after a -r. Use -r for a clean removal.

Two traps that cost a lot

The first trap is the most serious, and it is often reproduced as is in tutorials: a capability set on an interpreter benefits everyone.

The second trap is silent, and that is what makes it fearsome. File capabilities live in an extended attribute of the inode. When apt or dnf updates the package, it replaces the file with a new inode taken from the archive, and that archive does not contain your attribute. Your capability is lost without the slightest warning. The service fails again at the next restart, often weeks later, with a Permission denied on the bind that nobody links to the update.

It is the decisive argument for AmbientCapabilities=: the declaration lives in the systemd unit, a configuration file that package updates do not replace. It is replayed at every startup, hence immune to this trap.

Real case: Nginx on port 80 without root

Goal: make Nginx listen on port 80 while running under an unprivileged service account, instead of the classic setup where the master runs as root.

Two details of the Nginx unit packaged by Debian and Ubuntu govern the whole operation. It is declared Type=forking with a PIDFile=/run/nginx.pid, and its ExecStart is /usr/sbin/nginx -g 'daemon on; master_process on;'. In other words, Nginx detaches into the background and systemd follows it through its PID file. As soon as you switch to daemon off; to keep it in the foreground, that contract is broken and Type= must change.

  1. Create the dedicated service account, with no shell and no home directory.

    Terminal window
    sudo useradd --system --no-create-home --shell /usr/sbin/nologin nginx-svc
  2. Grant the capability through systemd, without touching the binary.

    AmbientCapabilities= grants CAP_NET_BIND_SERVICE to the process at startup, even when it runs under an unprivileged account. The systemd.exec(5) manual is clear about its use: "Ambient capability sets are useful if you want to execute a process as a non-privileged user but still want to give it some capabilities". No setcap is needed, so nothing to lose during updates of the Nginx package.

    CapabilityBoundingSet= also lowers the service's ceiling: even compromised, it can never claim anything other than this capability.

  3. Write the systemd override with sudo systemctl edit nginx.service.

    [Service]
    Type=simple
    PIDFile=
    User=nginx-svc
    Group=nginx-svc
    RuntimeDirectory=nginx
    RuntimeDirectoryMode=0750
    AmbientCapabilities=CAP_NET_BIND_SERVICE
    CapabilityBoundingSet=CAP_NET_BIND_SERVICE
    ExecStartPre=
    ExecStart=
    ExecStart=/usr/sbin/nginx -g 'daemon off; master_process on;'
    ExecReload=
    ExecReload=/bin/kill -s HUP $MAINPID

    The empty ExecStart= and ExecReload= lines are not a typo: they are essential. systemd.service(5) requires that "Unless Type= is oneshot, exactly one command must be given". Without the empty line that resets the list, your ExecStart adds to the package's and systemd refuses the unit: the journal shows Service has more than one ExecStart= setting, which is only allowed for Type=oneshot services. Refusing. The switch to Type=simple follows from daemon off;: Nginx stays in the foreground, it no longer forks, so Type=forking would make systemd wait forever. An empty PIDFile= removes the now useless reference. Finally, an empty ExecStartPre= removes the configuration test nginx -t that the package unit runs before every start: this safety check goes with it, so run sudo nginx -t yourself after every change to /etc/nginx.

  4. Adapt /etc/nginx/nginx.conf, which stays edited by root.

    Two directives cause trouble under an unprivileged account. The PID file points by default to /run/nginx.pid, in a /run owned by root: the service account cannot write there. Redirect it to the directory that RuntimeDirectory= creates for you, with the right permissions:

    pid /run/nginx/nginx.pid;

    Then comment out the user directive. It is only useful to a root master that wants to run its workers under another account. An already unprivileged master cannot change identity, and Nginx would print a warning at every startup.

  5. Give the service account only what it must write, that is logs and temporary directories.

    Terminal window
    sudo chown -R nginx-svc:nginx-svc /var/log/nginx /var/lib/nginx

    /var/lib/nginx holds the request buffers (body, proxy) on Debian and Ubuntu: without write access, Nginx fails on the first large request. /run/nginx is created automatically with the right owner by RuntimeDirectory=; you have nothing to do.

  6. Leave the configuration to root, read-only for the service.

    Terminal window
    sudo chown -R root:root /etc/nginx
    sudo chmod -R go-w /etc/nginx

    If you serve TLS, the private key is the exception: the unprivileged master must be able to read it. Give it to the service's group, and to it alone, rather than making it readable by everyone.

    Terminal window
    sudo chown root:nginx-svc /etc/ssl/private/mon-site.key
    sudo chmod 0640 /etc/ssl/private/mon-site.key
  7. Reload, start and check that the service listens without being root.

    Terminal window
    sudo systemctl daemon-reload
    sudo systemctl start nginx
    ss -tulpn | grep ':80'
    getpcaps "$(systemctl show -p MainPID --value nginx)"
    3489: cap_net_bind_service=eip

    The getpcaps output must show cap_net_bind_service and nothing else. The i (Inheritable) joins the ep because an ambient capability must also be in the Inheritable set: /proc/<pid>/status then shows the same 0x400 mask on all five lines. If the output shows =ep, the service still runs as root and the override was not taken into account. During the measurement, ps showed the master and its workers under nginx-svc, and curl http://127.0.0.1/ answered 200. Counter-test: with the AmbientCapabilities= line commented out, the service fails with bind() to 0.0.0.0:80 failed (13: Permission denied).

A classic mistake deserves to be ruled out explicitly: do not run chmod 750 /usr/sbin/nginx. The binary belongs to root:root; in mode 750, only root and the root group keep the right to execute it. Your nginx-svc account is neither: it loses the execute permission and the service no longer starts at all, with Failed to execute /usr/sbin/nginx: Permission denied and the 203/EXEC status in the journal. This apparent hardening breaks exactly what you were trying to build. Leave the binary with the package's 0755 permissions and control access through the service account and the bounding set.

The most common capabilities

Some forty capabilities exist, but a handful keeps coming up in administration and container security. The table below groups them by use, with a marker for the most sensitive ones.

CapabilityWhat it allowsTypical use
CAP_NET_BIND_SERVICEBind a socket to a port below 1024.Web server on port 80 or 443.
CAP_NET_RAWOpen raw sockets and forge packets.ping, mtr, tcpdump.
CAP_CHOWNChange a file's owner.File system administration tools.
CAP_SYS_TIMEChange the system clock.Time synchronisation daemons.
CAP_SYS_PTRACEAttach a debugger to another process.strace, gdb. Sensitive: gives access to other processes' memory.
CAP_DAC_OVERRIDEIgnore classic file permissions.Dangerous: amounts to reading and writing any file.
CAP_SYS_ADMINMounting, namespaces, and dozens of operations.To avoid: a catch-all so broad it is nearly worth root.

Three of them deserve particular vigilance. Over kernel versions, CAP_SYS_ADMIN has become a catch-all covering so many operations that granting it practically means giving root; that is why it regularly causes container escapes. CAP_DAC_OVERRIDE short-circuits the whole file permission model, so reading /etc/shadow becomes trivial. CAP_SYS_PTRACE lets you attach to another process and read its memory, secrets included. None of the three belongs in an ordinary application service.

Troubleshooting

The symptoms below cover most of what you will meet when setting your first capabilities. What most of them have in common: the kernel fails silently or with a generic message, without ever naming the missing capability.

SymptomLikely causeSolution
bind() to 0.0.0.0:80 failed (13: Permission denied)The capability was not granted, or it was lost during a package update.Check with getpcaps <PID>; prefer AmbientCapabilities= in the unit over setcap.
An account with no capability opens port 80 in a containerDocker sets net.ipv4.ip_unprivileged_port_start to 0 in its containers.Test on a VM, or set the value back to 1024 for an honest measurement.
getcap no longer returns anything on a binary that had its capabilityThe file was replaced (package update, copy without extended attributes), or its file system does not keep them.Check the mount with findmnt; file capabilities require extended attribute support.
systemd: Service has more than one ExecStart= settingThe override redefines ExecStart without resetting the list first.Add an empty ExecStart= line before the new definition.
The service stays stuck in activating then times outType=forking kept while the program runs in the foreground with daemon off;.Switch to Type=simple and empty PIDFile=.
status=203/EXEC and Failed to execute … Permission deniedThe binary lost the execute permission for the service account (chmod 750).Give the binary back the package's 0755 permissions.
The capability disappears after an apt upgradeThe package replaced the binary, hence the extended attribute that carried the file capability.Switch to AmbientCapabilities= in the systemd unit, which survives updates.

The origins of capabilities

Capabilities appeared with kernel 2.2, in 1999, to answer the "all or nothing" problem of the root model. The implementation draws on the POSIX 1003.1e draft, a standardisation project that was withdrawn before completion. That is why the behaviour of capabilities is portable to no other Unix, and why terminology varies from one documentation to the next.

The model was enriched afterwards. The bounding set became per-process from kernel 2.6.25, which made it possible to durably confine a tree of processes. The ambient set, introduced in kernel 4.3 in 2015, filled the last gap: granting a capability to an unprivileged service without a file capability on the binary. It is the building block that systemd's AmbientCapabilities= and the container privilege model rely on today.

Key points

  • A capability is a slice of root. Granting CAP_NET_BIND_SERVICE to a web server gives it port 80 without giving it /etc/shadow.
  • A process carries five sets, not three: Permitted (the ceiling it can turn on), Effective (what the kernel checks now), Inheritable (rarely useful), Bounding set (the absolute, irreversible ceiling) and Ambient set (what survives the execution of an unprivileged binary).
  • Capabilities follow a withdrawn POSIX draft, not a standard. No portability to another Unix is guaranteed.
  • Never set a capability on an interpreter (python3, perl, node): it would benefit any code run by any local user, who could then impersonate a service on a privileged port.
  • File capabilities are lost at every package update that replaces the binary. The failure happens weeks later, without warning.
  • Prefer AmbientCapabilities= in the systemd unit: the capability is granted to the service alone, replayed at every startup, and immune to package updates.
  • In a systemd override, an empty ExecStart= line is mandatory before redefining the command, and daemon off; requires switching to Type=simple.
  • Three capabilities must be treated as root: CAP_SYS_ADMIN (catch-all), CAP_DAC_OVERRIDE (bypasses permissions) and CAP_SYS_PTRACE (reads other processes' memory).
  • Audit reflex: getcap -r / 2>/dev/null lists the privileged binaries, getpcaps <PID> gives the capabilities of a live process.

Knowledge check

Check that the essentials of this guide are acquired. The questions only cover what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn