---
title: Capabilities
source: 'https://academia.sh/en/courses/kernel-interfaces/capabilities'
course: 'Kernel Interfaces and Isolation'
language: en
updated: '2026-08-17T18:09:57+00:00'
license: 'CC BY-SA 4.0'
---

# Capabilities

Superuser privilege is divided into separately grantable pieces; capability dropping closes only one of two facts and file permissions stay open, and in the seven-mechanism bundle the visible fact count drops to 11.

The previous two lessons narrowed **what a process sees** and **how much it consumes**.
The third question belongs to another axis: which **privileged operation** a process can
request from the kernel. A process that sees its own process table, with its quota
trimmed, can still attempt to open a mount point, change network configuration, or touch
another user's file. Whether these requests are granted has nothing to do with visibility
or consumption.

This lesson measures the mechanism that makes that decision. The catalog's heading was
"Capabilities"; because this curriculum settled the equivalent of `capability` as
**privilege**, the lesson's name is **Privileges**, and this single form is used
throughout the course.

## The Fragmentation of Superuser Privilege

The classic model is binary: a process either belongs to user zero and is exempt from
every kernel check, or it does not and is exempt from none. A thousand privileged
operations all hang on a single flag. A program that wants to listen on a network port
also gets, under this model, the right to load kernel modules and change the ownership of
any file.

A **capability** is this single flag divided into separately grantable pieces. Every
privileged operation class is bound to a capability: binding to a low-numbered port,
opening a raw socket, changing network configuration, changing file ownership, bypassing
file permission checks, raising process priority, changing the system clock, loading a
kernel module, and mounting are separate capabilities. A process can only request the
operations its granted capabilities cover; for the rest, even belonging to user zero is
not enough.

The fragmentation is not even, and it carries a detail this course cares about. Some
capabilities are narrow in scope and open a single operation class; others are broad and,
on their own, cover enough operation classes to crack open the door to other capabilities.
The capability that bundles mounting, namespace management, and a great many
administrative operations together on its own is the best known of these. The rule in
practice is therefore short: **a broad capability is not granted.** When a program
genuinely needs one, moving the needed operation into a separate helper is a narrower
solution than granting that capability to the whole process.

## Five Sets and the Transition Rule

A process's capabilities are not a single list but **five sets**, and each set has a
separate job. The **effective set** carries the capabilities currently used in checks. The
**permitted set** is the pool the process can pull into the effective set. The
**inheritable set** shows what can be carried across an exec. The **bounding set** is a
ceiling: a capability not in it can never be regained by any route. The **ambient set** is
the bridge that lets inheritable capabilities carry over to the new program.

A separate flag sits above these sets: when the **no-new-privileges** flag is turned on,
the process and all its children cannot gain new capabilities through an exec. The flag is
turned on once and cannot be turned off. Capability dropping being permanent depends on
this flag; dropping done without the flag turned on can be reversed by capabilities
written on a file.

```text
# example dump — not executed; mask values are fiction
$ grep -E '^(Cap|NoNewPrivs)' /proc/self/status
CapInh: 0000000000000000
CapPrm: 000000000000cc00
CapEff: 000000000000cc00
CapBnd: 000000000000cc00
CapAmb: 0000000000000000
NoNewPrivs: 1
```

The masks in the dump are bit sets; every bit corresponds to a capability. What must be
read is not the individual bits but the **relationship between the sets**: if the bounding
set equals the permitted set, no dropping has been done; if the ambient set is empty, no
capability is carried across an exec.

Capabilities sit not only on processes but also on **files**. An executable file can have a
capability marked on it, and when that file runs, the process starts with a capability the
caller did not have. This is a narrow alternative to the old ownership flag: the file
grants not the whole privilege, only the marked capability.

```text
# example dump — not executed
$ getcap /usr/local/bin/metrics-collector
/usr/local/bin/metrics-collector cap_net_bind_service=ep
$ setcap -r /usr/local/bin/metrics-collector
$ find /root-tree -perm /6000 -type f
/root-tree/usr/bin/old-helper
```

