Skip to main content

SELinux vs AppArmor — Mandatory Access Control Explained

· 8 min read
Goel Academy
DevOps & Cloud Learning Hub

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.

FeatureDAC (Traditional)MAC (SELinux/AppArmor)
Who sets policyFile ownerSystem administrator
Root bypassRoot ignores all DACRoot still subject to MAC
GranularityUser/group/otherPer-process, per-file, per-port
Default stanceEverything allowed unless deniedEverything 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
ModeBlocks?Logs?Use Case
EnforcingYesYesProduction — always
PermissiveNoYesDebugging, policy development
DisabledNoNoNever 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
ModeBehaviorEquivalent
EnforceBlock and log violationsSELinux Enforcing
ComplainLog violations but allowSELinux Permissive
UnconfinedNo restrictionsNo 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

FeatureSELinuxAppArmor
ApproachLabel-basedPath-based
Default onRHEL, Fedora, RockyUbuntu, Debian, SUSE
ComplexityHigh (steep learning curve)Medium (easier to start)
GranularityVery fine (per-object labels)Good (path-based rules)
Network controlYes (port labels)Limited
Multi-level securityYes (MLS/MCS)No
Container supportExcellent (labels follow objects)Good (but path-based has limits)
Debugging toolsaudit2why, audit2allowaa-logprof, aa-genprof
Rename protectionYes (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.