Namespaces, cgroups, Capabilities, and Containers
Understand how Linux namespaces, cgroup v2, capabilities, seccomp, and mandatory access controls combine to create and constrain containers.
Learning objectives
By the end of this lesson
- Explain why a Linux container is a constrained process tree rather than a miniature virtual machine.
- Inspect namespace membership, UID/GID mappings, and cgroup placement for running processes.
- Calculate and verify CPU and memory controls under cgroup v2.
- Reduce privileges with capabilities, no-new-privileges, seccomp, and LSM policy.
- Relate kernel primitives to OCI bundles and container-runtime behavior.
1. Containers compose kernel isolation and resource-control primitives
A Linux container does not boot a separate kernel. It starts ordinary processes that share the host kernel while receiving restricted views of selected global resources. Namespaces change what a process can see, cgroups govern how much resource it may consume, capabilities divide traditional root privilege, seccomp filters system calls, and SELinux or AppArmor can constrain object access. Filesystem layers and an OCI runtime assemble these mechanisms into a repeatable workload boundary.
flowchart LR I["OCI image and runtime configuration"] --> R["Runtime creates process"] R --> N["Namespaces: pid, mount, net, ipc, uts, user, cgroup, time"] R --> C["cgroup v2: CPU, memory, I/O, pids"] R --> P["Capabilities, seccomp, no_new_privs"] R --> L["SELinux or AppArmor policy"] N --> W["Container workload"] C --> W P --> W L --> W
Isolation is layered, not absolute. A container escape, over-broad device access, a privileged runtime socket, a vulnerable kernel, or unsafe host mounts can cross the intended boundary. Treat containers as a workload packaging and isolation mechanism—not as an automatic security classification.
2. Namespaces virtualize selected global resources
Each namespace type isolates a different kernel resource. Processes can share some namespaces while receiving separate instances of others. This is why containers in one Kubernetes Pod share a network namespace but normally retain process and filesystem controls determined by the runtime configuration.
# Inspect namespace membership for the current shell and PID 1.
readlink /proc/self/ns/*
readlink /proc/1/ns/*
# List namespace objects and their member processes.
lsns
lsns -t pid
lsns -t net
# Compare two processes. Equal inode numbers mean shared membership.
stat -Lc '%n -> inode %i' /proc/self/ns/{mnt,pid,net,user}
stat -Lc '%n -> inode %i' /proc/1/ns/{mnt,pid,net,user}
# Inspect a process from selected namespaces only when authorized.
# sudo nsenter -t PID -m -u -i -n -p -- ps -efnsenter can expose secrets, sockets, filesystems, or control interfaces inside another workload. Use an approved troubleshooting path and record the target PID and namespaces entered.
3. User namespaces remap identity and privilege scope
A user namespace can map a UID that appears as 0 inside the namespace to a non-root UID outside it. Capabilities granted in that namespace apply only to resources governed by that namespace and its descendants. Mapping ranges are visible in uid_map and gid_map. Rootless container engines normally require subordinate UID and GID ranges so one host user can represent multiple container identities.
pid=${1:-self}
printf 'UID map for %s:
' "$pid"
cat "/proc/$pid/uid_map"
printf 'GID map for %s:
' "$pid"
cat "/proc/$pid/gid_map"
# Subordinate ranges, when configured by the distribution.
grep -E "^${USER}:" /etc/subuid /etc/subgid 2>/dev/null || true
# Non-destructive user-namespace demonstration when unprivileged user
# namespaces are enabled. The command cannot grant host-level root.
unshare --user --map-root-user --mount-proc sh -c 'id; cat /proc/self/uid_map; capsh --print 2>/dev/null | head' User namespaces reduce the consequences of container-root compromise, but they expand kernel attack surface available to unprivileged users. Pair them with resource controls, current kernels, conservative device access, and workload-specific policy.
4. cgroup v2 organizes processes hierarchically and distributes resources
cgroup v2 presents one unified hierarchy, usually mounted at /sys/fs/cgroup. Controllers expose interface files such as cpu.max, memory.max, io.max, and pids.max. systemd normally owns the top-level hierarchy and delegates subtrees to services or container managers. Bypassing the service manager can create conflicting ownership and accounting.
# Confirm the unified hierarchy and current membership.
findmnt -t cgroup2
cat /proc/self/cgroup
systemd-cgls --no-pager
systemd-cgtop --depth=3
# Inspect controller availability and current limits.
cg=/sys/fs/cgroup
cat "$cg/cgroup.controllers"
cat "$cg/cgroup.subtree_control"
cat "$cg/cpu.max" 2>/dev/null || true
cat "$cg/memory.max" 2>/dev/null || true
cat "$cg/pids.max" 2>/dev/null || true
# For a systemd service, ask systemd for the authoritative placement.
systemctl show ssh.service -p ControlGroup -p CPUQuotaPerSecUSec -p MemoryMax -p TasksMax 2>/dev/null || trueDelegation is an ownership contract. A manager receiving a delegated subtree may create descendants and distribute resources within it, but should not modify ancestors or unrelated branches.
5. Resource limits require units, hierarchy, and workload context
In cpu.max, a quota of Q microseconds per period P permits an average CPU fraction of:
\[ CPU_{cores} = \frac{Q}{P} \qquad CPU_{percent} = 100 \times \frac{Q}{P} \]
For example, 50000 100000 permits approximately half of one CPU on average. Bursts, scheduler behavior, parent limits, and multiple runnable threads still affect observed latency.
memory.max is a hard ceiling; memory.high is a throttling and reclaim boundary; memory.current reports charged usage. Parent constraints always bound descendants. Monitor memory.events, pressure stall information, throttling, and workload latency instead of checking configuration alone.
# Run a transient service in a managed cgroup without modifying unit files.
systemd-run --user --scope -p CPUQuota=50% -p MemoryMax=256M -p TasksMax=64 bash
# In another shell, locate and inspect the scope.
systemctl --user list-units --type=scope
systemctl --user show UNIT.scope -p ControlGroup -p CPUQuotaPerSecUSec -p MemoryMax -p TasksMax
# Read cgroup event evidence using the returned ControlGroup path.
# cat /sys/fs/cgroup/user.slice/.../cpu.stat
# cat /sys/fs/cgroup/user.slice/.../memory.events6. Capabilities split root privilege into narrower operations
Linux capabilities replace one all-powerful superuser check with named privilege bits such as CAP_NET_BIND_SERVICE, CAP_CHOWN, and CAP_SYS_ADMIN. Processes have permitted, effective, inheritable, bounding, and ambient sets. Container runtimes normally start with a reduced default set, but the exact defaults are runtime and version dependent.
# Inspect capabilities of the current shell and a target process.
command -v capsh >/dev/null && capsh --print
pid=${1:-$$}
grep -E '^(Uid|Gid|Cap(Inh|Prm|Eff|Bnd|Amb)|NoNewPrivs|Seccomp):' "/proc/$pid/status"
# Decode a hexadecimal capability mask when capsh is installed.
mask=$(awk '/^CapEff:/ {print $2}' "/proc/$pid/status")
capsh --decode="0x$mask" 2>/dev/null || true
# File capabilities are separate from setuid bits.
getcap -r /usr/bin /usr/sbin 2>/dev/null | head -n 40Avoid treating CAP_SYS_ADMIN as a convenient compatibility switch. It covers many unrelated privileged operations and can erase much of the intended container boundary.
7. Seccomp, no-new-privileges, and LSM policy constrain behavior
no_new_privs prevents an execve transition from granting privileges that the process did not already hold. Seccomp can allow, deny, trap, or notify on system calls according to a filter. SELinux and AppArmor add policy decisions over files, sockets, capabilities, and other objects. These controls address different questions and should be composed.
pid=${1:-$$}
grep -E '^(NoNewPrivs|Seccomp|Seccomp_filters):' "/proc/$pid/status"
# Show mandatory-access-control state when available.
getenforce 2>/dev/null || true
sudo aa-status 2>/dev/null | head -n 40 || true
# systemd can expose a service's security posture.
systemd-analyze security --no-pager ssh.service 2>/dev/null | head -n 80 || trueA deny event must be observable. Preserve audit or journal evidence and distinguish a policy violation from an application bug. Avoid disabling the entire profile as the first troubleshooting step; reproduce the exact denied operation and make the narrowest justified change.
8. OCI specifications connect images, bundles, and runtime execution
An OCI image describes content-addressed filesystem layers and configuration. An OCI runtime bundle contains a root filesystem and config.json describing process arguments, mounts, namespaces, capabilities, resource settings, and hooks. A low-level runtime creates the process according to that bundle, while a higher-level engine handles images, networking, lifecycle, logging, and APIs.
# Runtime and engine inventory; commands are optional.
for cmd in runc crun containerd ctr nerdctl podman docker crictl; do
command -v "$cmd" >/dev/null && printf '%-10s %s
' "$cmd" "$(command -v "$cmd")"
done
# Inspect OCI-style configuration through an engine without executing it.
image=${1:-docker.io/library/alpine:latest}
podman image inspect "$image" 2>/dev/null | head -n 80 || docker image inspect "$image" 2>/dev/null | head -n 80 || trueImage metadata does not prove runtime policy. The final boundary is determined by the host kernel, runtime configuration, engine defaults, orchestrator settings, mounted devices and paths, network policy, and the identity that controls the runtime API.
9. Hands-on lab: inspect an isolated process without creating a permanent container
This lab uses a transient user service and, where permitted, a user namespace. It does not install a runtime or change host configuration.
lab="$HOME/devops-academy/linux/chapter19/lesson01"
mkdir -p "$lab"
{
date -u --iso-8601=seconds
uname -a
findmnt -t cgroup2
cat /proc/self/cgroup
stat -Lc '%n %i' /proc/self/ns/{mnt,pid,net,user}
} > "$lab/host-baseline.txt"
if unshare --user --map-root-user true 2>/dev/null; then
unshare --user --map-root-user --mount-proc sh -c '
id
printf "uid_map:
"; cat /proc/self/uid_map
printf "namespaces:
"; stat -Lc "%n %i" /proc/self/ns/{mnt,pid,net,user}
grep -E "^(CapEff|NoNewPrivs|Seccomp):" /proc/self/status
' > "$lab/user-namespace.txt"
else
printf 'Unprivileged user namespaces are disabled or unavailable.
' > "$lab/user-namespace.txt"
fi
systemd-run --user --wait --collect -p MemoryMax=128M -p TasksMax=32 /bin/sh -c 'printf "cgroup="; cat /proc/self/cgroup; id' > "$lab/transient-scope.txt" 2>&1 || true
sha256sum "$lab"/* > "$lab/SHA256SUMS"
printf 'Review %s
' "$lab"Verification checklist
10. Troubleshoot from the process outward
- Identify the exact host PID and container or orchestration identity.
- Record namespace inodes, UID/GID maps, cgroup path, capabilities, seccomp state, and LSM label.
- Confirm the effective configuration produced by the engine—not only the requested YAML or CLI flags.
- Check kernel and audit logs for denials, OOM events, throttling, and namespace failures.
- Reproduce with one controlled change and preserve before/after evidence.
“Container root is host root.”
With user namespaces it may map to an unprivileged host identity; without them, UID 0 remains more consequential. Inspect the mapping.
“A memory limit guarantees predictable latency.”
Hard limits can trigger reclaim or OOM termination. Observe pressure, events, and application behavior.
“Dropping one capability makes a container safe.”
Mounts, devices, runtime sockets, seccomp, LSM policy, kernel exposure, and application vulnerabilities still matter.
“The requested policy is the effective policy.”
Runtime defaults, parent cgroups, admission controls, and host configuration may alter or reject it.
11. Knowledge check
What is the difference between a namespace and a cgroup?
Why can PID 1 inside a container behave differently from an ordinary application process?
Why should capability inspection include the bounding and ambient sets, not only the effective set?
12. Summary
- Containers are host-kernel processes constrained by several independent mechanisms.
- Namespaces isolate views; cgroup v2 controls hierarchical resource distribution.
- User namespaces can map container root to an unprivileged host identity.
- Capabilities, seccomp, no-new-privileges, and LSM policy reduce different classes of risk.
- Effective runtime state must be inspected at the process and kernel interfaces.
13. Further reading
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.