The last line is an audit step: it lists the files in the root tree that carry an
ownership flag. If this list is not empty, a capability dropped from the process can come
back through the file. The dumps have not been executed, and no numeric claim comes from
them.

## Where a Capability Applies and Where It Is Written

Where a capability applies cannot be said without accounting for namespaces. A process
that opens a user namespace starts inside that namespace with a broad capability set; but
that set's scope is **only that namespace and the objects beneath it**. The same
capability does nothing to an object outside the namespace. The meaning of the first
lesson's mapping table is completed here: the user zero visible inside is an ordinary
mapped identity outside, and the boundary of its capabilities is the boundary of the
mapping.

This has two practical consequences. First, "appearing privileged inside" and "being
privileged on the machine" are two separate states, and mixing them up in diagnosis gives
the wrong answer. Second, when access to a resource outside the namespace is required — a
device node, a mount point, a kernel setting — the capability alone is not enough; that
resource must be explicitly brought inside the namespace. As long as it is not brought in,
the exposed surface does not grow.

Capabilities are written by the party that starts the process, and their durable place is
a configuration file.

```text
# example unit file fragment — not executed
[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/data/metrics
```

The first line in this fragment reduces the bounding set to a single capability: no
capability outside this list can ever be regained under this unit. The second line lets
that capability carry across an exec. The third line turns on the flag. The last two lines
touch the fact this lesson leaves open — they make the file system read-only and leave
only the needed directory writable. The settings' names vary by service manager; what does
not vary is the **order**: the ceiling is lowered first, then the single needed capability
is granted back.

A program's genuine capability needs are not found by guessing. The hardening habit works
in reverse: it starts by emptying the bounding set, the program is run in a test
environment, the denied operations are logged, and for every operation that lands in the
log, **a single capability** is granted back. This route earns two things. The
capabilities granted are the ones actually used, and the list turns into a written record
of what the program does; when a version changes, the list growing also becomes a visible
event. Least privilege was established in the Introduction to Cybersecurity course; its
counterpart here is this list.

## Privilege Axis: Two Facts, One Closure

- **IM17** — The privilege axis has **2 facts**: `capability-set` and `file-permissions`.
- **IM18** — Capability dropping is the only mechanism that closes the capability axis; it
  closes **1 fact** and **leaves open** the `file-permissions` fact.
- **IM19** — The distinction among the five sets is reduced in the model to a single
  fact; this is a deliberate limit of the model, and the sweep tests exactly this limit.
- **IM20** — File capabilities and file permission bits are gathered into the same fact
  in the model.

