Security Modules & Capabilities
Linux security is multi-layered. Understanding these mechanisms is essential for infrastructure engineers building secure container platforms and debugging permission issues. Think of Linux security as a series of checkpoints a request must pass through — each layer can say “no” independently, and the request only succeeds if every layer says “yes.” This defense-in-depth approach means that even if one layer is misconfigured or exploited, the others still provide protection.Key Topics: LSM framework, capabilities, seccomp-bpf, SELinux/AppArmor
Time to Master: 12-14 hours
Linux Security Architecture
Capabilities: Dividing Root Power
Traditional Unix has a binary security model: UID 0 (root) can do everything, everyone else is restricted. This means a web server that needs to bind to port 80 must run as root, getting ALL of root’s power including the ability to load kernel modules, read any file, kill any process, and change the system clock. Capabilities break this “all or nothing” model into approximately 40 distinct privileges.Common Capabilities
Capability Sets
Each process has multiple capability sets that interact during permission checks and acrossexecve() boundaries. This is where capabilities get subtle.
Working with Capabilities
Kernel Capability Checks
Understanding how the kernel checks capabilities helps you debug “permission denied” errors. Every privileged operation in the kernel callscapable() or ns_capable() before proceeding.
Linux Security Modules (LSM)
LSM provides a framework of hooks throughout the kernel for mandatory access control. Unlike DAC (where the file owner controls permissions), MAC policies are set by the administrator and cannot be overridden by users, even root. The key mental model: LSM hooks are checkpoints inserted at every security-relevant kernel operation. Each registered security module gets a chance to say “deny” at each checkpoint.LSM Architecture
LSM Hooks
SELinux
Type Enforcement security — every subject (process) and object (file, socket, etc.) is labeled with a security context. Policy rules define which types can interact with which other types and how. If there is no explicit “allow” rule, the access is denied. This “default deny” model is what makes SELinux so effective and so frustrating.SELinux Context
SELinux Policy Rules
SELinux Modes
SELinux Booleans
Booleans are pre-defined policy switches that enable/disable common configurations without writing custom policy.AppArmor
Profile-based MAC — simpler than SELinux because it uses pathnames rather than labels. Each confined program has a profile that lists exactly which files, capabilities, and network operations it can use. If it is not in the profile, it is denied.AppArmor Profiles
AppArmor Modes
AppArmor in Containers
Seccomp-BPF
Seccomp (Secure Computing) with BPF filters lets you restrict which system calls a process can make. This is your last line of defense against kernel exploits: even if an attacker gets code execution inside your container, they can only invoke the ~300 syscalls that the filter allows, not the ~400+ that the kernel provides.How Seccomp Works
prctl(PR_SET_SECCOMP) and is inherited by all child processes (including across execve()). Once attached, it cannot be removed or weakened — a process can only add more restrictive filters on top. This “no weakening” property is what makes seccomp safe against privilege escalation.
Writing Seccomp Filters
Docker Seccomp Profile
eBPF LSM
Write custom security policies with eBPF programs that attach to LSM hooks. This gives you the flexibility of custom kernel modules without the stability risk — BPF programs are verified for safety before loading.Container Security Stack
Docker Security Options
Kubernetes Pod Security
Debugging Security Issues
Capability Denied
SELinux Denials
AppArmor Denials
Seccomp Violations
Interview Questions
Q: How do you drop privileges in a containerized application?
Q: How do you drop privileges in a containerized application?
- User namespace: Map container UID 0 to unprivileged host UID
- Capabilities: Drop all, add only needed
- No new privileges: Prevent setuid escalation
- Seccomp: Filter dangerous syscalls
- Read-only rootfs: Prevent persistence
- In application code:
Q: What's the difference between SELinux and AppArmor?
Q: What's the difference between SELinux and AppArmor?
- SELinux for high-security environments needing fine-grained control (government, finance)
- AppArmor when simplicity and rapid deployment are preferred
- Both provide strong security when properly configured
Q: How does seccomp-bpf protect containers?
Q: How does seccomp-bpf protect containers?
- Container runtime installs a BPF filter at container start
- Every syscall is checked against the filter before entering the kernel
- Dangerous syscalls are blocked (e.g.,
ptrace,mount,kexec_load)
kexec_load- Replace running kernel (game over if allowed)mount- Mount filesystems (escape container filesystem isolation)ptrace- Debug/trace processes (read other containers’ memory)create_module/init_module- Load kernel modules (arbitrary kernel code)- And ~50 more dangerous syscalls
Q: What is capability-based security and why is it better than root/non-root?
Q: What is capability-based security and why is it better than root/non-root?
- UID 0 = all privileges (approximately 40 distinct powers)
- Regular user = almost no privileges
- Programs needing one privilege (bind port 80) got ALL of root’s power
- A compromised web server running as root could load kernel modules
CAP_NET_BIND_SERVICE- Bind to ports below 1024CAP_SYS_ADMIN- Various admin tasks (too broad, avoid this one)CAP_SYS_MODULE- Load kernel modules- etc.
- Least privilege: Grant only what is needed, nothing more
- Defense in depth: Compromised process has limited blast radius
- Container isolation: Different containers get different capability sets
- Auditable:
getpcapsshows exactly what a process can do
CAP_NET_BIND_SERVICE, not full root:Interview Deep-Dive
A container in your Kubernetes cluster was compromised. Walk through the security layers that should have limited the blast radius, how each layer restricts the attacker, and how you would investigate what happened.
A container in your Kubernetes cluster was compromised. Walk through the security layers that should have limited the blast radius, how each layer restricts the attacker, and how you would investigate what happened.
- I would work through the security layers from outermost to innermost to understand what the attacker could and could not do, then investigate what actually happened.
- Seccomp (layer 1): If the RuntimeDefault seccomp profile was applied, the attacker cannot call
ptrace(cannot debug other containers’ processes),mount(cannot mount the host filesystem),kexec_load(cannot replace the kernel), orinit_module(cannot load kernel modules). This eliminates the most common container escape techniques. I would checkkubectl get pod -o yamlto verify the seccomp profile was actually applied — ifseccompProfileis not set, no seccomp filter was active. - Capabilities (layer 2): If
drop: ALLwas set with only specific capabilities added back, the attacker cannot perform privileged operations even though they may be root inside the container. WithoutCAP_SYS_ADMIN, they cannot callmount()orsetns()to access other namespaces. WithoutCAP_NET_RAW, they cannot sniff network traffic. I would check the pod spec for thecapabilitiessection and cross-reference with the container runtime’s default capability list (Docker grants 14 by default if you do not specify). - Namespace isolation (layer 3): The PID namespace means the attacker sees only their container’s processes. The network namespace means they only see their container’s network stack (though they may be able to reach other pods via the pod network if NetworkPolicies are not in place). The mount namespace means
/procand/sysshow the container’s view, not the host’s. User namespaces (if enabled) mean that root inside the container maps to an unprivileged UID on the host. - LSM (layer 4): SELinux MCS labels (if enabled) prevent the container from accessing files belonging to other containers. Even if the attacker breaks out of mount namespace isolation, the SELinux label mismatch blocks access. AppArmor profiles restrict file access to paths explicitly listed in the profile.
- For investigation: I would start with the audit log (
ausearch -m avcfor SELinux denials,dmesg | grep seccompfor seccomp blocks). These logs tell me what the attacker TRIED to do that was blocked. Then I would examine the container’s filesystem (if not read-only) for dropped tools or modified files.kubectl logsand container runtime logs show the initial compromise vector. For network-level investigation, I would check Cilium/Calico flow logs to see what connections the compromised pod made — did it try to reach the metadata service? The Kubernetes API? Other pods?
privileged: true?Follow-up Answer:- A privileged container effectively disables ALL security layers: no seccomp filter, all capabilities granted (including
CAP_SYS_ADMIN), access to all host devices via/dev, and no LSM confinement. The attacker has essentially root access on the host. They can mount the host filesystem (mount /dev/sda1 /mnt), read any file, load kernel modules, attach to any namespace (nsenter -t 1 -m -u -i -n -p), and compromise every container on the node. This is whyprivileged: trueshould NEVER be used in production. The only legitimate use cases are system-level DaemonSets (CNI plugins, node monitoring agents) that genuinely need host access, and even those should be scrutinized for whether they can use specific capabilities instead.
Explain how Linux capabilities interact with user namespaces in rootless containers. Why can a process be root inside a container but unprivileged on the host, and what are the security boundaries?
Explain how Linux capabilities interact with user namespaces in rootless containers. Why can a process be root inside a container but unprivileged on the host, and what are the security boundaries?
- User namespaces create a mapping between UIDs inside the namespace and UIDs outside. When a process has UID 0 inside a user namespace, it has full capabilities WITHIN that namespace, but those capabilities are scoped to the namespace’s resources only. The kernel checks
ns_capable()which verifies that the process has the capability in the correct namespace for the operation being attempted. - Here is the mechanism: when a rootless container starts, it calls
clone(CLONE_NEWUSER)which creates a new user namespace. The process then writes a UID mapping like0 100000 65536to/proc/self/uid_map, meaning UID 0 inside maps to UID 100000 outside (an unprivileged user), and UIDs 1-65535 inside map to 100001-165535 outside. Inside the namespace, the process has all capabilities in its effective set. It can callmount(), create device nodes, and perform other privileged operations — but ONLY on resources owned by the namespace. - The security boundary is the namespace’s resource scope.
CAP_NET_ADMINinside a user namespace lets you configure the network stack of network namespaces owned by that user namespace, but NOT the host’s network stack.CAP_SYS_ADMINlets you mount filesystems (with restrictions — only certain fs types like tmpfs, proc, sysfs are allowed), but not mount raw block devices.CAP_MKNODis restricted — you can create device files but they will not function for accessing actual hardware because the device cgroup (or device filtering in cgroups v2) prevents it. - The critical invariant: a user namespace cannot grant privileges that its creator did not have in the parent namespace. If the parent process had no capabilities in the parent namespace, the child’s capabilities (even though they are “all” inside the new namespace) cannot affect anything outside the new namespace’s scope. The bounding set in the parent namespace remains the hard ceiling.
- Practical security implications for rootless containers: the container’s “root” can install packages, bind to port 80 inside the container’s network namespace, and manage processes — all without any privilege on the host. If the container is compromised, the attacker has UID 100000 on the host (an unprivileged user) and cannot read
/etc/shadow, load kernel modules, or affect any other container or the host.
- Several operations are either impossible or require workarounds in rootless containers. First, network: rootless containers cannot create veth pairs or configure bridge networking directly because those operations require real
CAP_NET_ADMINin the initial namespace. Rootless Docker uses slirp4netns (userspace TCP/IP stack) or pasta for networking, which adds ~10-20% network latency overhead compared to bridge networking. Second, storage: rootless containers cannot use some storage drivers (devicemapper, native overlay on kernels below 5.11). Overlayfs in a user namespace was only supported starting kernel 5.11 with themetacopyanduserxattrmount options. Third, cgroups v2 delegation: the systemd cgroup driver supports rootless delegation, but cgroups v1 does not. This means rootless containers on cgroups v1 systems cannot set memory limits on sub-containers. Fourth, certain syscalls likemknodfor real devices,mountfor block devices, andsetxattrfor security labels are restricted even inside the user namespace for safety reasons.
You need to implement a seccomp profile for a new microservice. The default Docker profile is too permissive, and you want a minimal allowlist. Walk through your methodology for building and testing a production seccomp profile.
You need to implement a seccomp profile for a new microservice. The default Docker profile is too permissive, and you want a minimal allowlist. Walk through your methodology for building and testing a production seccomp profile.
- I would use a four-phase approach: discover, build, test, and monitor.
- Phase 1 — Discover: Run the application with
SCMP_ACT_LOGas the default action in the seccomp profile. This allows all syscalls but logs every one. Simultaneously, run the application’s full test suite and exercise all code paths (including error paths, graceful shutdown, log rotation, etc.). Collect the syscall audit logs:grep SECCOMP /var/log/audit/audit.log | awk '{print $NF}' | sort -ugives the complete set of syscalls the application uses. Alternatively, usestrace -f -c ./myappfor a summary, or OCI runtime tools likeoci-seccomp-bpf-hookwhich automatically generate seccomp profiles from observed behavior. - Phase 2 — Build: Start with an empty allowlist and add only the syscalls discovered in Phase 1. For syscalls with argument-level sensitivity (like
clone, which should not be called withCLONE_NEWUSERorCLONE_NEWNSflags from inside a container), add argument filters:SCMP_CMP_MASKED_EQto check specific flag bits. For the default action, I preferSCMP_ACT_ERRNO(EPERM)overSCMP_ACT_KILLduring rollout because it returns an error rather than killing the process, making it easier to discover missing syscalls. Switch toSCMP_ACT_KILL_PROCESSonce the profile is validated. - Phase 3 — Test: Deploy the profile in a staging environment with the application under realistic load (not just unit tests — integration tests, performance tests, chaos tests). Monitor for two things: application errors (EPERM in logs indicates a missing syscall in the allowlist) and application functionality (all features work correctly). Run for at least one full application lifecycle including startup, steady state, graceful shutdown, and log rotation. Do not forget to test container restart, OOM recovery, and signal handling.
- Phase 4 — Monitor: In production, switch the default action to
SCMP_ACT_LOGfor the first week, which allows blocked syscalls but logs them. Monitor for unexpected syscall attempts — these could be legitimate code paths you missed in testing or could be actual attack attempts. After one week of clean logs, switch toSCMP_ACT_KILL_PROCESSfor full enforcement. Keep the monitoring active permanently: any new seccomp log entries after enforcement indicate either a bug in the profile (if the application crashes) or an attack attempt (if the application continues normally). - A practical shortcut for most teams: start with Docker’s default profile, which blocks the ~50 most dangerous syscalls. Only build a custom minimal profile for security-critical services or services exposed to untrusted input. The effort of maintaining a minimal profile (updating it with every dependency change) is significant and not always worth the marginal security improvement over the default.
- Dynamic languages are harder to profile because their syscall surface depends on which modules are loaded, which Python C extensions are called, and even which JIT paths the runtime takes. My approach changes in two ways: First, the discovery phase must be longer and more thorough. I would run the application for multiple days in
SCMP_ACT_LOGmode under production-like traffic, not just test traffic, to capture rare code paths. I would also parse the application’s dependency tree to identify C extensions (which make direct syscalls) and research their syscall requirements. Second, I would use a slightly broader allowlist than for a static binary — including syscalls that the runtime might use for garbage collection (madvise,mprotect), JIT compilation (mmapwith PROT_EXEC), and dynamic module loading (openat,mmap). The profile for a Python application might allow 150 syscalls versus 50 for a Go static binary, but it is still significantly smaller than the full ~400 available, eliminating the most dangerous attack surface.
Summary
Next Steps
- Namespaces - Container isolation primitives
- Cgroups - Resource limiting
- eBPF - Custom security with BPF LSM