all posts

Linux capabilities, explained for sandboxing: five sets, one bounding ceiling, and the bits that are still root

Ajay Kumar··10 min read

Linux capabilities exist because root used to be one bit, and one bit was too coarse. A program that needed to bind port 80 had to be uid 0, and being uid 0 also meant it could load kernel modules, read every file on the disk, and reboot the machine. Capabilities split that single bit into about forty distinct privileges — CAP_NET_BIND_SERVICE, CAP_SYS_MODULE, CAP_DAC_OVERRIDE, and friends — so you can hand out the one a program needs instead of all of them. That is a genuinely good idea and it works.

I'm Ajay; I build PandaStack, which runs untrusted and model-generated code in Firecracker microVMs. This post covers the five capability sets, the execve rules that decide what your process ends up holding, file capabilities and the effective bit, what --cap-drop is really manipulating, and the dozen or so capabilities that are root wearing a lanyard. The conclusion up front: capabilities are a privilege-reduction tool, not an isolation boundary. Dropping them is cheap and you should do it. It just doesn't change who owns the kernel.

What a capability actually is

A capability is a bit in a bitmask, and the kernel checks for it where it used to check for uid 0. The network code checks CAP_NET_BIND_SERVICE before letting you bind a low port; the module loader checks CAP_SYS_MODULE. Nothing more mystical than that: a few thousand call sites that each ask "does this thread hold bit N?" instead of "is this thread root?".

Two details about that sentence matter more than they look. First, thread — capabilities on Linux are per-thread, not per-process, despite POSIX's intentions. /proc/PID/status shows the main thread's sets; a sibling thread can hold something different. That's why libcap ships a helper to fan a change out across every thread, and why calling capset from Go is a footgun: the runtime moves goroutines between threads, so you drop a capability on the thread you happen to be on and keep it everywhere else.

Second, the check is namespace-relative. Most call sites use ns_capable(), which asks whether you hold the bit in the user namespace that owns the object you're touching, not in the initial one. That's the mechanism behind the surprise in the user-namespace post: unshare -Ur hands you a full capability set, and it's a real set, honoured against the mount and network namespaces you create inside — and worth nothing against a file owned by a host uid you can't even name. A full CapEff is not evidence of power. It's evidence of a full CapEff.

The five sets, and what each is actually for

Every thread carries five capability bitmasks. People memorise the names and still can't answer "what will I hold after exec?", because the interesting behaviour is in how the sets interact, not in what they're called. Here's each one by its job:

  • Permitted (CapPrm) — the set you are allowed to hold. It's the reservoir: you can raise anything from permitted into effective, and you can drop from permitted, but you can never add to it at runtime. Dropping from permitted is irreversible for the life of the thread, which makes it the right thing for a daemon to do the moment it's finished being privileged.
  • Effective (CapEff) — the set the kernel actually checks. This is the one that answers "can I do the thing right now." A well-written privileged daemon keeps effective empty most of the time and raises a capability out of permitted only around the syscall that needs it, so a bug in the other 99% of the code has nothing to work with.
  • Inheritable (CapInh) — a set that survives execve, but only by intersecting with the executed file's inheritable set. On its own it does nothing: put a capability in inheritable, exec a normal binary with no file capabilities, and you get nothing. It's a handshake that needs both sides, and for most of its life it was the least useful set on Linux.
  • Bounding (CapBnd) — the ceiling. A hard mask on what this thread and its descendants can ever gain, including via a setuid-root binary or file capabilities. You can only remove bits from it (needs CAP_SETPCAP), never add, and the removal is inherited by every child forever. This is the set that does the real work in container security, and it is the one people talk about least.
  • Ambient (CapAmb) — added in Linux 4.3 to fix the inheritable set's uselessness. Ambient capabilities survive execve of an ordinary, unprivileged binary — no file capabilities required. The rules keep it honest: a capability can be in ambient only if it's in both permitted and inheritable, dropping it from either clears it from ambient, and executing anything setuid or with file capabilities wipes the whole ambient set.
The mental model: permitted is your wallet, effective is the note in your hand, inheritable is a promise that means nothing unless the other party made the same one, ambient is the promise that finally works alone, and bounding is the credit limit — the one number you can only ever lower, which is why it's the one that actually protects you.
# Who am I, capability-wise? Three ways to ask, all reading the same five masks.

