Linux LSM and kernel security modules
TL;DR: Linux Security Modules (LSM) is the kernel framework that lets policies hook into syscall and object-access decisions to enforce Mandatory Access Control on top of the DAC model. SELinux, AppArmor, Smack, TOMOYO, Yama, Lockdown, and the newer BPF LSM are the major implementations. Operators usually meet LSM the first time something breaks (“permission denied” with
root), and attackers meet it when a container escape lands in a context that still blocksmountorptrace. Read alongside capabilities-privesc, namespaces-and-cgroups, container-runtime-escapes-modern, and kernel-exploits-linux.
Why it matters
Traditional Unix permissions (DAC) answer “does this UID own this inode?”. That model collapses the moment a process runs as root or is granted a broad capability. LSM is the kernel’s second opinion: even if DAC says yes, a loaded security module can still say no based on labels, paths, or policy state.
Three reasons it matters for both attack and defense:
- Containment of root. A compromised web server or a container that escaped its namespace is often still confined by SELinux
httpd_tor by an AppArmordocker-defaultprofile. Many CVEs are “code execution as root” that fail to convert into “host root” because LSM blocked the next step. - Detection surface. SELinux AVC denials and AppArmor
DENIEDevents are extremely high-signal log entries. They show up inauditdand feed siem-detection-use-case-catalog use cases for “policy violations on hardened workloads”. - Compliance leverage. PCI, FedRAMP, and most government baselines treat enforcing SELinux/AppArmor as a control. See pci-dss-4-implementation and the hardening notes referenced by secure-sdlc-rollout-playbook.
Architecture: the LSM framework
Hooks, not policies
LSM itself is not a policy. It is a set of ~200 hook points scattered through the kernel — security_inode_permission, security_bprm_check, security_socket_connect, security_file_open, etc. Each hook is called after the standard DAC check passes and before the kernel performs the action. A registered module returns 0 (allow) or -EACCES / -EPERM (deny).
This is why LSM does not weaken existing security. If DAC says no, the hook is never reached. LSM only further restricts.
Stacking
For years LSM only allowed one “major” module at a time (SELinux or AppArmor). Modern kernels (5.x+) support stacking: minor modules like Yama, Lockdown, and BPF LSM run alongside a major module. Boot parameter lsm= (or legacy security=) controls load order.
1
2
cat /sys/kernel/security/lsm
# Example: capability,yama,apparmor,bpf
Where it sits in a syscall
A simplified open("/etc/shadow", O_RDONLY) from a confined process:
- VFS resolves the path.
- DAC check (
inode_permission) — owner/group/mode. - LSM hook
security_inode_permission— SELinux compares subject context (httpd_t) and object context (shadow_t) against policy. - LSM hook
security_file_open. - If all return 0, the file descriptor is returned.
The same syscall may pass through 5–10 hooks. This is also why LSM-bypass kernel bugs are valuable: skipping the hook (e.g., via a kernel info leak that lets you patch the hook list, or a vulnerable path that does direct VFS access) gives you root regardless of policy.
Classes / patterns / process
Label-based: SELinux and Smack
SELinux assigns a security context of the form user:role:type:level to every process, file, socket, and IPC object. The kernel decides access by checking type enforcement (TE) rules between the subject type and the object type, plus optional MLS levels.
- Strengths: extremely expressive, can model multi-tenant separation, ships with type-enforced reference policies for hundreds of services on RHEL/Fedora.
- Weaknesses: policy authoring is hard; most operators only ever toggle booleans and run
audit2allowon denials.
Common operator commands:
1
2
3
4
5
6
7
getenforce # Enforcing | Permissive | Disabled
sestatus
ls -Z /etc/shadow # see object label
ps -eZ | grep nginx # see subject label
ausearch -m AVC -ts recent
audit2allow -a -M mymodule
semodule -i mymodule.pp
Smack (used heavily in Tizen and some embedded) is a simplified label model with the same hook integration.
Path-based: AppArmor and TOMOYO
AppArmor matches policy against the pathname of the file at access time. Profiles live in /etc/apparmor.d/ and look like:
1
2
3
4
5
6
7
8
9
profile docker-default flags=(attach_disconnected,mediate_deleted) {
network,
capability,
file,
deny @{PROC}/* w,
deny @{PROC}/sys/[^k]** wklx,
deny mount,
...
}
- Strengths: human-readable, easy to author, ships as the default on Ubuntu/Debian and underpins
docker-default,snap, andlxdconfinement. - Weaknesses: path-based mediation can be evaded with bind mounts, hardlinks, or
/proc/self/roottricks if the profile is not carefully written. Smaller policy surface than SELinux.
TOMOYO is conceptually similar but with a learning-mode workflow that records syscalls and proposes a path-based policy.
Minor / specialised modules
- Yama — restricts
ptrace./proc/sys/kernel/yama/ptrace_scopeof 1, 2, or 3 blocks attaching to arbitrary processes. This is what breaksgdb -pon hardened hosts and what hampers credential-dumping post-exploitation. - Lockdown — restricts even
rootfrom operations that would compromise kernel integrity: loading unsigned modules, writing/dev/mem, kexec of unsigned kernels, certain BPF features. Modes arenone,integrity,confidentiality. Tied to UEFI Secure Boot on most distros. - BPF LSM — load eBPF programs as LSM hooks. Enables fast iteration: ship a small policy update without recompiling a reference policy. Used by Cilium Tetragon, KubeArmor, and many edr-rules-as-code-from-attack-patterns pipelines. Pairs naturally with detection-engineering-pyramid-of-pain because it lets you express behavioural rules in code.
- IMA / EVM — measurement and signing of file content; often considered LSM-adjacent. Useful for boot integrity and tamper detection.
Common bypass classes
Attackers think about LSM bypass in roughly these buckets:
- Mislabeled object. A file written by an unconfined administrator into a directory owned by a confined service ends up with the wrong label or no profile match. SELinux
restoreconexists because this happens constantly. Attacker reads or writes a sensitive file the policy was supposed to protect. - Profile gap on path-based policy. AppArmor profile lists allowed paths; attacker symlinks, bind-mounts, or uses
/proc/<pid>/root/to access the same inode through a non-matching path. - Confined context that still has dangerous capability. Container escapes (see container-runtime-escapes-modern) where the breakout lands in a host context that LSM still partially confines but allows
CAP_SYS_ADMINormount— often enough to pivot. - Kernel bug bypassing the hook. Use-after-free or info-leak that lets the attacker patch the
security_hook_headslist or call into a function below the hook layer. See kernel-exploits-linux and linux-kernel-pwn-walkthrough. - Disabling via boot. If GRUB is unprotected and the host reboots,
selinux=0 apparmor=0 lockdown=noneon the kernel cmdline turns it off. Physical / IPMI access becomes LSM-relevant. unconfined_t/unconfinedprofile. Many SELinux policies leave admin shells unconfined. Land there and policy enforcement is effectively skipped for that subject.
Container runtimes and LSM
runc / containerd / crun apply both SELinux and AppArmor contexts when launching containers:
- Docker on Ubuntu loads
docker-defaultAppArmor profile by default; override with--security-opt apparmor=unconfined(often abused in misconfigured environments). - Podman on RHEL applies
container_tSELinux context; objects inside getcontainer_file_t. Mounting host paths without:zor:Zkeeps host labels, which is why-v /:/hostoften “just works” for an attacker but a properly labeled mount blocks the read. - Kubernetes Pod Security:
securityContext.seLinuxOptionsandseccompProfile+appArmorProfileannotations choose the profile per pod. Tie back to cloud-ir-k8s-audit-logs for auditing what was actually applied.
Defensive baseline
A pragmatic LSM posture for a security team:
- Enforcing, not permissive.
getenforceshould printEnforcing(SELinux) oraa-statusshould show profiles in enforce mode. Permissive logs denials but allows the action — useful for tuning, dangerous as a permanent state. - Audit AVC denials into the SIEM. Forward
/var/log/audit/audit.log(SELinux) andkern.log/audit.log(AppArmor) into the central pipeline. Build use cases per siem-detection-use-case-catalog: alert on denials from production workloads that should never trip the policy. - Don’t paper over with
setenforce 0. This is the SRE / sysadmin antipattern that turns a hardened host into a soft target. Track everysetenforce 0and everyaa-disableas a security event. - Protect the bootloader. GRUB password + Secure Boot + Lockdown mode
integrityorconfidentiality. Without this the kernel cmdline can disable LSM. - Treat
unconfined_tas privileged. Only emergency admin shells should land there, and they should be monitored likesudo. - Container profiles. Ship a non-
unconfinedAppArmor / SELinux profile for every workload; ban--privilegedand--security-opt apparmor=unconfinedin admission policy. Cross-reference appsec-maturity-checklist. - Use BPF LSM for what reference policies can’t express. Behavioural rules like “no process in container
foomay exec/usr/bin/curl” map cleanly to BPF LSM and are version-controlled like code.
Workflow to study
- Spin a Fedora and an Ubuntu VM in the lab from building-a-research-home-lab.
- On Fedora: read
sestatus, list types withseinfo -t, dump policy withsesearch --allow -s httpd_t. Trigger a denial (curl file:///etc/shadowashttpd_t) and walk throughausearch -m AVCandaudit2allow -a. - On Ubuntu:
aa-status, read/etc/apparmor.d/usr.bin.firefox, write a tiny custom profile withaa-genprof, switch betweenaa-complainandaa-enforce. - Install
bpftooland Tetragon or a small custom BPF LSM program; hookbprm_check_securityand log every exec. - Read the kernel source:
security/security.c,include/linux/lsm_hooks.h, and one module (security/apparmor/lsm.cis the most readable). Pair with kernel-syscall-source-review. - Read a real bypass writeup (CVE-2022-2588, CVE-2023-32233) and trace which hook would have caught it and why it didn’t. Feed back into kernel-exploits-linux.
- Exercise: take a Kubernetes pod, write a SecurityContextConstraint / PodSecurity policy that pins SELinux type and AppArmor profile, then try to break out. Pair with container-runtime-escapes-modern.
Related
- capabilities-privesc
- namespaces-and-cgroups
- kernel-exploits-linux
- container-runtime-escapes-modern
- linux-kernel-architecture
- kernel-syscall-source-review
- ld-preload-abuse
- edr-rules-as-code-from-attack-patterns
- siem-detection-use-case-catalog
- cloud-ir-k8s-audit-logs
- appsec-maturity-checklist
References
- Linux kernel docs, LSM framework: https://www.kernel.org/doc/html/latest/security/lsm.html
- SELinux Project wiki and policy guide: https://github.com/SELinuxProject/selinux-notebook
- AppArmor upstream documentation: https://gitlab.com/apparmor/apparmor/-/wikis/Documentation
- BPF LSM design and usage: https://docs.kernel.org/bpf/prog_lsm.html
- Kernel Lockdown mode overview: https://man7.org/linux/man-pages/man7/kernel_lockdown.7.html
- NSA / Red Hat SELinux deployment guidance: https://www.redhat.com/en/topics/linux/what-is-selinux