Skip to main content

Mandatory Access Control: Configuring SELinux and AppArmor

Security · 29.09.2026

What Is Mandatory Access Control and Why a Server Needs It

Mandatory access control (MAC) is an extra layer of protection on top of standard Linux permissions. Even a process running as root cannot exceed the actions its policy allows. A compromised web process on nginx or php-fpm without MAC gets access to the entire filesystem, while with SELinux or AppArmor it only reaches the files of its own profile.

On RHEL, CentOS, and AlmaLinux this job belongs to SELinux, while on Ubuntu and Debian it is AppArmor. Both are enabled by default, but administrators often disable them at the first access error — that strips the server of an important protection layer.

What Is the Difference Between SELinux and AppArmor

SELinux works on a label model: every file, process, port, and socket gets a security context in the form user:role:type:level, and the policy describes which processes may interact with which objects. This gives flexible but harder-to-debug control.

AppArmor is simpler: rules are tied not to labels but to paths, and a profile is a plain text file in /etc/apparmor.d/, for example /etc/apparmor.d/usr.sbin.nginx. The entry barrier is lower, but the protection is less granular: a path can be bypassed through a symlink or bind mount with a careless profile.

ParameterSELinuxAppArmor
DistributionsRHEL, CentOS, AlmaLinux, FedoraUbuntu, Debian
ModelLabels on objectsPaths in a profile
Rule formatBinary policy, boolean flagsPlain-text profile
Modesenforcing, permissive, disabledenforce, complain
Analysis toolaudit2allowaa-logprof

How to Check the Status and Switch the Working Mode

The current SELinux mode is shown by getenforce, details by sestatus. The persistent setting lives in /etc/selinux/config, in the SELINUX= parameter, which accepts enforcing, permissive, or disabled. Switch the mode temporarily, until the next reboot:

sestatus
setenforce 0   # temporarily switch to permissive
setenforce 1   # switch back to enforcing

In AppArmor, profile status is shown by aa-status: you can see whether a profile runs in enforce or complain mode. Switching one profile to complain mode for testing does not disable protection entirely, it only relaxes it for that single process:

aa-status
aa-complain /etc/apparmor.d/usr.sbin.nginx
aa-enforce /etc/apparmor.d/usr.sbin.nginx

How to Configure SELinux Contexts and AppArmor Profiles for Nginx and PHP-FPM

What matters is the correct context types on files and the right boolean switches. The site directory needs the httpd_sys_content_t type, while directories nginx or php-fpm write to (cache, uploads, sessions) need httpd_sys_rw_content_t. For network access, enable the matching boolean:

semanage fcontext -a -t httpd_sys_content_t "/var/www/site(/.*)?"
semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/site/uploads(/.*)?"
restorecon -Rv /var/www/site
setsebool -P httpd_can_network_connect 1

In AppArmor, profiles for nginx and php-fpm usually ship with the package, but the site's actual paths must be listed explicitly, otherwise access is denied. After editing a profile it must be reloaded:

vim /etc/apparmor.d/usr.sbin.nginx
apparmor_parser -r /etc/apparmor.d/usr.sbin.nginx
systemctl reload apparmor

The principle is the same: grant access narrowly, for a specific directory or port. Basic file permissions are covered in the article on Linux file permissions, and web server hardening steps are covered in the article on nginx security hardening.

How to Diagnose Denials with audit2allow and aa-logprof

When SELinux blocks a legitimate action, a record lands in /var/log/audit/audit.log. To generate a narrow rule instead of disabling protection entirely, use ausearch together with audit2allow:

ausearch -m avc -ts recent
audit2allow -a -M nginx_uploads
semodule -i nginx_uploads.pp

The nginx_uploads.pp module adds only the permission actually requested — far safer than switching the whole system to permissive. Before applying it, open the generated .te file and check that the rule does not grant excessive rights, for example access to shadow_t.

In AppArmor, aa-logprof plays a similar role: it reads the kernel log, finds DENIED entries for profiles in complain mode, and suggests adding a specific rule to the profile:

aa-complain /etc/apparmor.d/usr.sbin.php-fpm
# reproduce the problematic action on the site
aa-logprof

The aa-genprof tool builds a profile from scratch, and monitoring a process's real activity is worth supplementing with server inventory via osquery or log collection in Wazuh SIEM.

What to Do When Migrating a Site So a Wrong Context Does Not Break It

A common mistake after migrating a site or restoring from a backup is that files get copied via rsync, tar, or FTP and inherit the parent directory's default context instead of httpd_sys_content_t. Nginx and php-fpm then return 403 Forbidden even though chmod and chown permissions look correct — it is the SELinux context, not Unix permissions, blocking the read.

The fix is to restore the context according to policy rather than manually assigning a type to every file:

restorecon -Rv /var/www/site
ls -Z /var/www/site/index.php

AppArmor has no such file-context problem, but after moving a site to a new path (for example from /var/www/html to /var/www/site) the profile must be updated manually — otherwise the process fails to see the new files or gets DENIED.

Checklist for SELinux and AppArmor on a Production Server

  • Enforcing mode (SELinux) or enforce mode (AppArmor) is enabled in production, not only during testing.
  • After migrating a site, restorecon -Rv has been run for the site directory.
  • Upload and cache directories carry the httpd_sys_rw_content_t type, not a generic read-only type.
  • New denials are analyzed with audit2allow or aa-logprof instead of being solved by leaving setenforce 0 permanently.
  • Generated modules and profiles are reviewed manually before being applied in production.
  • MAC protection does not replace the server's base configuration — file permissions, firewall, and SSH are configured separately, as described in the general server security checklist.
← Back to Knowledge Base Ask Support