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
/procandgetpcaps. - 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:
getcap -r /usr/bin /usr/sbin 2>/dev/nullActual output on an Ubuntu workstation:
/usr/bin/mtr-packet cap_net_raw=epA 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:
grep -E '^Cap' /proc/1/statusCapInh: 0000000000000000CapPrm: 000001ffffffffffCapEff: 000001ffffffffffCapBnd: 000001ffffffffffCapAmb: 0000000000000000Each line is a bit mask in hexadecimal, one bit per capability. Here is what each one denotes:
/proc line | Set | What it means |
|---|---|---|
CapPrm | Permitted | The ceiling of what the process can turn on. A capability missing from here is permanently out of reach. |
CapEff | Effective | What the kernel checks right now. It is the set actually used for permission checks. |
CapInh | Inheritable | What survives an execve(), but only combined with the capabilities of the executed file. Little used in practice. |
CapBnd | Bounding set | The absolute ceiling: a capability removed from here can never be regained, neither by the process nor by its descendants. |
CapAmb | Ambient set | The 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.
grep -E '^Cap' /proc/self/statusCapInh: 0000000000000000CapPrm: 0000000000000000CapEff: 0000000000000000CapBnd: 000001ffffffffffCapAmb: 0000000000000000Everything 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:
capsh --decode=0x00000000000020000x0000000000002000=cap_net_rawThe full mask 0x000001ffffffffff decodes to the kernel's 41 capabilities. Faster still, getpcaps queries a PID directly and shows the result in text notation:
getpcaps 11: =epThe =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:
sudo setcap cap_net_bind_service=+ep /usr/local/bin/mon-serviceThe +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:
getcap /usr/local/bin/mon-service/usr/local/bin/mon-service cap_net_bind_service=epTo remove the attribute, use -r:
sudo setcap -r /usr/local/bin/mon-serviceMind 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.
-
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 -
Grant the capability through systemd, without touching the binary.
AmbientCapabilities=grantsCAP_NET_BIND_SERVICEto the process at startup, even when it runs under an unprivileged account. Thesystemd.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". Nosetcapis 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. -
Write the systemd override with
sudo systemctl edit nginx.service.[Service]Type=simplePIDFile=User=nginx-svcGroup=nginx-svcRuntimeDirectory=nginxRuntimeDirectoryMode=0750AmbientCapabilities=CAP_NET_BIND_SERVICECapabilityBoundingSet=CAP_NET_BIND_SERVICEExecStartPre=ExecStart=ExecStart=/usr/sbin/nginx -g 'daemon off; master_process on;'ExecReload=ExecReload=/bin/kill -s HUP $MAINPIDThe empty
ExecStart=andExecReload=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, yourExecStartadds to the package's and systemd refuses the unit: the journal showsService has more than one ExecStart= setting, which is only allowed for Type=oneshot services. Refusing.The switch toType=simplefollows fromdaemon off;: Nginx stays in the foreground, it no longer forks, soType=forkingwould make systemd wait forever. An emptyPIDFile=removes the now useless reference. Finally, an emptyExecStartPre=removes the configuration testnginx -tthat the package unit runs before every start: this safety check goes with it, so runsudo nginx -tyourself after every change to/etc/nginx. -
Adapt
/etc/nginx/nginx.conf, which stays edited byroot.Two directives cause trouble under an unprivileged account. The PID file points by default to
/run/nginx.pid, in a/runowned byroot: the service account cannot write there. Redirect it to the directory thatRuntimeDirectory=creates for you, with the right permissions:pid /run/nginx/nginx.pid;Then comment out the
userdirective. It is only useful to arootmaster 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. -
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/nginxholds the request buffers (body,proxy) on Debian and Ubuntu: without write access, Nginx fails on the first large request./run/nginxis created automatically with the right owner byRuntimeDirectory=; you have nothing to do. -
Leave the configuration to
root, read-only for the service.Terminal window sudo chown -R root:root /etc/nginxsudo chmod -R go-w /etc/nginxIf 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.keysudo chmod 0640 /etc/ssl/private/mon-site.key -
Reload, start and check that the service listens without being
root.Terminal window sudo systemctl daemon-reloadsudo systemctl start nginxss -tulpn | grep ':80'getpcaps "$(systemctl show -p MainPID --value nginx)"3489: cap_net_bind_service=eipThe
getpcapsoutput must showcap_net_bind_serviceand nothing else. Thei(Inheritable) joins theepbecause an ambient capability must also be in the Inheritable set:/proc/<pid>/statusthen shows the same0x400mask on all five lines. If the output shows=ep, the service still runs asrootand the override was not taken into account. During the measurement,psshowed the master and its workers undernginx-svc, andcurl http://127.0.0.1/answered200. Counter-test: with theAmbientCapabilities=line commented out, the service fails withbind() 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.
| Capability | What it allows | Typical use |
|---|---|---|
CAP_NET_BIND_SERVICE | Bind a socket to a port below 1024. | Web server on port 80 or 443. |
CAP_NET_RAW | Open raw sockets and forge packets. | ping, mtr, tcpdump. |
CAP_CHOWN | Change a file's owner. | File system administration tools. |
CAP_SYS_TIME | Change the system clock. | Time synchronisation daemons. |
CAP_SYS_PTRACE | Attach a debugger to another process. | strace, gdb. Sensitive: gives access to other processes' memory. |
CAP_DAC_OVERRIDE | Ignore classic file permissions. | Dangerous: amounts to reading and writing any file. |
CAP_SYS_ADMIN | Mounting, 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.
| Symptom | Likely cause | Solution |
|---|---|---|
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 container | Docker 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 capability | The 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= setting | The 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 out | Type=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 denied | The 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 upgrade | The 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. GrantingCAP_NET_BIND_SERVICEto 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, anddaemon off;requires switching toType=simple. - Three capabilities must be treated as
root:CAP_SYS_ADMIN(catch-all),CAP_DAC_OVERRIDE(bypasses permissions) andCAP_SYS_PTRACE(reads other processes' memory). - Audit reflex:
getcap -r / 2>/dev/nulllists 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
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
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
A new full quiz with random questions
Work again only on the questions you missed