File Permissions and Ownership covers discretionary access control (DAC): the file's owner decides who can read, write, or execute it. SELinux and AppArmor add a second, independent layer - mandatory access control (MAC) - where a system-wide policy, not any individual file owner, decides what a process is allowed to do, even if the file's own permissions would otherwise allow it.

Why a second layer

Standard permissions only limit users. A process running as an unprivileged user but with a bug (a web server with a file-upload vulnerability, say) can still do anything that user's permissions allow - read other files that user owns, make outbound network connections, whatever. MAC narrows this further: a policy can confine nginx specifically to reading its own web root and writing only its own logs, regardless of what the www-data user could otherwise touch - so a compromised process is boxed in even beyond its own account's normal permissions.

SELinux (RHEL, Fedora, CentOS)

Every process and file gets a security context (a label): user, role, type, and optionally a sensitivity level. Policy rules are written in terms of these labels, not filenames or usernames directly.

getenforce                    # Enforcing, Permissive, or Disabled
ls -Z /var/www/html/index.html
Enforcing
unconfined_u:object_r:httpd_sys_content_t:s0 index.html

Moving a file into a web root with mv (which preserves the source directory's context) instead of cp is a classic SELinux gotcha: the file keeps its old label, doesn't match what httpd_sys_content_t policy expects, and gets silently denied even though the DAC permissions look completely fine.

sudo restorecon -Rv /var/www/html/   # reset labels to policy defaults
sudo semanage fcontext -a -t httpd_sys_content_t "/srv/web(/.*)?"
sudo setenforce 0    # Permissive: log denials, but don't block them (diagnosis only)

Diagnosing a denial:

sudo ausearch -m avc -ts recent
sudo sealert -a /var/log/audit/audit.log   # human-readable explanation, if setroubleshoot is installed

audit2allow can generate a custom policy module from observed denials when a legitimate use case genuinely isn't covered by the stock policy - review what it generates before applying it, rather than granting it blindly.

AppArmor (Debian, Ubuntu, openSUSE)

AppArmor takes a simpler, path-based approach instead of labeling every file: a profile lists the exact file paths and capabilities one program is allowed to use.

sudo aa-status
apparmor module is loaded.
15 profiles are loaded.
13 profiles are in enforce mode.
2 profiles are in complain mode.

Profiles live in /etc/apparmor.d/, one file per confined program:

sudo aa-complain /etc/apparmor.d/usr.sbin.nginx   # complain mode: log only, don't block
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx    # enforce mode: actually block violations
journalctl -k | grep -i apparmor   # denials show up in the kernel log

aa-genprof walks through generating a new profile interactively: run it, exercise the program's normal behavior, and it proposes rules for each file/capability access it observes, which you then approve or deny.

Diagnosing "permission denied" that isn't

If standard permissions and ownership look correct (ls -l matches what you'd expect) but access still fails, MAC is the next thing to check - the symptoms look identical to a DAC issue from the application's point of view, but the fix is completely different (fixing a label or profile, not chmod/chown).

Don't just disable it

Turning SELinux or AppArmor off entirely to make an error go away is a common shortcut - and removes a real layer of defense along with it. In almost every case, the correct fix is adjusting the policy or label to match a legitimate need (as above), not disabling enforcement system-wide. See Hardening Basics for where this fits into a broader security baseline.