Mount options & filesystem
noexec, nosuid, nodev where they belong.
A large office building can hang the same lock on every door and still control what happens inside each room by posting a rule at the entrance: no open flames in the server room, no equipment leaves the workshop, no outside hardware in the lab. The rule covers the whole room, and it holds no matter what anyone carries in. Linux mount options work like those posted rules. A mount option is a restriction the kernel (the core part of the operating system that talks straight to the hardware) enforces across an entire mounted filesystem, whatever the individual files inside it claim they are allowed to do.
Three of those rules do most of the security work: noexec, nosuid, and nodev. Each strips a single power from a whole area at once. Because the rule lives on the mount and not on any one file, an attacker who can write files into that area has no way to switch it off. That is what makes them worth learning well: they shut down whole techniques instead of blocking one file at a time.
The three rules, in plain terms
noexec means no execute. The kernel refuses to launch any program stored on that filesystem. You can still read the file, copy it, open it in an editor, or checksum it. The moment you try to run it as a program, the kernel says no. It is the room where nothing can be switched on.
nosuid needs one piece of background. A special permission bit, the SUID (Set User ID) bit, and its sibling the SGID (Set Group ID) bit, let a program run with the powers of the file's owner instead of the powers of the person who started it. That is how the passwd command can rewrite the root-owned password file while you run it as an ordinary user. nosuid tells the kernel to ignore those bits on this filesystem. A root-owned program with the SUID bit set, dropped into a nosuid area, runs as whoever you already are. No free promotion to root.
nodev is about device files. A device file is a doorway drilled straight through to the hardware: /dev/sda opens onto the raw disk, /dev/mem onto physical memory. Reading or writing those doorways skips the normal file-by-file permission checks and talks to the device itself. nodev tells the kernel that any device file living on this filesystem is a dud, an ordinary lump of data with nothing wired to the hardware behind it. An attacker cannot smuggle a private /tmp/rawdisk doorway into a writable area and read the disk underneath everyone's permissions.
Why /tmp is the one everyone hardens first
/tmp is the shared coat-check counter of a Linux system. Every user and every service can drop things on it (it is world-writable, held in check only by the sticky bit that stops you deleting someone else's files). That openness is exactly what an attacker wants. After a first foothold, often a low-privilege service account, the routine next step is to pull tooling down from somewhere and run it: a privilege-escalation helper, a network scanner, a crypto miner, a reverse shell. It has to land in a writable spot, and /tmp is the obvious one.
Mount /tmp with noexec and that routine step falls over. The payload lands fine. Running it does not.
That is not a file-permission error. The copy is owned by you and marked executable. The 'Permission denied' is noexec on the mount refusing the launch. You can confirm which option is in force with findmnt, part of the util-linux tool set, which reads the live mount table instead of what fstab hoped for:
Add nosuid so a planted SUID-root binary gives no promotion, and nodev so a fake device file is inert, and the three together turn /tmp from a launchpad into a dead end for a lot of off-the-shelf tooling.
noexec is a speed bump, not a wall
Here is the honest part, and the part that keeps you from a false sense of safety. noexec blocks one specific thing: the kernel executing a file that lives on the mount. It does not stop a program that is already allowed to run from reading that file as input. An interpreter is exactly such a program.
sh lives in /usr/bin, a normal executable filesystem. It runs, opens /tmp/x.sh as text, and does what the text says. noexec never enters the picture. The same goes for python3 /tmp/payload.py, perl /tmp/x.pl, or node /tmp/x.js: the interpreter runs from a place that allows execution, and your file is only its input.
You may have read that the dynamic linker (the small program that loads shared libraries and starts a program running) hands you a way around noexec: call it by hand and give it the binary, /lib64/ld-linux-x86-64.so.2 /tmp/id. That trick is real, but it picks a different lock. It runs a binary that is missing its execute bit on an ordinary, execute-allowed filesystem. It does nothing against noexec. To start the program, the loader still has to map the code into memory as executable, and a noexec mount refuses that mapping just as flatly as it refuses a direct launch. So the loader trips over the same wire:
The move that does run a compiled binary out of a noexec /tmp goes around the mount instead of through it. Reading a file is always allowed, so the bytes get read, copied into an anonymous file that lives only in memory (memfd_create, a kernel call that gives you a file backed by RAM rather than by any mounted disk), and run straight from there with execveat (the system call that launches a program named by an open file descriptor). Nothing ever executes from /tmp, so noexec has nothing to refuse. That is the technique 'fileless' payloads lean on.
So why keep noexec at all? It shuts the laziest door, and, more useful to you as a defender, it turns an ordinary-looking action into a loud one. A process hitting 'permission denied' trying to run something out of /tmp, or a program reaching for ld-linux by its full path, is a strong anomaly you can log and alert on. Treat noexec as one layer that raises the attacker's cost and throws off signal, sitting under execution logging (auditd, the Linux audit daemon), application allow-listing, and least-privilege service accounts.
Putting the rules in place
Rules go in /etc/fstab, the filesystem table the system reads at boot to decide what to mount and with which options. Each line lists the device, the mount point, the filesystem type, the options, and two trailing numbers that control dump backups and the order of the boot-time filesystem check (fsck). For an area to carry its own options, it has to be its own mount. That means one of three things: a separate partition, a tmpfs (a filesystem that lives in memory), or a bind mount (a second doorway onto an existing directory). On a modern systemd machine, /tmp and /dev/shm as tmpfs is a clean, common choice:
# device / UUID mount point type options dump passtmpfs /tmp tmpfs defaults,nodev,nosuid,noexec,size=2G,mode=1777 0 0tmpfs /dev/shm tmpfs defaults,nodev,nosuid,noexec 0 0UUID=a1c9d4e2-... /home ext4 defaults,nodev,nosuid 0 2UUID=7b3f9c10-... /var ext4 defaults,nodev,nosuid 0 2UUID=e5a2f8b4-... /var/log ext4 defaults,nodev,nosuid,noexec 0 2
A couple of details in that file matter. mode=1777 keeps /tmp world-writable with the sticky bit, which real software depends on; leave it out and legitimate programs break. size=2G caps the memory the tmpfs can eat, so an attacker or a runaway job cannot swallow all the RAM you gave it by flooding /tmp. Note that /home and /var keep nodev and nosuid but not noexec: users and services legitimately run programs from those trees, and noexec there breaks normal work. /var/log gets all three, because nothing should ever execute out of your logs.
You do not have to reboot to try this. If an area is already its own mount, you can remount it in place and watch the options change, which is the safe way to test before you commit anything to fstab:
One catch: remount only works if /tmp is already a separate mount. If your /tmp is a plain folder on the root filesystem, mount -o remount /tmp fails, because there is no distinct mount to change. That is when you reach for a bind mount or a real partition, then make it stick in fstab.
Separation and read-only where the work allows
Two structural habits back all of this up. The first is separation: give /tmp, /var, /var/log, and /home their own filesystems. Think of the watertight compartments in a ship's hull. If someone floods /var/log with junk to force an outage, or a runaway process fills /tmp, the damage stays inside that compartment instead of spilling into the root filesystem and wedging the whole host. As a bonus, each compartment can carry its own mount rules, which is the whole point of this lesson.
The second is read-only. Areas that hold static system files, /usr and /boot especially, rarely change outside of an update. Mount them read-only and an attacker who lands on the box cannot quietly overwrite a system binary to plant a backdoor. When you do need to patch, you remount read-write for the update and read-only again after. The goal across all of it is that every part of the filesystem carries exactly the powers its job needs and nothing spare, which is least privilege applied to disk instead of to users.
Auditing what you really have
Wanting the options and having them are different things, and a typo in fstab can be silently ignored or, worse, strand a host at boot. Check the live system, not your intentions. findmnt with no filter draws the whole mount tree with the options actually in effect:
Before you ever reboot on a changed fstab, validate it. findmnt --verify parses the file the way the boot process will and flags missing mount points, unknown options, and duplicate entries; add --verbose and it walks each entry, ticking off target, options, and source one line at a time. This matters more than it sounds. A wrong device name, a misspelled option, or a stale UUID can drop the machine into emergency mode on the next boot, sometimes with no network and no easy console. Run it after every edit, test options live with mount -o remount first, and keep a console or recovery path open before you reboot a remote host on a fresh table.
python3 /tmp/payload.py. It works. Why didn't noexec stop it?mount -o remount,nodev,nosuid,noexec /tmp, but it fails. What's the most likely reason?One more sweep is worth building into your checks. Find every world-writable directory an attacker could stage in, then list the filesystems that still allow execution so you can eyeball what remains:
The first list is your staging map. The three that matter are /tmp, /var/tmp, and /dev/shm: writable scratch where an attacker can drop a file and try to run it, and all three deserve the same nodev, nosuid, noexec treatment. (/dev/mqueue and /run/lock show up because they are world-writable too, but they are small special-purpose mounts the system manages, not general scratch space.) /var/tmp is the one people forget, because unlike /tmp it survives a reboot, which makes it a favorite quiet parking spot for anything an attacker wants to keep around. Give it the rules you gave /tmp. The second list is your review pile: every real filesystem that can still run code (root and /usr have to, /boot is read-only), so nothing executable is hiding where you did not expect it.
Try this
Work through “Auditing what you really have” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: noexec is not a sandbox. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.