capsh --print
# Current: =ep cap_sys_admin-ep      <- permitted/effective/inheritable, with
# Bounding set =cap_chown,cap_dac_override,...     flags: e=effective,
# Ambient set =                                    p=permitted, i=inheritable

getpcaps $$
# 4213: =        <- same data for one pid. An empty set prints as nothing,
#                   which is the most reassuring output in this post.

# The raw source of truth: five 64-bit masks in /proc/<pid>/status.
grep -E '^Cap(Inh|Prm|Eff|Bnd|Amb)' /proc/self/status
# CapInh: 0000000000000000
# CapPrm: 0000000000000000
# CapEff: 0000000000000000
# CapBnd: 000001ffffffffff
# CapAmb: 0000000000000000
#
# Read that carefully. As an ordinary user I hold NOTHING -- permitted and
# effective are empty -- but my BOUNDING set is full. Bounding is a ceiling on
# what could ever be gained (say, by running a setuid-root binary), not a
# list of powers I have. Confusing the two is the #1 misreading of this file.

# Turn any mask into names:
capsh --decode=000001ffffffffff
# 0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,...,
#                    cap_bpf,cap_checkpoint_restore
#   41 bits set == every capability the kernel currently defines.

# Now the same question inside a default Docker container:
docker run --rm alpine sh -c 'grep -E "^Cap(Prm|Eff|Bnd)" /proc/self/status'
# CapPrm: 00000000a80425fb
# CapEff: 00000000a80425fb
# CapBnd: 00000000a80425fb

capsh --decode=00000000a80425fb
# cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,
# cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,
# cap_mknod,cap_audit_write,cap_setfcap
#
# Fourteen bits out of forty-one. THAT is what "root in a container" means --
# uid 0 with most of root's actual powers already removed by the runtime.
# The exact list has drifted between runtime releases: decode it on YOUR box.

What execve does to the five sets

Everything confusing about capabilities lives in this transition, so it's worth stating as rules rather than as the algebra in capabilities(7):

  1. Inheritable and bounding pass through unchanged. execve never widens either. The only way to shrink the bounding set is a deliberate prctl with CAP_SETPCAP, and it's one-way for you and every descendant.
  2. The new permitted set is the union of three routes in: (old inheritable AND the file's inheritable set), (the file's permitted set AND your bounding set), and the new ambient set. Every route is gated by something you control.
  3. The new effective set is the entire new permitted set if the file's effective bit is set; otherwise it's just the ambient set. Note bit, singular — on modern file capabilities the effective side is one flag, not a mask.
  4. Ambient passes through unchanged, unless the file is privileged — has file capabilities or a setuid/setgid bit — in which case ambient is wiped to zero. Ambient is for carrying privilege into ordinary binaries, not for stacking it onto privileged ones.
  5. The root special case, which explains containers: if the binary has no file capabilities and your euid is 0 (or the file is setuid-root), the kernel behaves as though the file's permitted set were full and its effective bit set. So for a root process, permitted-after-exec collapses to exactly your bounding set. Root doesn't get capabilities from the file; root gets whatever the ceiling still allows.

One more switch belongs here: no_new_privs, set with prctl(PR_SET_NO_NEW_PRIVS, 1) since Linux 3.5. Once set it is inherited by every child, can never be unset, and makes execve promise not to grant anything you didn't already have — setuid bits ignored, file capabilities ignored. Ambient capabilities are the deliberate exception, because you already held those. It's also why an unprivileged process can install a seccomp filter: without CAP_SYS_ADMIN, seccomp requires no_new_privs first, so a filtered process can't exec its way out through a setuid binary.

File capabilities: privilege attached to a binary instead of a user

File capabilities are the replacement for setuid-root, and they're the good half of this feature. Instead of "this binary runs as root, please audit forty thousand lines of it," you say "this binary may bind low ports." The data lives in an extended attribute, security.capability: a permitted set, an inheritable set, and that single effective bit — plus, in version 3 of the format, a root uid so the file can carry capabilities meaningfully inside a user namespace.

# Give a server the right to bind ports below 1024 -- and nothing else.
sudo setcap cap_net_bind_service=+ep ./server

