LSMs, AppArmor & SELinux
Mandatory Access Control — the last line of container defense
Linux Security Modules (LSM) Framework
What is LSM?
LSM is a framework of hooks placed throughout the Linux kernel. When the kernel is about to perform a sensitive operation (opening a file, creating a socket, mounting a filesystem), it calls the registered LSM hooks before proceeding. Security modules plug into these hooks to enforce their policies.
DAC vs. MAC
Traditional Unix file permissions:
- Owner sets permissions (
chmod,chown) - Root bypasses everything
- Users can share files however they want
- No system-wide policy enforcement
If a process runs as root, DAC provides zero protection. Root can read any file, kill any process, bind any port.
System-enforced security policy:
- Administrator defines policy centrally
- Even root is restricted
- Processes cannot override the policy
- Every access is checked against the policy
Even if an attacker gains root inside a container, the MAC policy prevents accessing host files, loading kernel modules, or escaping.
Major LSM Implementations
| LSM | Approach | Default On | Used By |
|---|---|---|---|
| AppArmor | Path-based profiles — each program has rules about which paths it can access | Ubuntu, Debian, SUSE | Docker (default on Ubuntu) |
| SELinux | Label-based policies — every object gets a security label, rules define label interactions | RHEL, CentOS, Fedora | Podman, CRI-O, OpenShift |
| TOMOYO | Path-based, learning mode for profile generation | None (opt-in) | Niche use |
| Smack | Simplified label-based MAC | Tizen OS | Embedded/IoT |
Note
LSM stacking: Since Linux 5.1, multiple minor LSMs can stack alongside one major LSM. Since Linux 6.8+, full major LSM stacking is supported (e.g., AppArmor + SELinux simultaneously, though this is rarely done in practice).
AppArmor
AppArmor uses path-based profiles — each program has a profile that defines exactly which files, capabilities, and network operations it can use. If a program tries something not in its profile, the access is denied.
Profile Modes
Violations are denied and logged.
This is the production mode — rules are actively enforced.
aa-enforce /etc/apparmor.d/usr.sbin.nginxViolations are logged but allowed.
Used for profiling and testing — lets you see what a program needs before writing a strict profile.
aa-complain /etc/apparmor.d/usr.sbin.nginxWhat Profiles Define
Example AppArmor Profile
# /etc/apparmor.d/usr.sbin.nginx
#include <tunables/global>
/usr/sbin/nginx {
#include <abstractions/base>
#include <abstractions/nameservice>
## File access rules
/etc/nginx/** r, # read config files
/var/log/nginx/** rw, # read/write logs
/var/www/html/** r, # read web content
/run/nginx.pid rw, # PID file
/usr/sbin/nginx mr, # read + mmap self
## Capability rules
capability net_bind_service, # bind port 80/443
capability setuid, # drop privileges after start
capability setgid,
capability dac_override,
## Network rules
network inet stream, # TCP over IPv4
network inet6 stream, # TCP over IPv6
## Deny everything else implicitly
deny /proc/sysrq-trigger rwklx, # explicit deny + log
deny /proc/kcore rwklx,
}| Rule Type | Syntax | What It Controls |
|---|---|---|
| File access | /path r, /path rw, /path/** rix |
Read, write, execute, memory-map, link — per file/directory |
| Capabilities | capability net_bind_service |
Which Linux capabilities the process can use |
| Network | network inet stream |
Which address families and socket types are allowed |
| Mount | mount options=(ro) /dev/sda -> /mnt |
Which mount operations are permitted |
| Signal | signal send set=(term) peer=other_profile |
Which signals can be sent to which processes |
| Deny | deny /path rwklx |
Explicit deny — overrides any allow rule, always logged |
Note
File permission codes: r = read, w = write, x = execute, m = memory map executable, k = lock, l = link, ix = inherit execute (child inherits profile), px = profile execute (child gets its own profile).
Docker's docker-default AppArmor Profile
When Docker runs on a system with AppArmor enabled (Ubuntu, Debian), it automatically applies the docker-default profile to every container. This profile:
Denies
- Writing to
/proc/sysrq-trigger(magic SysRq — can reboot, kill all processes) - Reading
/proc/kcore(physical memory dump) - Writing to
/proc/acpi/alarm - Accessing
/sys/firmware/** - Mounting any filesystem
- Modifying
/proc/sys/**(kernel tunables) - Accessing
/sys/kernel/security/**
Allows
- All normal file operations within the container's filesystem
- Network operations (container networking)
- Signal sending within the container's PID namespace
- Reading most
/procentries - Standard capabilities granted by Docker
Overriding AppArmor in Docker and Kubernetes
Docker
# Disable AppArmor for a container
docker run \
--security-opt apparmor=unconfined \
nginx
# Use a custom profile
docker run \
--security-opt apparmor=my-custom-profile \
nginx
# Load a profile first
apparmor_parser -r -W /etc/apparmor.d/my-custom-profileKubernetes
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
annotations:
# Per-container annotation
container.apparmor.security
.beta.kubernetes.io/web:
localhost/my-custom-profile
spec:
containers:
- name: web
image: nginxThe profile must be loaded on the node where the pod runs. K8s does not distribute profiles — use a DaemonSet or init system to load them.
SELinux
SELinux uses a label-based approach. Every process, file, port, and socket gets a security label. Policies define which label interactions are permitted.
SELinux Labels
Label Format: user:role:type:level
system_u:system_r:container_t:s0:c1,c2 ↓
User
system_u
SELinux user identity (not Unix user)
Role
system_r
Determines which types/domains are accessible
Type
container_t
Main enforcement mechanism — Type Enforcement (TE)
Level
s0:c1,c2
MCS/MLS — sensitivity + categories
How SELinux Protects Containers
- Type Enforcement (TE) — Container processes run as
container_t. SELinux policy defines whatcontainer_tcan access: container files (container_file_t), container ports, etc. It cannot accessetc_t,var_log_t, or other host file types. - Multi-Category Security (MCS) — Each container gets unique category labels (e.g.,
c1,c2vs.c3,c4). Even though both containers arecontainer_t, they cannot access each other's files because the categories don't match. - Separation — Container A (
s0:c1,c2) cannot read files labeleds0:c3,c4(Container B). The kernel checks categories at every file access, even for root.
- Path-based — rules reference file paths
- Easier to write and understand
- Profile per program
- No relabeling needed
- Default on: Ubuntu, Debian, SUSE
- Used by Docker by default (on Ubuntu)
- Label-based — rules reference security labels
- More granular, harder to learn
- System-wide policy for all processes
- Files must be labeled correctly
- Default on: RHEL, CentOS, Fedora
- Used by Podman, CRI-O, OpenShift
SELinux in Docker
Managing SELinux for containers
# Disable SELinux for a container (not recommended)
docker run --security-opt label=disable nginx
# Set a custom SELinux label
docker run --security-opt label=type:custom_t nginx
# Check container labels
ps -eZ | grep container
system_u:system_r:container_t:s0:c100,c200 12345 ? 00:00:01 nginx
# Check file labels
ls -Z /var/lib/docker/volumes/
system_u:object_r:container_file_t:s0:c100,c200 my_volumeWarning
:z or :Z suffix to relabel the content:
-v /data:/data:z— shared label (multiple containers can access)-v /data:/data:Z— private label (only this container)
Defense in Depth — The Container Security Boundary
No single mechanism provides container security. The boundary is built from multiple overlapping layers, each catching what others miss:
MAC policy enforcement — restricts file, network, and capability access even for root
Syscall filtering — blocks dangerous kernel interfaces (mount, reboot, kexec)
Privilege subsetting — splits root into discrete, droppable units
Prevents CPU/memory/IO exhaustion — see Virt 03
Hides host PIDs, network, mounts, users — see Virt 02
Isolated process running here
Must pass through ALL layers to reach the host
Warning
Critical limitation: No single layer is sufficient. Containers share the host kernel — a kernel exploit bypasses ALL these layers. Every layer above is enforced by the kernel. If an attacker can execute arbitrary code in kernel space, namespaces, cgroups, capabilities, seccomp, and LSMs are all irrelevant. For stronger isolation, see sandboxed runtimes (gVisor, Kata Containers) in Virt 06.
Container Security Layers — Summary
| Layer | Mechanism | What It Does | If Bypassed |
|---|---|---|---|
| MAC | AppArmor / SELinux | Restricts file access, network operations, and capability usage — even for root | Attacker can access any host file, load kernel modules, modify system configuration |
| Syscall Filter | Seccomp-BPF | Blocks dangerous syscalls — prevents mount, reboot, kexec, module loading | Attacker can invoke any kernel interface, exploit kernel vulnerabilities via obscure syscalls |
| Capabilities | Linux capabilities | Limits root powers — each container gets only the capabilities it needs | Attacker can escalate to full root — mount, ptrace, load modules, configure networking |
| Resources | cgroups | Prevents resource exhaustion — CPU, memory, I/O, PIDs limits | Attacker can DoS the host — consume all memory, fork bomb, saturate I/O |
| View | Namespaces | Hides host resources — separate PID, network, mount, user, UTS, IPC views | Attacker can see and interact with host processes, network stack, and filesystem |
How the Layers Interact
Process calls syscall
e.g., open("/etc/shadow")
Seccomp check
Is open() allowed at all?
Capability check
Does process have CAP_DAC_OVERRIDE?
DAC check
Unix file permissions
LSM check
AppArmor/SELinux policy allows?
Note
Order matters: Seccomp runs first (in the syscall entry path). Then the kernel checks capabilities and DAC. Finally, LSM hooks fire last. A request must pass all checks — any single "deny" blocks the operation.
Terminal — Checking Active LSM
$ cat /sys/kernel/security/lsm
lockdown,capability,landlock,yama,apparmor
$ # Check AppArmor status
$ aa-status
apparmor module is loaded.
54 profiles are loaded.
42 profiles are in enforce mode.
docker-default
/usr/sbin/nginx
...
12 profiles are in complain mode.
3 processes have profiles defined.
3 processes are in enforce mode.
$ # Check SELinux status (on RHEL/CentOS)
$ getenforce
Enforcing
$ sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
Policy MLS status: enabled
Policy deny_unknown status: allowed
Memory protection checking: actual (secure)
Max kernel policy version: 33