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, securityHardening systemd services
Last reviewed
Every running daemon is reachable attack surface. Turn off the units you don't need and sandbox the ones you keep, then measure each one's exposure.
The threat: forgotten daemons and over-reaching ones
A stock Linux install boots dozens of units you never asked for: a printing stack (cups), a name-caching resolver, Bluetooth, Avahi/mDNS discovery, a debug shell, core-dump collection. Each one is code that runs as a privileged process, frequently listens on a socket, and can be reached before a single human logs in. The 2024 cups-browsed RCE chain is the textbook case: a service almost nobody needed on a server, listening on the network, turned into remote code execution.
A service you forgot is running is worse than one you chose: it is unpatched in your threat model, unmonitored, and often socket-activated so it springs to life on demand even when it looks idle. And a daemon that must run still executes with far more reach than it needs (full filesystem visibility, write access to /home, the ability to gain new privileges), so a single exploit in it owns the host.
Why harden it
The safest service is the one that isn't running, and the second safest is the one that can't reach beyond its job. Disabling unneeded units shrinks the attack surface: fewer open ports, fewer privileged processes, fewer CVEs that apply to you. For the daemons you keep, sandboxing caps the blast radius: if the process is compromised, the kernel still denies it the filesystem, the home directories, the extra capabilities and the dangerous syscalls it never legitimately uses.
Measure exposure first: systemd-analyze security
Don't guess which services are exposed, measure them. systemd-analyze security lists every service with an exposure score from 0.0 (safe) to 10.0 (unsafe) and an UNSAFE/EXPOSED/MEDIUM/OK verdict; systemd-analyze security <unit> breaks down each sandboxing directive and what it would gain. This is the baseline before and after hardening, and the fastest way to find the worst offender.
systemd-analyze security # rank all services by exposure
systemd-analyze security nginx.service # per-directive breakdown + suggested fixesSandbox the services you keep
Apply the directives in a drop-in, never by editing the vendor unit: systemctl edit <unit> writes /etc/systemd/system/<unit>.d/override.conf, then systemctl daemon-reload && systemctl restart <unit>. The high-value directives:
| Directive | Effect |
|---|---|
ProtectSystem=strict |
the whole filesystem read-only except API paths (pair with ReadWritePaths=) |
ProtectHome=yes |
/home, /root, /run/user made inaccessible |
PrivateTmp=yes |
a private /tmp, isolated from the host |
PrivateDevices=yes |
a minimal /dev, no raw hardware |
NoNewPrivileges=yes |
blocks setuid/setgid/capability escalation via execve |
ProtectKernelTunables/Modules/Logs=yes |
/proc+/sys read-only, no module load, no kmsg |
ProtectControlGroups=yes, ProtectClock=yes |
no cgroup or clock tampering |
RestrictAddressFamilies= |
only the socket families the service needs |
RestrictNamespaces=, RestrictSUIDSGID=yes, RestrictRealtime=yes |
deny namespace creation, setuid files, RT scheduling |
MemoryDenyWriteExecute=yes, LockPersonality=yes |
no W+X memory, frozen architecture |
SystemCallFilter=@system-service |
allowlist the syscalls a service legitimately uses |
CapabilityBoundingSet= |
drop every Linux capability the service doesn't need |
What Pavois audits: the live systemd state
The differentiator is that Pavois reads the live systemd state, never a config file. For each unit it queries the resolved is-enabled / is-active status (InSpec's service resource over systemctl), so it sees what the machine really does at the next boot, and it is aware of socket activation: a cups.service that looks idle can still be revived by cups.socket, which a scan of /etc/cups/cupsd.conf would never catch.
Pavois's rules check that unneeded units are disabled and stopped (cups, avahi-daemon, bluetooth, cockpit, nfs, rpcbind, snmpd, slapd, named, dnsmasq, autofs, smb, vsftpd, tftp, telnet, xinetd, rsyncd, dovecot, squid, apport, kdump, the kernel debug-shell and systemd-coredump) and that security-relevant units are enabled and running (auditd, systemd-journald, sssd, usbguard, fapolicyd, rngd, cron/crond). Findings map to CIS, NIST CM-7 (least functionality) and ANSSI BP-028 R67, and the harden plan can disable/stop or enable/start each unit.
Pitfalls
ProtectSystem=strictwithoutReadWritePaths=makes the service's own state/log/cache directories read-only and it fails to start. Add the paths it must write (/var/lib/<svc>,/var/log/<svc>).- A too-aggressive directive leaves the service active but mute: it starts, then silently can't do its job (e.g. a denied syscall). Watch
systemctl statusand the exit code. - Sandbox exit codes are diagnostic:
226/NAMESPACE,227/SECCOMP...228/SECCOMP,233/NETWORKpoint straight at the directive that is too tight. maskvsdisable:systemctl disablestops auto-start;systemctl maskmakes the unit unstartable entirely (a stronger guarantee for a banned service).
FAQ
How do I know which services are over-exposed? systemd-analyze security ranks them 0-10; start with the UNSAFE ones.
Why does Pavois read systemctl, not the config? Because socket activation and the resolved enabled/active state are the truth; a unit can be socket-revived even when its config file looks inert.
My service won't start after sandboxing. Why? Most often ProtectSystem=strict with no ReadWritePaths=, or a SystemCallFilter that dropped a needed call. The 226-233 exit codes name the culprit; loosen the one directive, not the whole sandbox.