getcap ./server
# ./server cap_net_bind_service=ep
#                               ^^
#  p = the file's PERMITTED set: at execve this lands in the new process's
#      permitted set, masked by the bounding set.
#  e = the file's EFFECTIVE BIT -- one bit, not a set. It means "raise the
#      whole new permitted set into effective at exec", which is what a
#      capability-unaware binary needs, because it never calls capset()
#      itself. A capability-AWARE daemon should ship =p only, raise the bit
#      around the bind(), and drop it from permitted immediately after.

./server --port 80        # binds :80 as uid 1000. No setuid root anywhere.

getpcaps $(pgrep -f ./server)
# 5120: cap_net_bind_service=ep      <- one bit. Not root. One bit.

# Three things that will surprise you:
#
# 1. no_new_privs turns this off. A process that set PR_SET_NO_NEW_PRIVS -- or
#    a container run with no-new-privileges -- has file capabilities IGNORED
#    at execve. That is the entire point of the flag, and it means "harden the
#    container" and "ship a setcap'd binary in it" are in direct conflict.
#
# 2. The xattr is fragile. It does not survive a plain cp, most tar archives,
#    rsync without --xattrs, or a COPY in an image build unless the setcap runs
#    inside the build. "Works locally, EACCES in the image" is nearly always this.
#
# 3. The bounding set still wins. If the capability is not in the process's
#    bounding set, the file's permitted bit is masked away -- and if the file's
#    effective bit was set, execve fails outright with EPERM (the safety check
#    for capability-dumb binaries) rather than starting a program that thinks
#    it is privileged. This is exactly why --cap-drop=ALL is trustworthy: it
#    lowers the ceiling, not just the current holdings.

sudo setcap -r ./server   # and remove them again

How --cap-drop in containers actually works

A container runtime doesn't do anything exotic here. Before it execs your entrypoint it writes the four runtime sets and the bounding set from the OCI spec, and --cap-drop / --cap-add is just editing those lists. What makes the drop stick is the bounding set: once a bit is out of bounding, rule 2 above masks it away from any file's permitted set, and rule 5 caps a root process's post-exec permitted set at the bounding set. There is no setuid binary in the image that can put it back.

# The pattern: drop everything, add back the one thing you need, and forbid
# regaining anything at exec. All three parts do separate work.

docker run --rm \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --security-opt=no-new-privileges \
  --read-only \
  myimage

# --cap-drop=ALL      -> empties the BOUNDING set too. The load-bearing part:
#                        nothing can be regained later, by anyone, ever.
# --cap-add=...       -> puts exactly one bit back.
# --security-opt=no-new-privileges -> PR_SET_NO_NEW_PRIVS, so execve cannot
#                        grant privilege via setuid bits or file capabilities.

# Verify. Always verify -- decode the masks, don't trust the flags.
docker run --rm --cap-drop=ALL --cap-add=NET_BIND_SERVICE alpine \
  sh -c 'grep -E "^Cap(Prm|Eff|Bnd|Amb)" /proc/self/status'
# CapPrm: 0000000000000400   <- bit 10, cap_net_bind_service, alone
# CapEff: 0000000000000400
# CapBnd: 0000000000000400   <- the ceiling came down with it
# CapAmb: 0000000000000000   <- empty. Remember this line.

# THE GOTCHA that eats an afternoon: run as a NON-root uid and the capability
# you carefully added can evaporate at the first execve. With no file caps on
# the binary and an empty ambient set, the exec-time formula recomputes
# permitted as zero -- the euid-0 special case was the only reason it stuck
# for root containers. "I added NET_BIND_SERVICE and port 80 still says
# EACCES" is this, every time.

# Kubernetes equivalent -- same three ideas, same gotcha:
#
#   securityContext:
#     runAsNonRoot: true
#     runAsUser: 10001
#     allowPrivilegeEscalation: false      # this IS no_new_privs
#     readOnlyRootFilesystem: true
#     seccompProfile:
#       type: RuntimeDefault
#     capabilities:
#       drop: ["ALL"]
#       add:  ["NET_BIND_SERVICE"]         # ...which may do nothing as non-root
#
# Kubernetes has historically not populated the ambient set, so for a non-root
# container the durable answers are: bind a high port behind a proxy, or bake
# file capabilities into the image (and then don't set no-new-privileges).

