Seccomp, AppArmor, and SELinux
You want to restrict which syscalls a container can make, so that even if it's compromised, it can't do much. Three technologies can do this. Which one, and how?
Namespaces limit what a process can see, cgroups limit what it can use, and capabilities limit what privileged operations it can perform. This lesson adds the last layer of the kernel-level boundary: restricting the process at the syscall and access-control level, so that even a compromised, capable process is boxed in. Three mechanisms do this, and they are complementary rather than competing. The scenario's answer is not "pick one," it is "these operate at different layers and you use them together as defense in depth."
Every interaction a container has with the kernel is a syscall. Namespaces and capabilities decide whether an operation is allowed in principle; seccomp decides which syscalls the process may even attempt, and the LSMs (AppArmor, SELinux) decide which specific objects it may touch. Layering them means a single bypass at one layer still meets another wall.
seccomp: filter the syscalls themselves
seccomp-bpf attaches a BPF program to a process that the kernel consults on every syscall. The program inspects the syscall number (and sometimes arguments) and returns an action: allow, return an error (SCMP_ACT_ERRNO), or kill the process (SCMP_ACT_KILL). It is the narrowest, most direct control: it shrinks the set of kernel entry points the process can reach at all.
Docker and containerd apply a default seccomp profile to every container. It allows the large majority of the roughly 300-plus syscalls an ordinary program needs and blocks a few dozen dangerous or rarely-legitimate ones, for example reboot, mount, swapon, kexec_load, init_module, and old-kernel key-management calls. That default alone removes a meaningful slice of kernel attack surface for free.
# See that the default profile is active (not "unconfined")
docker inspect --format '{{ .HostConfig.SecurityOpt }}' <container>
# The dangerous but educational opposite: turn the filter off
docker run --security-opt seccomp=unconfined ... # do NOT do this in production
# Apply your own tighter profile
docker run --security-opt seccomp=./my-profile.json ...
The point of a custom profile is to go further than the default: allow only the syscalls a specific workload actually issues. Lesson 7.3 covers engineering these profiles in depth.
AppArmor and SELinux: control which objects, not which syscalls
seccomp does not know about files or paths; it only sees syscall numbers. The Linux Security Modules (LSMs) fill that gap by mediating access to specific objects. Two implementations dominate, and a host runs one of them.
- AppArmor is path-based. A profile says "this program may read
/etc/nginx, write/var/log/nginx, and nothing else." Containerd ships adocker-default(orcri-containerd.apparmor.d) profile. This is the default on Debian and Ubuntu. - SELinux is label-based. Every process and every file carries a security label; a policy of type-enforcement rules says which process types may touch which object types. For containers it also uses MCS (Multi-Category Security): the runtime assigns each container a unique category pair, so container A is labeled differently from container B and the kernel forbids A from reading B's files even if the paths line up. This is the default on RHEL, Fedora, and CentOS Stream.
# SELinux: see the container process label and its MCS categories
ps -eZ | grep container
# system_u:system_r:container_t:s0:c123,c456 ... the c123,c456 is this container's unique MCS pair
The practical difference: AppArmor reasons about paths, SELinux reasons about labels. SELinux is stricter and harder to misconfigure into a bypass, but more work to write policy for; AppArmor is easier to read and author. You do not choose between them per container; you get whichever the host runs.
How the layers stack
Two complementary layers: seccomp vs the LSMs
seccomp
Filters syscalls (the kernel entry points)
AppArmor / SELinux (LSM)
Mediates access to specific objects
Defense in depth means these run at the same time as namespaces and capabilities. Suppose an attacker gains code execution in a container and even holds a capability they should not. seccomp may still block the specific syscall they need; if not, the LSM may still deny access to the object they target; if not, the user namespace may still mean their "root" is an unprivileged host UID. Every independent layer is another thing that has to fail before a compromise reaches the host.
Common mistakes
- Running
seccomp=unconfinedto make something work. This removes the syscall filter entirely for a one-syscall problem. Find the blocked syscall and write a profile that allows exactly it. - Assuming seccomp understands files. It does not. If you need to restrict which paths a process reads or writes, that is an LSM job, not seccomp.
- Disabling SELinux because a container had a permission error.
setenforce 0turns off protection host-wide. The right fix is a targeted policy or the correct label, not disabling the whole system. - Thinking you pick one of the three. They operate at different layers. A hardened container uses the default (or a tighter) seccomp profile, the host's LSM, dropped capabilities, and namespaces, all together.
- Blocking syscalls blindly. An over-tight profile breaks the app: block
cloneorfutexand threading dies; block a newer syscall and a fresh glibc build fails at startup. Tighten from real observation, not guesswork.
The interview answer
A seccomp profile reduces attack surface by cutting the number of kernel entry points a process can reach: fewer reachable syscalls means fewer bugs and fewer privileged operations available to an attacker who lands code execution. Good candidates to block are syscalls a normal application never needs but attackers love: mount, reboot, swapon, kexec_load, init_module / finit_module, ptrace, and old key-management calls. The risk is over-blocking: block clone or futex and multithreaded apps break; block a syscall a newer libc started using and the program fails to start. So you do not guess. You determine the real syscall set by tracing the workload (strace, seccomp audit logging, or a seccomp-notify recorder) under representative load, allow that set plus a safety margin, and test before enforcing. That method is the subject of Lesson 7.3.
Summary
seccomp filters the syscalls a process may attempt; AppArmor and SELinux (the LSMs) mediate which objects it may access, by path or by label with MCS isolating containers from each other. Container runtimes apply a default seccomp profile and an LSM profile out of the box, and these stack with namespaces, cgroups, and capabilities as defense in depth: several independent walls, each of which an attacker must defeat. This closes Module 1. You now hold the four primitives, visibility, resources, privilege, and syscall/object confinement, that every later topic (images, runtimes, escape, and runtime security) builds on.
An interviewer asks how you would stop a compromised container from mounting a host filesystem and from reading another container's files. Which combination is correct, and why not just one mechanism?
How does a seccomp profile reduce container attack surface? What syscalls would you block and what breaks if you block the wrong ones?