Handbook sections▾
Foundations
Understanding the threats to a Linux hostWhy we hardenThe defense principles behind every ruleEffective configuration: the truth no file holdsThe standards Pavois maps toSOCLE control ID modelEvidence & exportsHow the A-E grade is computedWhat a PASS proves: the qualified verdictTrust model for the evidence bundleIs SOCLE just your own norm? (governance & the circularity question)What Pavois covers, and what it doesn'tDomains
Hardening SSHHardening PAMMandatory Access Control (SELinux / AppArmor)Hardening the host firewallHardening sudoFile Permissions & OwnershipMount & filesystem hardeningHardening kernel modulesHardening the kernel & network with sysctlAudit logging with auditdHardening logging with journald and rsyslogHardening systemd servicesPackage hygieneHardening the bootloader (GRUB)Synchronizing timeLogin banners and MOTDControlling cron and at accessHardening the GNOME desktop (dconf)Tooling
Undo a hardening run: restore pointsOperating Pavois: privileges, air-gap, timing, exceptionsRun Pavois in CI (GitHub Actions)Feature status: delivered, partial, roadmapGovernance: licence, versioning, provenance, securityMandatory 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(persistentSELINUX=enforcing). - Booleans toggle optional policy without writing rules:
getsebool -a, thensetsebool -P httpd_can_network_connect on(the-Pmakes 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 -Zshows them; a file in the wrong context is denied. Fix withrestorecon -Rv <path>(re-apply policy defaults) or define a rule withsemanage fcontext -a -t <type> '<path>(/.*)?'thenrestorecon. - Confined vs unconfined: a service running in
unconfined_service_tis not protected;ps -eZandsesearchreveal 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 itPrefer 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 0is not a fix: it disables protection globally to paper over one denial. Use a boolean, a relabel, oraudit2allow.- Disabling then re-enabling SELinux forces a full relabel (
touch /.autorelabel+ reboot), which can be slow; preferpermissiveoverdisabledif you must loosen temporarily. - AppArmor
complainmode logs but allows: it is for tuning, not protection. Confirmenforcewithaa-status. - A service in
unconfined_service_tis 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.