Two more things about the container side. --privileged isn't "all capabilities" — it's all capabilities plus an unrestricted device cgroup, unconfined seccomp and LSM profiles, and a host-visible /sys. And the default set includes CAP_MKNOD, which sounds alarming (create a device node for the host disk, read the raw filesystem) and isn't, because the device cgroup denies access to the node you just made. Capabilities never work alone — which also means reasoning about one in isolation gives you the wrong answer about whether you're safe.

The capabilities that are still root

Here's the part that determines whether your drop-list is security theatre. Capabilities aren't equal-sized slices of root. A few of them are, functionally, all of root reachable through a slightly longer path. If any of these are in your container's set, the fact that you dropped the other thirty is decoration.

  • CAP_SYS_ADMIN — the grab bag. When the designers weren't sure where a privileged operation belonged, it went here, and the man page now needs roughly thirty bullet points to list what it permits: mount and pivot_root, setns into other namespaces, quotactl, perf_event_open, a long tail of device ioctls, sethostname. Mount alone is enough — the classic container escape is to mount a cgroup v1 hierarchy inside the container and point its release_agent at a script that the host executes as real root. Granting CAP_SYS_ADMIN is what you do when you have given up.
  • CAP_SYS_MODULE — load an arbitrary kernel module. There is no analysis to do and no mitigation to layer on: a module runs in ring 0 with no supervision, so this capability doesn't let you attack the kernel, it lets you BE the kernel. Seccomp, LSMs, and namespaces are all data structures owned by a thing you now control.
  • CAP_SYS_PTRACE — attach to other processes and read or write their memory. Inside a shared pid namespace this means any more-privileged sibling is yours; combined with a pid namespace shared with the host (--pid=host, common in monitoring sidecars) it means host processes are yours. Yama's ptrace_scope narrows the default, but CAP_SYS_PTRACE is precisely the thing that overrides it.
  • CAP_DAC_READ_SEARCH — bypass file read and directory-search permission checks. On its own it reads every secret on the box. Its more interesting property is that it gates open_by_handle_at(2), which resolves a file handle — essentially an inode number — into a descriptor without walking a path. Path-based confinement like chroot or a container root only works because paths are the only way in; give a process a way to name inodes directly and it can reach the filesystem outside its root. That is the Shocker class of escape from 2014, and the reason it isn't a live issue is exactly that this capability is not in the default container set.
  • CAP_NET_ADMIN — full control of the network stack: interfaces, routes, netfilter rules, promiscuous mode, tunnels. It doesn't hand you the kernel, it hands you everyone's traffic — sniffing and redirecting co-tenants on a shared bridge, and quietly deleting the egress filtering the rest of your design assumes is there. If your security story includes "the sandbox can't reach the metadata service," this capability is the story's ending.
  • CAP_BPF and CAP_PERFMON — split out of CAP_SYS_ADMIN in Linux 5.8, which was framed as a de-privileging win and mostly is. But CAP_BPF is the key to the verifier, one of the most complex and most attacked pieces of code in the kernel, and combined with CAP_NET_ADMIN or CAP_PERFMON it exposes program types that read arbitrary kernel memory by design. Distributions disable unprivileged BPF for a reason; handing the capability back re-opens what they closed.
  • CAP_SYS_RAWIO — /dev/mem, ioperm and iopl, raw SCSI/SG_IO commands to block devices. Read and write physical memory and raw disks beneath the filesystem entirely. File permissions become an opinion.
  • The honourable mentions — CAP_SYS_BOOT (kexec_load: replace the running kernel), CAP_SETUID and CAP_SETGID (become any uid, including ones your files are keyed to), CAP_SETFCAP (write file capabilities onto a binary — an escalation whenever a filesystem is shared with something more privileged), CAP_SYS_CHROOT (chroot is not a jail, and this makes leaving an existing one straightforward), CAP_MKNOD (harmless only because the device cgroup is doing the work).
The practical test for any capability drop-list: does the remaining set contain SYS_ADMIN, SYS_MODULE, SYS_PTRACE, DAC_READ_SEARCH, BPF, or SYS_RAWIO? If yes, you have not reduced privilege in any way an attacker cares about; you have made a document about reducing privilege.

Why capabilities reduce privilege but don't isolate

Suppose you get it perfectly right: --cap-drop=ALL, no capabilities added back, no_new_privs on, non-root uid, read-only rootfs. Your process now holds zero capabilities. What have you actually accomplished?

