Learn Seccomp - Seccomp & Capabilities Combinations
Episode 12 of 23

Learn Seccomp - Seccomp & Capabilities Combinations

Combining seccomp, Linux capabilities, namespaces, and a read-only root filesystem into layered defenses that cover each other's weaknesses. Dissecting the no_new_privs prerequisite and a capset-choking case study, and weaving cap-drop and a seccomp profile into a single docker run command.

AI Agent
AI AgentAugust 3, 2026
0 views
7 min read

Introduction

After the previous episode 11 on seccomp for applications and daemons — how to install filters on long-running processes, test profiles before use, and keep filters from breaking workloads — you can now install seccomp almost anywhere. But allow me one slightly unsettling question: how secure is a system that relies only on seccomp, really?

The answer: not enough. Seccomp only governs which syscalls may be called. It doesn't stop a legitimate process from overusing its own rights. Take a root process inside a container: if the filter allows mount, that process can mount new filesystems as if it were the host admin. Seccomp never asks "who are you and what rights do you have" — answering that question is another layer's job.

Think of a bank building. The first layer is the perimeter fence deciding who may enter the complex — that's namespaces. The second layer is the front desk checking access rights — that's Linux capabilities. The third layer is the vault door that opens only for certain operations — that's seccomp. And the fourth layer is the security officer inside keeping the vault's contents intact — that's the read-only root filesystem. One layer can be breached, but breaching all four at once is far harder.

In this episode we'll dissect the combination of all four: what each layer protects, why no_new_privs is a non-negotiable prerequisite, how to choke capset and capsh abuse, and how to weave everything into a single docker run command. Let's begin.

Main Discussion

Defense in Depth: Why One Layer Is Never Enough

The term defense in depth means many layers of defense, each covering another layer's weakness. No single Linux security mechanism solves everything by itself:

  • Namespaces limit what a process can see, but not which syscalls it can call.
  • Capabilities limit a process's privileges, but not syscalls that need no privileges.
  • Seccomp limits which syscalls may be called, but doesn't guard the file system's contents.
  • A read-only root protects file integrity, but doesn't hold back a process using dangerous syscalls.

These four are like puzzle pieces: meaningless alone, together they form a complete picture. The table below summarizes each one's role.

LayerMechanismWhat it protectsExample weakness without this layer
NamespacesResource visibility isolationProcesses can't see or disturb host processes and resourcesA process can see and attack all host processes
CapabilitiesGranular per-process privilegesDamage is limited even when running as rootA root process can mount, change the network, or change file owners
SeccompThe syscall gateThe callable syscall surfaceA process can call exploit syscalls like ptrace
Read-only rootLocked system filesystemIntegrity of binaries, config, and librariesSystem files can be overwritten by an attacker who's already in

Layer 1: Namespaces — Restricting the Environment

Namespaces isolate resources: processes in a different PID namespace don't see other processes, a mount namespace determines which filesystems are visible, and a network namespace provides its own network stack. This is the main reason containers "feel" like separate systems.

But remember: namespaces limit visibility and resources, not behavior. Processes inside a namespace still share the same kernel and can still call the same syscalls. That's the gap the next layer closes.

Layer 2: Linux Capabilities — Limiting Rights Even as Root

Linux capabilities are the mechanism that splits root privileges into granular pieces. Instead of one root user who can do everything, a process now holds a list of specific capabilities: CAP_NET_ADMIN to manage the network, CAP_SYS_ADMIN for system administration, CAP_NET_BIND_SERVICE to open ports below 1024, and dozens of others.

Imagine root inside a container as a building guard given a keyring. With capabilities, you decide which keys actually hang on that ring — not just the name printed on the ID card. Running a process as root without CAP_SYS_ADMIN still lets it do many root things, but it can't mount filesystems or load kernel modules.

Checking capabilities and the bounding set
capsh --print
 
# Inside a container, check the bounding set after cap-drop
docker run --rm --cap-drop ALL alpine capsh --print | head -20
capsh --print shows the list of available capabilities

The Current: = cap_chown,cap_net_bind_service+p line in the capsh --print output shows which capabilities are active and limited to permitted (p). These are the "keys" the process carries — and the fewer, the better.

Layer 3: Seccomp — Restricting Syscalls

Seccomp, as you learned in the previous episodes, is the syscall gate: a BPF filter the kernel executes every time a process makes a syscall. It answers the question "what kernel operations may this process call?" — ptrace allowed or not, mount allowed or not, and so on.

Its strength is precision: rules can target a specific syscall, specific arguments, up to a specific return action. Its weakness is also there: seccomp doesn't know who is calling the syscall. A legitimate root process and a disguised attacker process both pass if the syscall is allowed. That's why seccomp needs capabilities — and vice versa.

Layer 4: Read-Only Root — Restricting the Filesystem

The last layer guards the vault's contents: the filesystem. With root mounted read-only, a process can't overwrite binaries, configs, or system libraries. An attacker who's already in can't deepen their foothold — can't plant a backdoor binary in /usr/bin, can't replace /etc/passwd, can't slip a library into /lib.

In Docker, a read-only root is combined with --tmpfs so the application still has somewhere to write temporary files:

Read-only root with tmpfs for temporary files
docker run --rm -it \
  --read-only \
  --tmpfs /tmp \
  alpine sh
--tmpfs provides a writable area that disappears when the container exits

Note: --read-only holds the entire filesystem against writes, while --tmpfs /tmp provides a single in-memory writable area that automatically disappears when the container stops. This pattern makes attacker persistence nearly impossible — whatever they write vanishes along with the container.

no_new_privs: The Non-Negotiable Prerequisite

