SELinux vs AppArmor
Normal Linux permissions ask one question: which user is this? Root passes every check. That's a problem when a web server or a network service gets hacked: whatever the attacker tricks it into doing, it does with that service's full rights. Mandatory access control adds a second question: which program is this, and what is it allowed to touch? The RHEL family answers with SELinux, Ubuntu with AppArmor. This is the biggest security difference between the two families, and a classic cause of "permission denied… but the permissions are fine!".
You will learn
- DAC (normal permissions) vs MAC (SELinux / AppArmor)
- SELinux: labels (
ls -Z,ps -Z), modes, reading denials,semanage+restorecon, ports and booleans - AppArmor: profiles by path,
aa-status, enforce vs complain,apparmor_parser -r - Why "just turn it off" is the wrong fix, and how to test safely instead
Two layers of "may I?"
| DAC: permissions | MAC: SELinux / AppArmor | |
|---|---|---|
| Asks | which user and group? | which program, touching which kind of thing? |
| Who decides | the file's owner (chmod, chown) | a policy written by the admin and the distribution |
| Root | passes everything | is stopped too, if the program isn't allowed |
Both layers must say yes. So when something fails with "Permission denied" and ls -l looks fine, the answer is often MAC.
SELinux (Rocky, RHEL, Fedora, Alma…)
SELinux puts a label on every file and every process. The policy says which process types may use which file types. Apache runs as httpd_t and may read files labelled httpd_sys_content_t. That's the label everything in /var/www gets automatically.
getenforce # Enforcing | Permissive | Disabled ls -Z /var/www/html # system_u:object_r:httpd_sys_content_t:s0 index.html ps -eZ | grep httpd # system_u:system_r:httpd_t:s0 … httpd sudo ausearch -m avc -ts recent # what SELinux blocked, and why ("AVC denied")
Files get labels from rules about their location. Two very common traps:
- mv keeps the old label. A file made in your home folder is
user_home_t. Move it into the web folder and it keeps that label, so Apache can't read it. (cpgives the copy the new location's label.) - Non-standard places have no web label. Serve a site from
/srv/wwwand it'svar_t: not for Apache.
The permanent fix is a rule plus a relabel:
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/www(/.*)?" # the rule (package policycoreutils-python-utils) sudo restorecon -Rv /srv/www # apply the rules to the files
chcon -t … also changes a label, but only until the next relabel, and then your fix silently disappears. Prefer semanage + restorecon.
SELinux guards ports too: Apache may only listen on ports labelled http_port_t (sudo semanage port -l | grep http). A new port needs sudo semanage port -a -t http_port_t -p tcp 8081. And booleans switch optional powers on or off: getsebool -a | grep httpd, then sudo setsebool -P httpd_can_network_connect on (needed when Apache is a reverse proxy; -P = permanent).
AppArmor (Ubuntu, Debian, SUSE)
AppArmor doesn't label files. Each profile belongs to a program (by its path) and lists which paths it may read (r) or write (w). Programs without a profile are unconfined.
sudo aa-status # profiles loaded, enforce vs complain, confined processes cat /etc/apparmor.d/usr.local.bin.cht-notes # a profile is plain text sudo journalctl -k | grep DENIED # apparmor="DENIED" … name="/srv/notes/…"
/usr/local/bin/cht-notes {
include <abstractions/base>
/etc/cht-notes.conf r,
/var/lib/cht-notes/** rw, # ** = everything below, / included
}
To allow a new place, add a rule, then load the profile. The kernel uses the loaded copy, not the file, so editing alone does nothing:
sudo apparmor_parser -r /etc/apparmor.d/usr.local.bin.cht-notes # -r = replace the loaded profile
Testing without switching it off
sudo setenforce 0 # permissive: log, don't block # …test: does it work now? sudo setenforce 1 # back on, then fix the real cause
sudo aa-complain /usr/local/bin/cht-notes # this program only # …test: logs say ALLOWED instead of DENIED sudo aa-enforce /usr/local/bin/cht-notes
Forum answers love setenforce 0 or SELINUX=disabled. It makes the error go away, and removes a whole layer of protection for every service on the machine. Use permissive or complain mode for one minute to confirm the cause, switch back, and fix it properly: a label rule, a port, a boolean, or a profile line.
Practice: permission denied, but why? 🛡️
On Rocky, the family website moved to /srv/www and now Apache answers 403 Forbidden. On Ubuntu, the cht-notes service should save notes in /srv/notes, but every attempt fails, even though it runs as root. Switch families to try both.
Quick check
1. ls -l says the file is readable by everyone, but Apache on Rocky gets "Permission denied". What do you check next?
✓ Permissions were fine, so the other layer said no.
2. Why is semanage fcontext + restorecon better than chcon?
✓ chcon's change is undone by the next restorecon or full relabel.
3. You added a line to an AppArmor profile, but the program is still denied. What did you forget?
✓ The kernel enforces the loaded profile. Then restart the service.
Next up: Linux Sysadmin · Run real servers, starting with “Networking: ifconfig vs ip”.