SELinux vs AppArmor — Mandatory Access Control Explained
That mysterious "Permission denied" when everything looks correct — SELinux is probably why. Every SRE has had the moment: file permissions are 777, the process runs as root, yet it still can't read the file. The reflex is to run setenforce 0 and move on. This post teaches you why that's dangerous and how to actually work with Mandatory Access Control.
DAC vs MAC: Why Traditional Permissions Aren't Enough
Traditional Linux permissions (user/group/other) are Discretionary Access Control (DAC). The file owner decides who can access it. The problem: if a process running as root is compromised, DAC offers zero protection — root can do anything.
Mandatory Access Control (MAC) adds a second layer. Even root is subject to policy rules. A compromised web server running as root still can't read /etc/shadow if the MAC policy doesn't allow it.
| Feature | DAC (Traditional) | MAC (SELinux/AppArmor) |
|---|---|---|
| Who sets policy | File owner | System administrator |
| Root bypass | Root ignores all DAC | Root still subject to MAC |
| Granularity | User/group/other | Per-process, per-file, per-port |
| Default stance | Everything allowed unless denied | Everything denied unless allowed |
SELinux: Security-Enhanced Linux
SELinux was developed by the NSA and is the default MAC on RHEL, CentOS, Fedora, Rocky, and AlmaLinux. It uses labels (security contexts) on every file, process, port, and socket to enforce policy.
SELinux Modes
# Check current mode
getenforce
# Output: Enforcing, Permissive, or Disabled
# Check detailed status
sestatus
# Temporarily switch to permissive (logs violations but doesn't block)
sudo setenforce 0
# Switch back to enforcing
sudo setenforce 1
# Permanent mode change (survives reboot) — edit /etc/selinux/config
# SELINUX=enforcing
| Mode | Blocks? | Logs? | Use Case |
|---|---|---|---|
| Enforcing | Yes | Yes | Production — always |
| Permissive | No | Yes | Debugging, policy development |
| Disabled | No | No | Never in production |
Understanding Security Contexts
Every object in SELinux has a label with four fields: user:role:type:level. The type is what matters 99% of the time.
# View file security context
ls -Z /var/www/html/
# Output: unconfined_u:object_r:httpd_sys_content_t:s0 index.html
# View process security context
ps auxZ | grep httpd
# Output: system_u:system_r:httpd_t:s0 /usr/sbin/httpd
# View port labels
sudo semanage port -l | grep http
# http_port_t tcp 80, 443, 488, 8008, 8009, 8443
The policy says: process type httpd_t can read file type httpd_sys_content_t. If you put web files in a directory with the wrong type, Apache can't read them even with 777 permissions.
Common SELinux Operations
# Relabel a file to the correct type
sudo restorecon -Rv /var/www/html/
# Change the default type for a custom directory
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/myapp(/.*)?"
sudo restorecon -Rv /srv/myapp/
# Allow httpd to connect to the network (disabled by default!)
sudo setsebool -P httpd_can_network_connect on
# Allow httpd to connect to databases
sudo setsebool -P httpd_can_network_connect_db on
# List all booleans related to httpd
getsebool -a | grep httpd
SELinux Booleans: Quick Policy Toggles
Booleans are pre-defined policy switches. They're the fast way to adjust SELinux behavior without writing custom policy.
# List all booleans with descriptions
sudo semanage boolean -l | head -20
# Common booleans for web servers
sudo setsebool -P httpd_can_network_connect on # Allow outgoing network
sudo setsebool -P httpd_enable_homedirs on # Serve files from home dirs
sudo setsebool -P httpd_use_nfs on # Access NFS mounts
# Common booleans for containers
sudo setsebool -P container_manage_cgroup on # Containers manage cgroups
sudo setsebool -P container_use_cephfs on # Containers access CephFS
Troubleshooting SELinux Denials
This is where most people give up and disable SELinux. Don't. The troubleshooting tools are excellent.
# View recent denials in the audit log
sudo ausearch -m avc -ts recent
# The human-readable explanation — this is the golden tool
sudo ausearch -m avc -ts recent | audit2why
# Example output:
# type=AVC msg=audit(...): avc: denied { read } for pid=1234 comm="httpd"
# name="config.json" scontext=httpd_t tcontext=default_t
# Was caused by: Missing type enforcement (TE) allow rule.
# You can use audit2allow to generate a local policy module.
# Generate and install a custom policy module
sudo ausearch -m avc -ts recent | audit2allow -M my-httpd-fix
sudo semodule -i my-httpd-fix.pp
The workflow is: set to permissive, run your application, collect all denials, generate a policy, review it, install it, switch back to enforcing.
# Complete troubleshooting workflow
sudo setenforce 0 # Step 1: Go permissive
systemctl restart httpd # Step 2: Exercise the application
sudo ausearch -m avc -ts recent | audit2why # Step 3: Analyze denials
sudo ausearch -m avc -ts recent | audit2allow -M fix # Step 4: Generate policy
cat fix.te # Step 5: REVIEW the policy
sudo semodule -i fix.pp # Step 6: Install policy
sudo setenforce 1 # Step 7: Back to enforcing
Adding Custom Port Labels
# Allow httpd to listen on port 8443
sudo semanage port -a -t http_port_t -p tcp 8443
# Allow a custom app to bind to port 9090
sudo semanage port -a -t myapp_port_t -p tcp 9090
# List all port assignments
sudo semanage port -l | grep -E "http|myapp"
AppArmor: Path-Based MAC
AppArmor is the default MAC on Ubuntu, Debian, and SUSE. Instead of labels, it uses file paths to define what a process can access. This makes it simpler to understand but less granular than SELinux.
AppArmor Modes
# Check status of all profiles
sudo aa-status
# Output shows:
# - Number of loaded profiles
# - Profiles in enforce mode
# - Profiles in complain mode
# - Processes with profiles
| Mode | Behavior | Equivalent |
|---|---|---|
| Enforce | Block and log violations | SELinux Enforcing |
| Complain | Log violations but allow | SELinux Permissive |
| Unconfined | No restrictions | No profile loaded |
Working with AppArmor Profiles
Profiles live in /etc/apparmor.d/ and define exactly what a program can access.
# List all profiles and their modes
sudo aa-status
# Put a profile into complain mode (for debugging)
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx
# Put it back into enforce mode
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
# Disable a profile entirely
sudo aa-disable /etc/apparmor.d/usr.sbin.nginx
# Reload all profiles
sudo systemctl reload apparmor
Creating a Custom AppArmor Profile
# Generate a profile skeleton
sudo aa-genprof /usr/local/bin/myapp
# Or create one manually
sudo tee /etc/apparmor.d/usr.local.bin.myapp << 'PROFILE'
#include <tunables/global>
/usr/local/bin/myapp {
#include <abstractions/base>
#include <abstractions/nameservice>
# Binary itself
/usr/local/bin/myapp mr,
# Configuration
/etc/myapp/ r,
/etc/myapp/** r,
# Data directory
/var/lib/myapp/ rw,
/var/lib/myapp/** rw,
# Logs
/var/log/myapp/ rw,
/var/log/myapp/** rw,
# Temp files
/tmp/myapp-* rw,
# Network
network inet stream,
network inet dgram,
# Deny everything else implicitly
}
PROFILE
# Load the new profile
sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.myapp
Troubleshooting AppArmor Denials
# View AppArmor denials in the system log
sudo journalctl -k | grep "apparmor.*DENIED"
# Or in the audit log
sudo dmesg | grep "apparmor.*DENIED"
# Example denial:
# apparmor="DENIED" operation="open" profile="/usr/sbin/nginx"
# name="/srv/custom/index.html" pid=1234 comm="nginx"
# requested_mask="r" denied_mask="r"
# Fix: add the path to the profile
# /srv/custom/ r,
# /srv/custom/** r,
# Use aa-logprof to interactively update profiles based on denials
sudo aa-logprof
# It reads the log, shows each denial, and asks how to handle it:
# (A)llow, (D)eny, (G)lob, (N)ew profile, etc.
SELinux vs AppArmor: Head-to-Head
| Feature | SELinux | AppArmor |
|---|---|---|
| Approach | Label-based | Path-based |
| Default on | RHEL, Fedora, Rocky | Ubuntu, Debian, SUSE |
| Complexity | High (steep learning curve) | Medium (easier to start) |
| Granularity | Very fine (per-object labels) | Good (path-based rules) |
| Network control | Yes (port labels) | Limited |
| Multi-level security | Yes (MLS/MCS) | No |
| Container support | Excellent (labels follow objects) | Good (but path-based has limits) |
| Debugging tools | audit2why, audit2allow | aa-logprof, aa-genprof |
| Rename protection | Yes (labels move with files) | No (path changes break rules) |
When to Use Which
Choose SELinux when:
- Running RHEL-family distros (it's already there and configured)
- You need fine-grained network access control (port labels)
- Running containers (SELinux labels work naturally with container isolation)
- Compliance requires MLS (Multi-Level Security)
Choose AppArmor when:
- Running Ubuntu/Debian (it's already there and configured)
- You need simpler profile management
- Your team is new to MAC and needs a gentler learning curve
- Path-based rules map well to your application architecture
The Golden Rule
Never disable your MAC system. Period. If something breaks, switch to permissive/complain mode, fix the policy, and switch back. Every "just disable SELinux" shortcut is a production security incident waiting to happen.
# The wrong way (do NOT do this)
# sudo setenforce 0 # and forget about it
# sudo systemctl disable apparmor
# The right way
# 1. Switch to permissive/complain
# 2. Reproduce the issue
# 3. Read the denial logs
# 4. Fix the policy
# 5. Switch back to enforcing
This wraps up our Linux deep-dive series. From performance tuning and kernel parameters through containers, security hardening, troubleshooting, filesystems, networking, and mandatory access control — you now have the knowledge to manage Linux servers at production scale. Keep practicing, keep reading the man pages, and never stop exploring /proc.