```python
"""Shared definition, the part this lesson uses: 24 facts, 9 axes, and
capability dropping that closes the capability axis. What is measured: REMAINING EXPOSED SURFACE."""

FACTS = [
    ("other-processes-list", "process"), ("own-process-number", "process"),
    ("ancestor-process-chain", "process"),
    ("root-filesystem-tree", "mount"), ("other-mounts", "mount"),
    ("shared-temp-dir", "mount"),
    ("machine-interfaces", "network"), ("machine-routing-table", "network"),
    ("listening-ports", "network"),
    ("user-id-mapping", "user"), ("file-ownership", "user"),
    ("machine-name", "machine"),
    ("cpu-share", "resource"), ("memory-limit", "resource"),
    ("used-memory", "resource"), ("cpu-count", "resource"),
    ("kernel-version", "kernel"), ("kernel-settings", "kernel"),
    ("system-load", "kernel"), ("clock", "kernel"),
    ("capability-set", "privilege"), ("file-permissions", "privilege"),
    ("access-label", "label"), ("policy-rules", "label"),
]
MECHANISMS = {
    "pid-namespace":     {"axis": "process", "left_open": ["own-process-number"]},
    "mount-namespace":   {"axis": "mount", "left_open": ["root-filesystem-tree"]},
    "network-namespace": {"axis": "network", "left_open": []},
    "user-namespace":    {"axis": "user", "left_open": ["file-ownership"]},
    "uts-namespace":     {"axis": "machine", "left_open": []},
    "cgroup":            {"axis": "resource", "left_open": ["cpu-count"]},
    "capability-dropping": {"axis": "privilege", "left_open": ["file-permissions"]},
}
SIX = ["pid-namespace", "mount-namespace", "network-namespace",
       "user-namespace", "uts-namespace", "cgroup"]


def visible(mechanisms, facts=None):
    facts = FACTS if facts is None else facts
    closed = set()
    for m in mechanisms:
        v = MECHANISMS[m]
        closed |= {o for o, e in facts if e == v["axis"]}
        closed -= set(v["left_open"])
    return [o for o, _ in facts if o not in closed]


def open_axes(mechanisms, facts=None):
    facts = FACTS if facts is None else facts
    g = set(visible(mechanisms, facts))
    return sorted({e for o, e in facts if o in g})


open_facts = set(visible(["capability-dropping"]))
print("ORACLE  fact:", len(FACTS), "| axis:", len({e for _, e in FACTS}))
print("capability axis has", sum(1 for _, e in FACTS if e == "privilege"), "facts")
print()
print("fact                 capability dropping")
for o, e in FACTS:
    if e == "privilege":
        print(f"  {o:20s} {'LEAVES OPEN' if o in open_facts else 'closes'}")
print()
g = visible(["capability-dropping"])
print("capability dropping alone: closed", len(FACTS) - len(g),
      "| left open 1 | visible", len(g),
      "| open axis", len(open_axes(["capability-dropping"])))
```

```
ORACLE  fact: 24 | axis: 9
capability axis has 2 facts

fact                 capability dropping
  capability-set       closes
  file-permissions     LEAVES OPEN

capability dropping alone: closed 1 | left open 1 | visible 23 | open axis 9
```

Three numbers stand side by side. **Oracle:** 24 facts, 9 axes; 2 facts on the capability
axis. **Isolation:** capability dropping closes **1 fact**. **Remaining exposed surface:**
**23 facts** and **9 axes**. This is among the least-closing of the mechanisms measured
alone, and the number is not misleading: capability dropping narrows what a process **can
request**, not what it sees.

This is a limit of the measurement and must be written down openly. This course counts
visibility; a mechanism's operational value is not measured by its closed-fact count
alone. Against its single fact in the table, capability dropping closes most of the
privileged operation classes a process could request from the kernel. The two measures
answer separate questions, and neither substitutes for the other.

## The Fact Left Open: File Permissions

The fact that does not close is in an unexpected place. A capability dropped from a
process **does not change what is written on the file.** If a file in the root tree
carries an ownership flag or a marked capability, when that file runs, the new process's
starting set is computed from the file. The dropping was done on the process, not on the
file system, and the two sides do not know about each other.

The same distinction holds for file permission bits. When the capability that bypasses
permission checks is dropped, the process can no longer access every file; but the
permissions written on the files stay in place, and a world-writable directory stays
writable for a process with dropped capabilities too. Capability dropping is a **request
filter**, not a file system operation.

The narrowing path therefore runs through the file system and has four steps. First, the
root tree is mounted **read-only**; writable space is handed to a separate, narrow mount.
Second, the `nosuid`, `nodev`, and `noexec` options are placed on writable mounts — the
first cuts off gaining a capability through a file, the second producing a device node,
the third running a program from that space. Third, while the root tree is being
prepared, files carrying an ownership flag are searched for and the flag is cleared on the
ones that do not need it; this is the job of the last line in the lesson's example dump.
Fourth, the **no-new-privileges** flag is turned on and the bounding set is dropped; done
together, a dropped capability never comes back through any exec.

What the four have in common is this: none of them is done on the process, all are done in
**the process's world**. The model represents this distinction with a single open fact,
and what closing that fact requires is not a second capability setting but narrowing the
root tree itself.

## The Growing Bundle and Fact Set Sweep

- **IM21** — Bundle order is binding: five namespaces, the cgroup, then capability
  dropping.