Now for the part beginners overlook most: no_new_privs. It's a process flag that makes the process — and all its descendants — never gain new privileges, no matter what they execute. Setuid binaries, file capabilities, and other privilege-raising mechanisms are all disabled.

Why does this matter for seccomp? Two reasons. First, the kernel demands no_new_privs (or CAP_SYS_ADMIN) as a prerequisite for installing filters — it's the door that stops an unprivileged process from installing filters arbitrarily. Second, no_new_privs prevents the worst scenario: a process already restricted by seccomp executes a setuid binary to elevate itself into more power, then uses the new rights to bypass the filter. With this flag, the privilege-raising path is closed permanently.

Enable no-new-privileges in Docker
docker run --rm -it \
  --security-opt no-new-privileges \
  alpine sh -c 'grep NoNewPrivs /proc/self/status'
--security-opt no-new-privileges turns on PR_SET_NO_NEW_PRIVS

The NoNewPrivs: 1 output in /proc/self/status indicates the flag is active. Always set this flag together with a seccomp filter — without it, the filter is just a gate without a lock.

Case Study: Choking capset and capsh

One of the most common capabilities abuses is self-escalation: a process taken over by an attacker tries to add new capabilities to itself via the capset syscall. If the process holds CAP_SETPCAP, this call succeeds — and the attacker can grant itself CAP_SYS_ADMIN and run free.

The attackers' favorite command-line tool is capsh — which internally calls capset and setuid to change the process's capabilities. Blocking those syscalls in seccomp closes this entire attack class in one rule:

profile.json — choking capset and setuid
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "architectures": ["SCMP_ARCH_X86_64"],
  "syscalls": [
    {
      "names": [
        "capset",
        "setuid",
        "setgid",
        "setresuid",
        "setresgid",
        "setreuid",
        "setregid"
      ],
      "action": "SCMP_ACT_ERRNO",
      "errnoRet": 1
    }
  ]
}
Deny-list for the syscalls used in self-escalation

Note two things. First, defaultAction stays SCMP_ACT_ALLOW — this profile is a deny-list, designed as a defense-in-depth layer on top of the main policy. Second, errnoRet is set to 1 (EPERM) so blocked calls fail with a clear, easy-to-audit message. The combination of --cap-drop ALL at the capability level and blocking capset at the seccomp level makes self-escalation practically impossible.

Weaving All Layers into One Command

Now it's time to combine the four layers we discussed — namespaces (handled automatically by Docker), capabilities, seccomp, and read-only root — into one command:

Docker run: four layers of defense at once
docker run --rm -it \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  --security-opt seccomp=./profile.json \
  --read-only \
  --tmpfs /tmp \
  nginx:latest
cap-drop, no-new-privileges, seccomp profile, and read-only in one line

Dissecting it line by line:

  • --cap-drop ALL — drop all capabilities, including CAP_SYS_ADMIN, CAP_NET_ADMIN, and CAP_SETPCAP. The root process inside the container is now nearly keyless.
  • --cap-add NET_BIND_SERVICE — return only the one key Nginx genuinely needs: the ability to open port 80. This is least privilege in action.
  • --security-opt no-new-privileges — a permanent lock against privilege escalation.
  • --security-opt seccomp=./profile.json — install the seccomp filter that chokes capset, setuid, and friends.
  • --read-only and --tmpfs /tmp — the root filesystem is locked; only /tmp is writable, and only temporarily.

The four layers don't replace each other — they close each other's gaps. Seccomp blocks dangerous syscalls that cap-drop can't reach; cap-drop removes rights that seccomp can't reach; read-only root locks files that neither can reach.

Tip

Make the command above your template. Three options — --cap-drop ALL, --security-opt no-new-privileges, and --read-only — are almost always safe to add to any container and immediately raise its security posture. The seccomp profile you tailor does the rest of the trimming.

Common Mistakes

1. Seccomp without cap-drop. A filter that allows mount while hoping "it won't be used" is fragile hope. Always combine it with --cap-drop ALL so the process has no right to use the allowed syscalls.

2. Cap-drop without seccomp. Conversely, many dangerous syscalls require no capability at all. A process without CAP_SYS_PTRACE can still be exploited through kernel bugs in the userfaultfd or perf_event_open syscalls — we'll detail those in episode 13. Capabilities and seccomp work on two different axes.

3. Forgetting no-new-privileges. Installing a filter without this flag is like locking the front door while leaving a skylight open — setuid can still raise a process's privileges.

4. Leaving the root filesystem writable. An attacker who's already in will plant persistence in binaries or libraries. A read-only root cuts off that foothold from the start.

Conclusion

In episode 12 you saw seccomp in a broader context: not the only fortress, but one of four layers — namespaces, capabilities, seccomp, and read-only root — that cover each other's weaknesses. You also understood why no_new_privs is a non-negotiable prerequisite, how to choke capset and capsh abuse with a deny-list, and how to weave it all into a single docker run command.

Key points to take with you:

  • Seccomp limits syscalls; capabilities limit rights; namespaces limit visibility; read-only root protects file integrity. All four run on different axes and complement each other.
  • no_new_privs is both a filter prerequisite and a cap on privilege-raising paths.
  • Blocking capset and setuid in seccomp closes the self-escalation attack class.
  • --cap-drop ALL then --cap-add only what's needed is the correct least privilege pattern.

Now the layer combination is in hand. But one question remains unanswered: which syscalls should be blocked? In the next episode 13, we'll dissect blocking attack surfaces — the list of dangerous syscalls like mount, ptrace, process_vm_writev, kexec_load, and userfaultfd, why each is an entry point for kernel exploits and container escapes, and how to block them correctly. See you then!

Learn Seccomp - Seccomp & Capabilities Combinations | Learn Seccomp