Processes & Privileges

Overview

What a running process may do: capabilities, syscall filters, and the isolation primitives behind containers.

Every security decision on a Linux box eventually resolves to a question about a running process: what user does it run as, what can it touch, which system calls can it make, and what can it see of the rest of the machine. Classic Unix answered this with one blunt instrument, the UID: root (UID 0) could do everything and everyone else was constrained by file permissions. That model is easy to reason about and completely inadequate for modern workloads, where a single host runs dozens of semi-trusted services and containers side by side.

The kernel now offers a toolbox of finer-grained controls that layer on top of the old UID model. This group walks through each of them, from the process credentials themselves up to the isolation primitives that containers are assembled from. Understanding these building blocks is what lets you say precisely why a compromised web server cannot read /etc/shadow, load a kernel module, or see the database container running next to it.

The layers of process confinement

Confinement is not one feature but a stack. Each layer answers a different question and can fail independently, which is why defence in depth matters here.

LayerQuestion it answersCovered in
Credentials (UID/GID)Who is this process, for permission checks?Process Model
CapabilitiesWhich slices of root power does it hold?Linux Capabilities
SeccompWhich system calls may it invoke?Seccomp
NamespacesWhat slice of the system can it see?Namespaces
CgroupsHow much CPU, memory, and IO may it consume?Cgroups
MAC (AppArmor/SELinux)What does policy allow, regardless of ownership?Mandatory Access Control

A container runtime such as Docker, containerd, or a Kubernetes kubelet combines all of these at once. When you write a Kubernetes securityContext, you are choosing values for capabilities, seccomp, and (optionally) AppArmor at the same time. The Kubernetes Security foundation revisits these same primitives from the orchestration side, so the vocabulary here transfers directly.

How privileges attach and inherit

Privileges are attached to a process, not to a program file on disk (with the exception of setuid bits and file capabilities, which are how a fresh privilege gets injected at exec time). When a process calls fork(), the child inherits the parent’s credentials, capability sets, seccomp filters, namespaces, and cgroup membership. An execve() can change some of these according to well defined rules.

The security consequence is that privilege flows downhill by default. A daemon started as root that spawns worker processes hands those workers root unless it deliberately drops privilege first. Getting the drop right, lowering the UID, clearing capabilities, and installing a seccomp filter before running untrusted code, is the recurring pattern behind every hardened service unit. The Process Model page traces exactly how these credentials pass from parent to child.

Splitting and dropping root

The single most important idea in this group is that “root” is not atomic anymore. The kernel decomposed the powers of UID 0 into roughly forty capabilities, so a program that only needs to bind port 80 can hold CAP_NET_BIND_SERVICE and nothing else. A backup job that needs to read every file can hold CAP_DAC_READ_SEARCH without being able to load kernel modules or change the system clock.

This decomposition is what makes least privilege achievable on Linux. Instead of “run as root or run as nobody,” you can grant the exact powers a task requires and drop the rest. The Linux Capabilities page covers the sets, the dangerous ones, and how to audit what a running process actually holds.

Hands-on: fingerprint a process

You do not need special tooling to start reasoning about confinement. The /proc filesystem exposes almost everything about a process’s security state as plain text.

# Look at the systemd-resolved daemon
pid=$(pgrep -x systemd-resolve | head -1)
grep -E 'Uid|Gid|CapEff|Seccomp|NoNewPrivs' /proc/"$pid"/status

Expected output:

Uid:	991	991	991	991
Gid:	991	991	991	991
CapEff:	0000000000003400
Seccomp:	2
Seccomp_filters:	1
NoNewPrivs:	1

That single block tells a story: the daemon runs as an unprivileged UID (not 0), it holds only two capabilities in its effective set (the 3400 mask), it has a seccomp filter installed in filter mode (Seccomp: 2), and NoNewPrivs is set so it can never gain privilege through a setuid binary. Every page in this group teaches you to read and set one of those fields.

Practical Guidance

  1. Treat “runs as root” as a finding, not a default. For any daemon, ask which specific capabilities it truly needs and drop the rest with systemd’s CapabilityBoundingSet= and AmbientCapabilities=.
  2. Read /proc/<pid>/status before trusting a service’s confinement. The CapEff, Seccomp, and NoNewPrivs fields are the ground truth, not the documentation.
  3. Layer controls rather than relying on one. A seccomp filter plus dropped capabilities plus a namespace is far harder to escape than any single mechanism.
  4. Learn the primitives on the host first, then map them to containers. Everything Docker and Kubernetes do here is a wrapper over the same syscalls you can drive by hand with unshare, capsh, and setcap.
  5. Use the disposable lab VM to break things safely. Reset from a snapshot after each experiment so a botched seccomp filter or namespace never leaves the host in a strange state.