Linux Capabilities is a security mechanism that breaks the traditional UNIX “superuser gets everything” model into a set of fine-grained, independent privileges. Instead of giving a process all root powers, you give it only the specific powers it actually needs. This drastically reduces the attack surface if a process is ever compromised.
Think of it like a keyring. In the old model, root had one master key that opened every door in the building. Capabilities say: give the janitor only the key to the janitor’s closet, the office cleaner only the key to offices, and so on β nobody carries the full master key unless absolutely necessary.
In classic UNIX (and Linux), there are exactly two kinds of processes when it comes to privilege:
| Process Type | Effective UID | Privilege Level |
|---|---|---|
| Privileged Process | 0 (root) | Bypasses ALL kernel permission checks |
| Unprivileged Process | Non-zero | Subject to ALL permission checks |
This is an all-or-nothing design. If you want a program to change the system clock β a single privileged operation β you must run it as root. But running it as root also gives it the ability to:
- Read and write any file on the system
- Send signals to any process
- Load or unload kernel modules
- Bypass all file permission checks
- Reboot the system
- β¦and hundreds of other privileged operations
This is the principle of least privilege violation. A program that only needs to adjust the clock should not have the ability to delete files or kill arbitrary processes. If such a program has a bug or is exploited by a malicious user, the damage it can cause is catastrophic.
set-user-ID-root programs combined with manually dropping privileges (calling seteuid() to switch away from UID 0 temporarily). But this required careful, error-prone programming, and the process still retained root UID in the saved set-user-ID, meaning it could reacquire full root powers at any time.The Linux capabilities scheme solves this by splitting the single “root superpower” into around 40 distinct units called capabilities. Each capability covers a specific category of privileged operations.
For example:
| Capability | What it allows |
|---|---|
CAP_SYS_TIME |
Change the system clock |
CAP_NET_BIND_SERVICE |
Bind to ports below 1024 |
CAP_KILL |
Send signals to any process |
CAP_SYS_BOOT |
Reboot the system |
CAP_DAC_OVERRIDE |
Bypass file permission checks |
A process can be granted only CAP_SYS_TIME to change the clock, without getting any other privileges. If the process is compromised, the attacker still cannot read protected files or kill system processes, because the process never had those capabilities.
Most of the time, capabilities are invisible to normal application developers. This is because when a process assumes an effective user ID of 0 (root), the kernel automatically grants it the full set of all capabilities. So existing root-based programs continue to work exactly as before β capabilities are fully backward-compatible.
The Linux capabilities implementation is based on the POSIX 1003.1e draft standard. Although this standardization effort was never completed (it was abandoned in the late 1990s), Linux and other UNIX systems (Sun Solaris, SGI Trusted IRIX, TrustedBSD for FreeBSD) all implemented capabilities based on the draft. Some capabilities in Linux are directly from the draft; many others are Linux-specific extensions.
| Scenario | Old Approach | Capabilities Approach |
|---|---|---|
| Web server on port 80 | Run as root (dangerous) | Grant only CAP_NET_BIND_SERVICE |
| NTP time sync daemon | Set-user-ID-root binary | Grant only CAP_SYS_TIME |
| Packet capture tool | Must be root | Grant only CAP_NET_RAW |
In each of these cases, the capabilities approach means that even if the service is completely taken over by an attacker, they gain only the specific capability the process had β not full root access over the entire system.
Before manipulating capabilities, you first need to know how to view them. Linux exposes each process’s three capability sets via the /proc/PID/status file. The following program reads and prints the raw capability bitmasks for the calling process, then shows how to interpret them using the libcap library’s human-readable API.
The three fields of interest in /proc/self/status are:
CapInhβ Inheritable capability set (hex bitmask)CapPrmβ Permitted capability set (hex bitmask)CapEffβ Effective capability set (hex bitmask)
/*
* show_caps.c
*
* Demonstrates two ways to inspect process capabilities:
* 1. Directly reading /proc/self/status (raw hex bitmasks)
* 2. Using the libcap API for human-readable output
*
* Compile:
* gcc -o show_caps show_caps.c -lcap
*
* Run as normal user:
* ./show_caps
*
* Run as root:
* sudo ./show_caps
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/capability.h> /* libcap header */
#include <unistd.h>
/* ---------------------------------------------------------------
* Method 1: Read raw capability bitmasks from /proc/self/status
* This works without libcap β just file I/O.
* --------------------------------------------------------------- */
static void show_raw_caps(void)
{
FILE *fp;
char line[256];
printf("=== Raw capability bitmasks from /proc/self/status ===\n");
fp = fopen("/proc/self/status", "r");
if (fp == NULL) {
perror("fopen /proc/self/status");
return;
}
while (fgets(line, sizeof(line), fp) != NULL) {
/*
* We are looking for lines that start with:
* CapInh: (Inheritable)
* CapPrm: (Permitted)
* CapEff: (Effective)
* CapBnd: (Bounding set)
*/
if (strncmp(line, "CapInh:", 7) == 0 ||
strncmp(line, "CapPrm:", 7) == 0 ||
strncmp(line, "CapEff:", 7) == 0 ||
strncmp(line, "CapBnd:", 7) == 0)
{
/*
* Each line looks like:
* CapPrm: 0000000000000000
* A value of 0000000000000000 means no capabilities.
* A value of 0000003fffffffff means all capabilities set.
*/
printf(" %s", line); /* line already has '\n' */
}
}
fclose(fp);
printf("\n");
}
/* ---------------------------------------------------------------
* Method 2: Use libcap to get human-readable capability strings.
* cap_get_proc() β fetches the current process cap sets
* cap_to_text() β converts them to a readable string
* cap_free() β frees memory allocated by libcap
* --------------------------------------------------------------- */
static void show_caps_libcap(void)
{
cap_t caps;
char *text;
printf("=== Human-readable capabilities via libcap ===\n");
/*
* cap_get_proc() returns a cap_t structure representing
* all three capability sets (permitted, effective, inheritable)
* of the calling process.
*
* Returns NULL on error.
*/
caps = cap_get_proc();
if (caps == NULL) {
perror("cap_get_proc");
return;
}
/*
* cap_to_text() converts the cap_t structure into a
* human-readable string. The second argument (if non-NULL)
* is set to the length of the returned string.
*
* Example outputs:
* Normal user: "" (empty β no capabilities)
* Root: "=ep" (all capabilities, permitted + effective)
*/
text = cap_to_text(caps, NULL);
if (text == NULL) {
perror("cap_to_text");
cap_free(caps);
return;
}
printf(" Capability sets: [%s]\n\n", text);
if (strlen(text) == 0) {
printf(" (No capabilities β this is a normal unprivileged process)\n\n");
}
/*
* IMPORTANT: Always free libcap-allocated memory with cap_free(),
* not with the regular free(). This applies to both cap_t handles
* and strings returned by cap_to_text().
*/
cap_free(text);
cap_free(caps);
}
int main(void)
{
printf("Process capabilities viewer\n");
printf("PID: %d UID: %d EUID: %d\n\n",
(int)getpid(), (int)getuid(), (int)geteuid());
/* Method 1 β raw /proc parsing */
show_raw_caps();
/* Method 2 β libcap API */
show_caps_libcap();
printf("Tip: Run this program with 'sudo' to see all capabilities set.\n");
return 0;
}
Process capabilities viewer PID: 4821 UID: 1000 EUID: 1000 === Raw capability bitmasks from /proc/self/status === CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: 000001ffffffffff === Human-readable capabilities via libcap === Capability sets: [] (No capabilities β this is a normal unprivileged process)
Expected output (root):
CapPrm: 000001ffffffffff CapEff: 000001ffffffffff Capability sets: [=ep]
The traditional UNIX model is binary β a process either has UID 0 and bypasses all kernel permission checks (superuser), or it has a non-zero UID and is fully subject to them. This coarse granularity means that granting any privileged operation to a process automatically grants it all privileged operations. There is no way to say “this process can change the clock but cannot read arbitrary files.”
Linux Capabilities divides the monolithic root privilege into approximately 40 independent units. Each unit (capability) governs a specific set of privileged operations. A process can be granted only the capabilities it actually needs, so even if it is compromised, an attacker gains only those specific privileges β not full system access.
When a process sets its effective user ID to 0 (root), the kernel automatically grants it the full set of all capabilities. This preserves complete backward compatibility β existing programs that run as root behave exactly as before, without any need to be updated to understand capabilities.
You can inspect /proc/PID/status and look at the fields CapInh (inheritable), CapPrm (permitted), and CapEff (effective). These are shown as 64-bit hexadecimal bitmasks. For a human-readable format, use the getpcap tool from the libcap package, or call cap_get_proc() + cap_to_text() in your C code.
The Linux capabilities implementation is based on the POSIX 1003.1e draft standard. Although this standardization effort was abandoned in the late 1990s before completion, Linux and several other UNIX systems (Solaris, Trusted IRIX, FreeBSD’s TrustedBSD project) all implemented capabilities based on this draft. Some capabilities in Linux follow the draft; many others are Linux-specific extensions.
A web server like Apache or Nginx must bind to port 80 (a privileged port below 1024). Traditionally this required running the process as root. Using capabilities, you can assign only CAP_NET_BIND_SERVICE to the binary. The server can bind to port 80, but if it is compromised, the attacker cannot read arbitrary files, cannot kill other processes, and cannot load kernel modules β because the process never had those capabilities.
Next: The Linux Capabilities Table β Part 3: Process & File Capability Sets