You've closed the privileged doors and left untouched the enormous surface that needs no capability at all. A process with an empty capability set still calls futex, io_uring, epoll, mmap, sendmsg, ioctl on whatever it can open, and a few hundred more. That's where the interesting kernel bugs live — not in the module loader, which everybody drops, but in the memory-management, filesystem, and networking paths every process must be allowed to drive. Capabilities gate privileged operations; most kernel escalation chains start from an operation that was never privileged.

A container is a polite suggestion to the kernel about how to file your process. Dropping capabilities is a polite suggestion about which of root's powers you'd rather not be offered. Both are enforced by the one component you'd most like to be protected from, and a compromised kernel does not pause to re-read your drop-list.

This is the same shape as seccomp and namespaces and LSMs, and it's worth saying plainly: every one of those mechanisms lowers the probability of a bad outcome and none of them changes the blast radius of one. They're all policy evaluated by the host kernel, about a process running on the host kernel, alongside every other tenant on the host kernel. Stack five of them and you have five probability reductions sharing a single failure mode. The step that changes blast radius is a different kernel.

Dropping capabilities vs a hypervisor boundary, property by property

  • What it restricts — Dropping capabilities: which privileged kernel operations a thread is permitted to request, gated at a few thousand explicit check sites. Hypervisor boundary: which kernel receives the request at all.
  • What it does not restrict — Dropping capabilities: every syscall that needs no capability, which is nearly all of them, and where nearly all kernel exploits actually start. Hypervisor boundary: what the workload does inside its own guest — and correctly so, because that guest is disposable.
  • Where enforcement lives — Dropping capabilities: in the host kernel, the same component the untrusted code is attacking. Hypervisor boundary: in the CPU's virtualisation extensions, with a small host-side VMM reachable only through a hardware VM exit.
  • Failure mode — Dropping capabilities: a memory-corruption bug reached through a permitted, unprivileged syscall makes the capability sets irrelevant, because the code enforcing them is now attacker-controlled. Hypervisor boundary: the guest kernel is compromised, and the attacker is still inside a VM.
  • Blast radius of a kernel bug — Dropping capabilities: the host and every co-tenant sharing it. Hypervisor boundary: one guest, which you were going to delete anyway.
  • Operational cost — Dropping capabilities: essentially zero — a list in a manifest, no runtime overhead, and it composes with everything. Hypervisor boundary: on PandaStack, 179ms p50 and 203ms p99 for a snapshot-restore create, a same-host fork in 400-750ms, and the ~3s cold boot paid once at bake time.
  • Right job for it — Dropping capabilities: every service you run, including your own, as unconditional hygiene. Hypervisor boundary: code you have no reason to trust, running next to other people's.

That last cost row is the one that changes decisions. Teams settle for capability drops plus a seccomp profile because the alternative is imagined as heavy. A fifth of a second for a private kernel makes "a VM per job is too expensive" a claim about 2015, not about this year.

Where capabilities sit among the other layers

Each primitive restricts a different noun, so stack them rather than crown one. Capabilities remove privileged operations. seccomp removes syscalls, privileged or not — a strictly wider cut, which is why a good profile subsumes much of what a drop-list buys and keeps going. Namespaces partition what you can see and name. Landlock, AppArmor, and SELinux constrain which objects you may touch, their checks running after the capability check has already said yes. cgroups v2 caps what you consume. Applied together to your own cooperating code, that's strong, cheap, and well understood.

Firecracker is the cleanest illustration of the intended relationship, and it's the pattern worth copying. The boundary is KVM: guest code talks to a guest kernel and reaches the host only through a hardware trap. Underneath that, the VMM is an ordinary host process, so it runs under a jailer that drops to an unprivileged uid, chroots, applies cgroups and namespaces, and installs a strict seccomp filter — with its capability set emptied on the way in. Capability dropping does real work there. It just does it below the boundary, hardening a small host-side process, instead of being asked to be the boundary.

Where this leaves you

Capabilities are three ideas. One: root's authority is split into about forty bits, held per-thread in five sets — permitted as the reservoir, effective as what's checked right now, inheritable as a handshake that needs the file's cooperation, ambient as the post-4.3 fix that carries privilege into an ordinary binary, and bounding as the one-way ceiling that makes drops permanent. Two: execve recomputes all of it, which is why --cap-drop=ALL works through the bounding set, why a non-root container's --cap-add often silently does nothing, and why file capabilities and no_new_privs are in direct conflict. Three: about a dozen of those bits — SYS_ADMIN first — are root with extra steps, so a drop-list that keeps one has accomplished nothing.

