Filesystem hardening
Most of filesystem hardening is deciding, per mount, which of three powers a directory is allowed to grant: running a binary, honouring its setuid bit and letting it be a device node. The default, defaults, grants all three everywhere, which is convenient and wrong for the directories users and daemons can write to. The rest is separating partitions so one cannot starve the others, turning on the kernel’s link protections and keeping an eye on which files carry setuid. None of it is exotic and the payoff is that whole categories of local escalation stop having a place to land.
Mount options: withhold what the directory does not need
The three options worth withholding are noexec (nothing executes from here), nosuid (setuid and setgid bits are ignored) and nodev (device nodes are not honoured). A writable scratch directory needs none of those powers, so a sane fstab strips them from exactly the places an attacker likes to drop a payload:
# /etc/fstab — the security-relevant mounts
/tmp /tmp ext4 defaults,nosuid,nodev,noexec 0 2
/var/tmp /var/tmp ext4 defaults,nosuid,nodev,noexec,bind 0 2
/dev/shm /dev/shm tmpfs defaults,nosuid,nodev,noexec 0 0
/home /home ext4 defaults,nosuid,nodev 0 2
/var /var ext4 defaults,nosuid,nodev 0 2
/tmp, /var/tmp and /dev/shm are world-writable by design, so they get all three: a payload dropped there cannot be executed, cannot gain privilege through a setuid bit and cannot be a backdoor device node. /home and /var keep exec (users and daemons legitimately run things there) but lose nosuid and nodev, because no user’s home has any business holding a setuid-root binary or a device file.
The honest caveat is that noexec is defense-in-depth, not a wall. It stops ./payload, but an interpreter reads a script as data, so python /tmp/x.py or sh /tmp/x.sh still runs and a sufficiently determined attacker has memfd and other tricks. noexec raises the cost and blocks the lazy drop-and-run; it does not make /tmp safe to ignore. Treat it as one layer among several and it earns its place.
Separate partitions so one cannot sink the ship
A single flat root filesystem means anything that fills a directory fills everything: runaway logs in /var/log, a user filling /home, a temp-file flood in /tmp, all of them end the same way, a full / where the system can no longer write and services fall over. Separate partitions (or LVM volumes, or subvolumes) for /tmp, /var, /var/log and /home contain that: a filled /var/log is a logging problem, not a dead machine. The separation is also what makes the per-mount options above possible in the first place, since you can only set noexec on /tmp if /tmp is its own mount.
Turn on the kernel link protections
The symlink and hardlink attacks are old and still work where the protections are off. The classic is a TOCTOU: a privileged program checks /tmp/userfile, an attacker swaps it for a symlink to /etc/passwd between the check and the write and the program writes through the link as root. The hardlink variant keeps a readable link to a sensitive file that outlives its replacement. The kernel closes both and modern distributions enable them, but it is worth asserting rather than assuming:
# /etc/sysctl.d/filesystem.conf
fs.protected_symlinks = 1
fs.protected_hardlinks = 1
fs.protected_fifos = 2
fs.protected_regular = 2
protected_symlinks makes the kernel refuse to follow a symlink in a sticky world-writable directory unless the follower owns the link or the directory, which is exactly the /tmp TOCTOU; the others extend the same idea to hardlinks, FIFOs and regular files in those directories. These cost nothing and shut a decades-old door.
Audit setuid and prefer capabilities
Every setuid-root binary is a small privileged program that runs as root on behalf of an unprivileged user, which makes the set of them an attack surface worth knowing by heart. Enumerate it and question anything surprising:
find / -xdev -perm -4000 -type f 2>/dev/null
The list should be short and boring: sudo, su, passwd, mount, ping on older systems, a few others. Anything else, especially an interpreter or a copied-in tool, is a finding. Where a program needs one specific privilege rather than all of root, a file capability (for example cap_net_raw on a ping-like tool) is strictly better than setuid-root, because it grants the one power instead of the whole account. Stripping an unnecessary setuid bit (chmod u-s) is one of the cheapest risk reductions on a host.
Immutable where it helps
For files that should never change after provisioning, the immutable attribute is a cheap tripwire and brake: even root cannot modify or delete an i-flagged file without first removing the flag, which stops a careless script and slows a careful attacker.
chattr +i /etc/passwd /etc/shadow /etc/ssh/sshd_config
It is not a strong boundary, since an attacker who is already root can chattr -i first, but it turns a silent overwrite into a two-step action that an audit rule can catch and it prevents a surprising amount of accidental damage.
Where to land
Give /tmp, /var/tmp and /dev/shm their own mounts with nosuid,nodev,noexec and /home and /var with nosuid,nodev, remembering noexec is a layer and not a wall. Separate the partitions that can be filled so one cannot take down the root, which is also what lets you set those options at all. Assert the kernel link protections rather than trusting the default. Know your setuid inventory by heart and strip or replace what does not belong, preferring file capabilities to setuid-root. And flag the handful of files that should never change as immutable, as a brake and a tripwire rather than a wall. The theme is the same throughout: a writable directory should hold data, not powers.