- **IM22** — The added fact `ambient-capabilities` belongs to the capability axis and is
  not among what capability dropping leaves open.
- **IM23** — The removed `file-permissions` is the capability axis's single fact left
  open.

```python
# --- This block builds on the previous block: FACTS, MECHANISMS, SIX,
# visible and open_axes come from there.
SEVEN = SIX + ["capability-dropping"]
print("the bundle growing through the course")
print("stage                      mechanism  visible  open axis")
for name, d in (("six mechanisms", SIX), ("+capability dropping", SEVEN)):
    print(f"  {name:24s} {len(d):7d}  {len(visible(d)):7d}  {len(open_axes(d)):10d}")
print()
ADDED = FACTS + [("ambient-capabilities", "privilege")]
MISSING = [(o, e) for o, e in FACTS if o != "file-permissions"]
print("fact set sweep (seven mechanisms applied)")
print("set                        fact  axis  visible  open axis  privilege open")
for name, facts in (("base", FACTS), ("+ambient-capabilities", ADDED),
                     ("-file-permissions", MISSING)):
    g = set(visible(SEVEN, facts))
    e = open_axes(SEVEN, facts)
    po = sum(1 for o, ax in facts if ax == "privilege" and o in g)
    print(f"  {name:24s} {len(facts):4d}  {len({x for _, x in facts}):5d}"
          f"  {len(g):7d}  {len(e):10d}  {po:10d}")
```

```
the bundle growing through the course
stage                      mechanism  visible  open axis
  six mechanisms                 6       12           7
  +capability dropping           7       11           7

fact set sweep (seven mechanisms applied)
set                        fact  axis  visible  open axis  privilege open
  base                       24      9       11           7           1
  +ambient-capabilities      25      9       11           7           1
  -file-permissions          23      9       10           6           0
```

The seventh mechanism drops the visible fact count from 12 to **11**: one fact. The open
axis count still stands at **7**, and this gives the same result for a third time. Adding
a mechanism narrows the surface, not the axis; something stays open on every axis.

The sweep also shows the model's limit. When a fact independent of the eighth mechanism is
added to the privilege axis, capability dropping closes it too, and the surface stays at
11: modeling the five sets separately would not have changed the result. By contrast, when
the single fact left open is removed, the axis closes, the surface drops to **10**, and
the open axis count to **6**. The result points the same direction as the previous two
lessons: what determines the surface is not the fact count on the axis, but **the
existence of the fact left open.** Every number in the model is a full count over the
fiction, not a measurement taken on a machine.

## Summary

- A capability is superuser privilege divided into separately grantable pieces;
  capabilities are not equal in scope, and a **broad capability is not granted**.
- A process has **five capability sets** — effective, permitted, inheritable, bounding,
  and ambient — and above them sits the **no-new-privileges** flag, which is turned on
  once and cannot be turned off.
- The privilege axis has **2 facts**; capability dropping closes **1** of them, **leaves
  open** the `file-permissions` fact, and on its own leaves **23** of the 24 facts
  visible.
- The reason the fact stays open is structural: dropping is done on the process, what is
  written on the files does not change. The narrowing path is in the file system — a
  read-only root, the `nosuid`, `nodev`, and `noexec` options, clearing ownership flags,
  and dropping the bounding set.
- In the seven-mechanism bundle, the visible fact count drops **from 12 to 11**, and the
  **open axis count stays at 7**; in the sweep, removing the single fact left open drops
  the surface to **10** and the axis to **6**.

## Next Step

Capability dropping narrowed what a process can request from the kernel, but it never
asked **whom** the request is directed at. Permission checking still rests on decisions
made by the file's owner: if a directory is marked world-writable, a process with dropped
capabilities writes there too. The next lesson takes up the layer that takes this decision
out of the owner's hands and binds it to a policy. Subject and object are given an access
label, every access between them is made explicitly allowed by a policy rule, and access
is denied if there is no rule. What is measured is two-sided: the fact the policy closes,
and the policy's **own** exposed surface, that is, the number of accesses allowed when not
needed.
