Linux hosts

Mount options: noexec, nosuid, nodev, the secure way

The attacker's first move after a web shell is almost always the same: drop a binary in /tmp and run it. It is a little like leaving the workshop unlocked and then acting surprised about the new furniture.

The short answer

Mount every filesystem that users or services can write to, such as /tmp, /var/tmp, /dev/shm and /home, with nosuid and nodev, and with noexec where nothing needs to run from it. Set it in /etc/fstab, apply with mount -o remount, and check with findmnt. noexec stops direct execution only; scripts run through an interpreter still work.

Updated Houssam Hammoudi, CTOTested with util-linux 2.38.1, bash 5.2 (Debian 12)

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

What goes wrong

Every Linux host has directories that anyone can write to. /tmp, /var/tmp and /dev/shm are writable by every user and every service. Home directories are writable by their owners, which includes a compromised web application running as its own user.

By default, a file in those places can be executed, can carry a setuid bit that the kernel honors, and can be a device node that gives raw access to hardware. An attacker who can write a file can often run it too.

Three mount options close these doors for a whole filesystem at once:

  • noexec: the kernel refuses to run programs directly from it.
  • nosuid: setuid and setgid bits and file capabilities are ignored.
  • nodev: device files on it cannot be opened.

What the docs say

Do not permit direct execution of any binaries on the mounted filesystem.

Source: mount(8), noexec

Do not honor set-user-ID and set-group-ID bits or file capabilities when executing programs from this filesystem.

Source: mount(8), nosuid

Do not interpret character or block special devices on the filesystem.

Source: mount(8), nodev

The important word is "direct". noexec does not stop sh /tmp/x.sh, python3 /tmp/x.py or perl /tmp/x.pl, because the program that runs is the interpreter, which lives on another filesystem. The man page does not spell this out, so many people think noexec blocks all code.

The secure configuration

conf
# /etc/fstab (add or adjust these lines; keep your existing / and /boot entries)
# /tmp in RAM, nothing runs from it, no setuid, no devices.
tmpfs      /tmp      tmpfs  rw,nosuid,nodev,noexec,mode=1777,size=2G  0 0
# /var/tmp survives reboots, so bind it to its own disk path with the same rules.
/srv/vartmp /var/tmp none   rw,bind,nosuid,nodev,noexec              0 0
# Shared memory: nothing should be executed from here.
tmpfs      /dev/shm  tmpfs  rw,nosuid,nodev,noexec                    0 0
# Home directories: users may run their own scripts, but setuid and devices are ignored.
UUID=0a1b2c3d-0000-4000-8000-000000000001  /home  ext4  defaults,nosuid,nodev  0 2
# /boot is only read by the bootloader and the kernel update scripts.
UUID=0a1b2c3d-0000-4000-8000-000000000002  /boot  ext4  defaults,nosuid,nodev,noexec  0 2
bash
# Create the backing directory for /var/tmp once.
install -d -m 1777 /srv/vartmp
findmnt --verify            # checks /etc/fstab for errors before you reboot
systemctl daemon-reload     # systemd reads fstab into mount units
mount -o remount /tmp       # re-reads the options from fstab
mount -o remount /dev/shm
mount /var/tmp
findmnt -o TARGET,OPTIONS /tmp /var/tmp /dev/shm /home

Prove it

The test mounts three tmpfs filesystems: /mnt/plain with no options, /mnt/nosuid-only which keeps exec so the setuid bit can be tested, and /mnt/hardened with all three options.

bash
findmnt -o TARGET,FSTYPE,OPTIONS /mnt/plain
findmnt -n -o TARGET,FSTYPE,OPTIONS /mnt/nosuid-only
findmnt -n -o TARGET,FSTYPE,OPTIONS /mnt/hardened
text
TARGET     FSTYPE OPTIONS
/mnt/plain tmpfs  rw,relatime,size=16384k
/mnt/nosuid-only tmpfs  rw,nosuid,nodev,relatime,size=16384k
/mnt/hardened tmpfs  rw,nosuid,nodev,noexec,relatime,size=16384k

noexec stops a copied binary, and a script run by its path:

bash
/mnt/plain/id -un
/mnt/hardened/id -un
/mnt/hardened/job.sh
text
root
bash: line 17: /mnt/hardened/id: Permission denied
bash: line 19: /mnt/hardened/job.sh: Permission denied

It does not stop the same script given to its interpreter. It does stop the old trick of starting a binary through the dynamic loader:

bash
sh /mnt/hardened/job.sh
/lib64/ld-linux-x86-64.so.2 /mnt/hardened/id -un
text
script ran
/mnt/hardened/id: error while loading shared libraries: /mnt/hardened/id: failed to map segment from shared object

nosuid: a setuid-root copy of bash, run by the unprivileged user nobody:

bash
printf "plain:       "; su -s /bin/sh nobody -c "/mnt/plain/rootbash -p -c \"id -un\""
printf "nosuid-only: "; su -s /bin/sh nobody -c "/mnt/nosuid-only/rootbash -p -c \"id -un\""
text
plain:       root
nosuid-only: nobody

nodev: a character device node created on each mount (major 1, minor 5, the same device as /dev/zero):

bash
head -c 4 /mnt/plain/zero | od -An -tx1 | sed "s/^/plain: read bytes/"
head -c 4 /mnt/hardened/zero
text
plain: read bytes 00 00 00 00
head: cannot open '/mnt/hardened/zero' for reading: Permission denied

The test script is secure-tests/mount-options-noexec-nosuid-nodev/run.sh.

Mistakes people make

Believing noexec blocks all code

It blocks execve() of files on that filesystem. Interpreters, bash script, and code loaded by a program that is already running are not blocked. Treat noexec as one layer that removes the easy path, not as a sandbox.

Forgetting /var/tmp and /dev/shm

Hardening /tmp alone moves the attacker one directory over. /var/tmp and /dev/shm are just as writable.

Editing fstab and never checking

Options you wrote are not options the kernel uses until the filesystem is remounted. findmnt shows what is in force; /etc/fstab shows what you hoped.

A typo that stops the boot

A broken line in /etc/fstab can drop a host into emergency mode. Run findmnt --verify after every edit, and keep console access before you reboot a remote host.

Installers that run from /tmp

Some installers unpack a helper into /tmp and run it. Point them at a private directory with TMPDIR=/root/tmp for that one command instead of removing noexec for everyone.

Checklist

  • /tmp is mounted with nosuid,nodev,noexec.
  • /var/tmp is mounted with nosuid,nodev,noexec.
  • /dev/shm is mounted with nosuid,nodev,noexec.
  • /home is mounted with nosuid,nodev.
  • Any removable or data filesystem is mounted with nosuid,nodev.
  • findmnt --verify reports no errors.
  • findmnt -o TARGET,OPTIONS shows the options on the running host.
  • A test binary copied to /tmp fails with Permission denied.

Three words in fstab will not stop a determined attacker. They will stop the lazy ones, and the lazy ones are most of them.

FND

Learn it on a live range

Linux 3: securing Linux, in Foundation: a real host in your browser, and every objective checked on the machine.

Start free

The Secure Way

More on linux hosts

SSH, sudo, firewalls, mount options, updates and the host basics every engineer should get right.

All linux hosts guides