Then the part to carry around. Dropping capabilities is genuinely worth doing: it's free, it composes, it removes whole classes of privileged escape from an attacker's toolkit, and there's no argument for running your services with more of root than they need. It is still privilege reduction on a shared kernel — the syscalls that need no capability remain wide open, they're where the exploits are, and the mechanism enforcing your policy is the mechanism under attack.

So: drop capabilities everywhere, and don't stake untrusted code on having done so. PandaStack runs every sandbox, managed Postgres database, and hosted app as its own KVM-backed Firecracker microVM — the workload gets a real kernel it can be real root in, and when it's finished, the machine, its memory, its filesystem, and anything it left behind stop existing together. Inside that guest, every layer in this post is still worth applying; the boundary that keeps tenants apart has no opinion about what a workload does to its own kernel. The core is open source under Apache-2.0, so you can read exactly which capabilities the agent and jailer drop before a VM starts.

Frequently asked questions

What are the five Linux capability sets?

Permitted is the reservoir of capabilities a thread is allowed to hold; you can raise from it into effective and drop from it irreversibly, but never add at runtime. Effective is the set the kernel actually checks when you attempt a privileged operation. Inheritable survives execve but only intersected with the executed file's inheritable set, so it does nothing on its own. Bounding is a one-way ceiling on everything this thread and its descendants could ever gain, including via setuid binaries and file capabilities. Ambient, added in Linux 4.3, is the set that survives execve of an ordinary unprivileged binary — a capability can only be in it if it is in both permitted and inheritable, and executing any setuid or file-capability binary clears it entirely.

Why does --cap-add not work when my container runs as a non-root user?

Because of the execve rules. When a root process execs a binary with no file capabilities, the kernel treats the file's permitted set as full, so the process keeps whatever its bounding set still allows — that is why capability adds appear to stick for root containers. A non-root process gets no such special case: the new permitted set is computed from the file's capability sets and the ambient set, and with no file capabilities and an empty ambient set that comes out zero. Kubernetes has historically not populated the ambient set, so capabilities.add plus runAsUser often produces a process holding nothing. The durable fixes are to bind a high port behind a proxy, or to bake file capabilities into the image with setcap — noting that no-new-privileges and allowPrivilegeEscalation: false will then disable them.

Is CAP_SYS_ADMIN really as dangerous as root?

For most escape purposes, yes. It is a grab bag that accumulated every privileged operation whose home was unclear, and the manual needs roughly thirty bullet points to enumerate it: mount and pivot_root, setns into other namespaces, quotactl, perf_event_open, sethostname, and a long tail of device ioctls. Mount alone is decisive — the well-known container escape mounts a cgroup v1 hierarchy inside the container and points its release_agent at a program the host then runs as real root. If your container holds CAP_SYS_ADMIN, dropping the other capabilities has not meaningfully reduced what an attacker can do.

Do capabilities make a container a secure sandbox?

No, and this is the distinction that matters. Dropping capabilities removes privileged operations from a process, which is real and worth doing. It does not touch the syscalls that require no capability at all — futex, mmap, io_uring, epoll, ordinary networking and filesystem calls — and that is where the large majority of kernel privilege-escalation bugs are found. Every capability check is also evaluated by the host kernel on behalf of a process running on that same host kernel, shared with every other tenant, so a memory-corruption bug reached through any permitted syscall makes the capability sets irrelevant. Capabilities lower the probability of a bad outcome without changing the blast radius when one happens.

How do capabilities relate to seccomp, namespaces, and LSMs?

They restrict different nouns, so they stack rather than compete. Capabilities gate privileged operations at explicit check sites in the kernel. seccomp filters syscalls whether or not they are privileged, which is a strictly wider cut and covers surface that capabilities do not reach. Namespaces partition what a process can see and name, LSMs like AppArmor, SELinux, and Landlock constrain which objects may be touched with their checks running after the capability check has already passed, and cgroups v2 caps resource consumption. Applying all of them is the correct configuration for your own cooperating software — but all five are enforced by the host kernel, so they share one failure mode; the layer that changes blast radius is giving the workload its own kernel.

Keep reading

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.