SELinux and AppArmor: Mandatory Access Control
Traditional Unix permissions (user, group, other) provide discretionary access control (DAC) -- the file owner decides who can access a file, and root can override everything. Mandatory access control (MAC) adds a layer enforced by the kernel itself that restricts what processes can do regardless of what DAC permits. Even root-owned processes are confined. On Linux, the two main MAC frameworks are SELinux and AppArmor. This guide covers both in enough detail to configure, troubleshoot, and choose between them.
Hub: Linux Security Hardening | See also: Audit & Intrusion Detection, Server Checklist
Why MAC Matters
Consider a web server running as www-data. With DAC alone, a compromised
web server process can read any file that www-data has permission to read,
write to any directory it owns, open network connections to arbitrary hosts,
and potentially escalate privileges through SUID binaries or kernel exploits.
MAC confines the web server to only the files, directories, ports, and system
calls defined in its security policy, regardless of Unix file permissions.
Even if an attacker achieves remote code execution through an application
vulnerability, MAC prevents the compromised process from reading
/etc/shadow, writing to /etc/cron.d/, or connecting to the database on a
port the policy does not allow. This is the principle of least privilege
enforced at the kernel level.
SELinux
SELinux (Security-Enhanced Linux) was developed by the NSA and is the default MAC on Red Hat, CentOS, Rocky Linux, AlmaLinux, Fedora, and their derivatives. It labels every process, file, port, and user with a security context (also called a label) and enforces a policy that defines which contexts can interact with which other contexts.
SELinux Modes
SELinux operates in three modes:
- Enforcing -- policy violations are blocked and logged.
- Permissive -- policy violations are logged but not blocked (useful for testing and policy development).
- Disabled -- SELinux is completely off (strongly discouraged).
# Check the current mode
getenforce
# Returns: Enforcing, Permissive, or Disabled
# Temporarily switch to permissive (takes effect immediately, resets on reboot)
sudo setenforce 0
# Switch back to enforcing
sudo setenforce 1
# Check detailed SELinux status
sestatus
The permanent mode is set in /etc/selinux/config:
# /etc/selinux/config
SELINUX=enforcing
SELINUXTYPE=targeted
Never change from enforcing to disabled without understanding the
consequences. Re-enabling SELinux after disabling it requires a full
filesystem relabel (fixfiles -F onboot followed by a reboot), which can take
a very long time on large filesystems and may cause services to fail if
contexts are wrong.
Managing File Contexts with semanage and restorecon
Every file on an SELinux system has a security context. When files are created in the wrong location or copied (rather than moved), they may end up with incorrect contexts, causing services to fail:
# View file contexts
ls -Z /var/www/html/
# Output: system_u:object_r:httpd_sys_content_t:s0 index.html
# Assign the correct context for a custom web document root
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?"
# Apply the context to existing files recursively
sudo restorecon -Rv /srv/www
# Check what context a path should have according to policy
matchpathcon /srv/www/index.html
# Restore contexts for the entire filesystem (after major changes)
sudo restorecon -Rv /
Managing Port Contexts
SELinux labels network ports. If you run a service on a non-standard port, you must register that port in the policy:
# List all ports allowed for the httpd_t domain
sudo semanage port -l | grep http
# Allow Apache/Nginx to listen on port 8080
sudo semanage port -a -t http_port_t -p tcp 8080
# Allow a custom application on port 9090
sudo semanage port -a -t my_app_port_t -p tcp 9090
# Remove a custom port assignment
sudo semanage port -d -t http_port_t -p tcp 8080
Troubleshooting with audit2allow and sealert
When SELinux blocks an action, it logs an AVC (Access Vector Cache) denial in
the audit log. The tools audit2allow and sealert help you diagnose the
denial and create a policy to fix it:
# Show all recent AVC denials
sudo ausearch -m AVC -ts today
# Use sealert for a human-readable explanation (requires setroubleshoot)
sudo sealert -a /var/log/audit/audit.log
# Generate a custom policy module from recent denials
sudo ausearch -m AVC -ts today | audit2allow -M my_custom_policy
# Review the generated policy (always do this before installing)
cat my_custom_policy.te
# Install the policy module
sudo semodule -i my_custom_policy.pp
# List all installed custom modules
sudo semodule -l | grep my_custom
Caution: Do not blindly apply audit2allow output. Review the generated
.te file to ensure you are not granting overly broad permissions. A
allow httpd_t file_type:file read would let Apache read every file on the
system -- defeating the purpose of MAC entirely.
Common SELinux Booleans
SELinux booleans are toggle switches for predefined policy options that cover common administrator needs:
# List all booleans related to httpd
sudo getsebool -a | grep httpd
# Allow httpd to make outbound network connections (reverse proxy, API calls)
sudo setsebool -P httpd_can_network_connect on
# Allow httpd to connect to databases
sudo setsebool -P httpd_can_network_connect_db on
# Allow httpd to send mail
sudo setsebool -P httpd_can_sendmail on
# Allow Samba to share home directories
sudo setsebool -P samba_enable_home_dirs on
The -P flag makes the change persistent across reboots.
AppArmor
AppArmor is the default MAC on Ubuntu, Debian, SUSE, and their derivatives. Instead of labeling every file with a security context, AppArmor confines processes based on path-based profiles. This path-based approach makes profiles somewhat easier to write and understand compared to SELinux type enforcement policies.
Checking AppArmor Status
# Show all loaded profiles and their current modes
sudo aa-status
# Typical output:
# 38 profiles are loaded.
# 36 profiles are in enforce mode.
# 2 profiles are in complain mode.
# 12 processes have profiles defined.
Profile Modes
AppArmor profiles operate in two modes:
- Enforce -- violations are blocked and logged.
- Complain -- violations are logged but not blocked (for testing).
# Set a profile to enforce mode
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
# Set a profile to complain mode for testing
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx
# Disable a profile entirely
sudo ln -s /etc/apparmor.d/usr.sbin.nginx /etc/apparmor.d/disable/
sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.nginx
# Re-enable a disabled profile
sudo rm /etc/apparmor.d/disable/usr.sbin.nginx
sudo apparmor_parser -a /etc/apparmor.d/usr.sbin.nginx
Writing AppArmor Profiles
Profiles live in /etc/apparmor.d/ and are named after the binary's absolute
path with slashes replaced by dots. A profile for a custom application:
# /etc/apparmor.d/usr.local.bin.myapp
#include <tunables/global>
/usr/local/bin/myapp {
#include <abstractions/base>
#include <abstractions/nameservice>
#include <abstractions/openssl>
# Allow reading configuration
/etc/myapp/ r,
/etc/myapp/** r,
# Allow reading and writing data directory
/var/lib/myapp/ rw,
/var/lib/myapp/** rw,
# Allow writing logs
/var/log/myapp/ rw,
/var/log/myapp/** w,
# Allow network access (TCP and UDP)
network inet tcp,
network inet udp,
network inet6 tcp,
# Allow reading shared libraries
/usr/lib/** mr,
# Deny everything else by default (implicit in AppArmor)
}
# Load or reload the profile
sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.myapp
# Generate a profile interactively from observed behavior
sudo aa-genprof /usr/local/bin/myapp
# Exercise the application, then press S to scan logs and F to finish.
# aa-genprof will suggest rules based on the access patterns observed.
Troubleshooting AppArmor
# View AppArmor denials in the kernel log
journalctl -k | grep apparmor
# Or search the audit log directly
sudo grep apparmor /var/log/audit/audit.log
# Use aa-logprof to update a profile based on recent denials
sudo aa-logprof
# Check if a specific process is confined
sudo aa-status | grep nginx
Choosing Between SELinux and AppArmor
| Criterion | SELinux | AppArmor |
|---|---|---|
| Default on | RHEL, CentOS, Fedora | Ubuntu, Debian, SUSE |
| Model | Label-based (contexts) | Path-based (profiles) |
| Learning curve | Steeper | Gentler |
| Granularity | Very fine-grained | Moderate |
| Policy language | Type Enforcement (m4 macros) | Simple text profiles |
| File renames | Context follows the inode | Path must be updated in profile |
| Network control | Full port/protocol labeling | Basic network rules |
The practical advice: use whichever your distribution ships by default. Switching from AppArmor to SELinux (or vice versa) on a distro that does not natively support it is possible but creates significant maintenance overhead and may conflict with distribution packages. Both frameworks provide strong confinement when properly configured, and both are vastly better than running with no MAC at all.
MAC is the last kernel-level defense against compromised processes. Even if an attacker gains code execution as a service user, a well-written SELinux policy or AppArmor profile limits the damage to only those files and ports the policy explicitly allows. Combine MAC with auditing to detect when policies are being tested by an attacker and respond before they find a gap.