Skip to content
Menu

Virtualization8 min read

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

DAC — Discretionary Access Control

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.

MAC — Mandatory Access Control

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

Enforce Mode

Violations are denied and logged.

This is the production mode — rules are actively enforced.

bash
aa-enforce /etc/apparmor.d/usr.sbin.nginx
Complain Mode

Violations are logged but allowed.

Used for profiling and testing — lets you see what a program needs before writing a strict profile.

bash
aa-complain /etc/apparmor.d/usr.sbin.nginx

What Profiles Define

Example AppArmor Profile

console
# /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 /proc entries
  • Standard capabilities granted by Docker

Overriding AppArmor in Docker and Kubernetes

Docker

console
# 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-profile

Kubernetes

console
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: nginx

The 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

  1. Type Enforcement (TE) — Container processes run as container_t. SELinux policy defines what container_t can access: container files (container_file_t), container ports, etc. It cannot access etc_t, var_log_t, or other host file types.
  2. Multi-Category Security (MCS) — Each container gets unique category labels (e.g., c1,c2 vs. c3,c4). Even though both containers are container_t, they cannot access each other's files because the categories don't match.
  3. Separation — Container A (s0:c1,c2) cannot read files labeled s0:c3,c4 (Container B). The kernel checks categories at every file access, even for root.
AppArmor
  • 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)
SELinux
  • 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

console
# 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_volume

Warning

Volume mount gotcha: When mounting host directories into containers with SELinux, you often need the :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)
Without relabeling, SELinux denies access even though the file permissions look correct.

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:

Hardware / Host Kernel
Layer 1 — Linux Security Module (AppArmor / SELinux)

MAC policy enforcement — restricts file, network, and capability access even for root

Layer 2 — Seccomp-BPF

Syscall filtering — blocks dangerous kernel interfaces (mount, reboot, kexec)

Layer 3 — Linux Capabilities

Privilege subsetting — splits root into discrete, droppable units

Layer 4 — cgroups (Resource Limits)

Prevents CPU/memory/IO exhaustion — see Virt 03

Layer 5 — Namespaces (View Isolation)

Hides host PIDs, network, mounts, users — see Virt 02

Container Process

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

  1. Process calls syscall

    e.g., open("/etc/shadow")

  2. Seccomp check

    Is open() allowed at all?

  3. Capability check

    Does process have CAP_DAC_OVERRIDE?

  4. DAC check

    Unix file permissions

  5. 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

console
console

    $ cat /sys/kernel/security/lsm

    lockdown,capability,landlock,yama,apparmor
  
console
console

    $ # 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.
  
console
console

    $ # 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
  
Solidnines — solidnines.com