Handbook sections

Mandatory Access Control (SELinux / AppArmor)

Last reviewed

Mandatory Access Control confines each service to least privilege so a compromised process can't roam the whole system. The only state that protects you is enforcing mode.

The threat: DAC gives a compromised root the whole machine

Standard Linux permissions are discretionary (DAC): the owner of a process decides what it can touch, and a process running as root can touch everything. So the moment an attacker takes over a network-facing daemon (a vulnerable web server, a parser, a misconfigured database) they inherit that daemon's reach. A single remote-code-execution bug becomes a foothold to read /etc/shadow, write to /root, load a kernel module, or pivot to other services. Nothing in DAC says the web server has no business reading the SSH host keys. The blast radius of one compromise is the whole machine.

Why harden it

Mandatory Access Control (MAC) flips the model: the kernel enforces a system-wide policy that no process, not even root, can override. Each confined service gets a profile (AppArmor) or a domain/type (SELinux) describing exactly which files, capabilities, ports and network operations it may use. Anything outside the profile is denied and logged, regardless of file ownership. That turns a full system compromise into a contained incident: the cracked web server can still only do what a web server is allowed to do.

The catch is that MAC only protects you when it is actually enforcing. SELinux has three states (enforcing, permissive which logs but allows, and disabled), and AppArmor profiles sit in enforce or complain mode. A box left in permissive/complain, or with selinux=0 smuggled onto the kernel command line, looks protected but blocks nothing.

What Pavois audits: the effective, kernel-resolved state

Pavois checks the effective, kernel-resolved state of MAC, not just whether config files mention it. On Debian/Ubuntu it runs aa-status to confirm the AppArmor module is loaded, profiles are loaded, and they are in enforce (not silently unconfined); it verifies apparmor, apparmor-utils and libpam-apparmor are installed. On RHEL it confirms the SELinux runtime (libselinux, policycoreutils) and inspects /proc/cmdline to ensure SELinux was not disabled with selinux=0. It guards the policy by checking ownership/permissions on /etc/selinux, asserts the auditd rule that records MAC-policy changes, and flags policy-relaxing helpers (setroubleshoot, mcstrans) to remove. Because it reads the resolved kernel and tool state, a host stuck in permissive/complain is caught where a file-only scan calls it compliant.

How do you verify and operate?

SELinux:

  • Mode: getenforce (live), sestatus (full status + policy), setenforce 1 (temporary), /etc/selinux/config (persistent SELINUX=enforcing).
  • Booleans toggle optional policy without writing rules: getsebool -a, then setsebool -P httpd_can_network_connect on (the -P makes it persistent). This is how you grant a confined service a legitimate extra permission, not by disabling SELinux.
  • File contexts are labels, not paths: ls -Z shows them; a file in the wrong context is denied. Fix with restorecon -Rv <path> (re-apply policy defaults) or define a rule with semanage fcontext -a -t <type> '<path>(/.*)?' then restorecon.
  • Confined vs unconfined: a service running in unconfined_service_t is not protected; ps -eZ and sesearch reveal domains that escaped confinement.

AppArmor: aa-status lists loaded profiles and their mode; aa-enforce /etc/apparmor.d/<profile> and aa-complain switch a profile; profiles live in /etc/apparmor.d/.

Fixing a denial the right way

When a confined service breaks, the wrong fix is setenforce 0. The right workflow reads the AVC denial from the audit log and adjusts policy:

ausearch -m avc -ts recent            # find the denial
audit2why < /var/log/audit/audit.log  # explain WHY it was denied
audit2allow -a -M mymodule            # generate a targeted policy module
semodule -i mymodule.pp               # load it

Prefer a boolean if one covers the case (setsebool -P), then a relabel (restorecon), and only then a custom module. AppArmor's equivalent is aa-logprof, which walks the complain-mode log and proposes profile updates.

Pitfalls

  • setenforce 0 is not a fix: it disables protection globally to paper over one denial. Use a boolean, a relabel, or audit2allow.
  • Disabling then re-enabling SELinux forces a full relabel (touch /.autorelabel + reboot), which can be slow; prefer permissive over disabled if you must loosen temporarily.
  • AppArmor complain mode logs but allows: it is for tuning, not protection. Confirm enforce with aa-status.
  • A service in unconfined_service_t is unprotected even with SELinux enforcing; check that your exposed daemons run in a confined domain.

FAQ

Permissive or enforcing? Only enforcing (SELinux) and enforce (AppArmor) block anything. Permissive/complain only log, so they are tuning modes, not hardening.

My app breaks under SELinux. Do I disable it? No. Read the AVC (ausearch -m avc), explain it (audit2why), then fix with a boolean, a restorecon, or a targeted audit2allow module.

Why does Pavois read /proc/cmdline and aa-status instead of /etc/selinux/config? Because selinux=0 on the kernel line overrides the config file, and a profile can be loaded yet unconfined. Only the resolved runtime tells the truth.