SUID, SGID and the sticky bit
The special bits, their risks and nosuid.
Three permission bits sit on top of the read, write and execute bits you already know, and they answer a different question. SUID and SGID can change who you become while a program runs; the sticky bit changes who may delete files from a shared directory. A SUID program owned by root is one of the first things an intruder looks for, and one of the first things they leave behind, so an operations or security engineer needs to read these bits cold, know the baseline set on a host, and notice the day a new one appears. This lesson demonstrates each bit on Ubuntu 26.04, shows why the same program that runs as root from /var/tmp does not from /tmp, and ends with the sweep you run to find a planted one. (The umask, which sets ordinary permissions on new files, is covered in the permissions lesson.)
SUID: borrowing the owner's identity
When the SUID (set user ID) bit is set on a program, the program runs with the identity of the file's owner, not the person who started it. The everyday example ships on every Linux machine: passwd, which you use to change your own password. Changing a password writes to /etc/shadow, a file only root may touch, yet any user can run passwd. It works because passwd is owned by root and carries the SUID bit.
Read the permission string. Where the owner's execute letter would be there is a lowercase s: that is the SUID bit sitting on the execute bit. This program, run by anyone, executes as root. To show the effect safely, make a root-owned copy of gnuid, the GNU version of id, which only prints identity and does nothing privileged. install copies a file and sets its owner, group and mode in one step; the 4 in 4755 is the SUID bit. (Use gnuid rather than id: on Ubuntu 26.04 id is a link into the multi-call Rust binary, which picks its tool from the name it runs under and fails when copied to a new name. On RHEL, copy /usr/bin/id.)
Read the last line closely. The real UID is still 1001 (deploy), but the euid (effective user ID, the identity the kernel actually checks for permissions) is 0, which is root. The SUID bit turned the caller into root for the life of the program. That is exactly why an unfamiliar SUID-root program is dangerous: whoever runs it gets the owner's power, and if the owner is root and the program can be talked into running other commands, the machine is theirs.
Linux honours SUID only on compiled programs. On a script (a file starting with #!), the kernel ignores the SUID and SGID bits, as execve(2) documents, because the interpreter that reads the script could be steered by the caller. That is why the demonstration uses a compiled binary, and why a SUID bit on a shell script is a sign of a misunderstanding rather than a working privilege.
Notice that making that file took sudo. An ordinary user can set the SUID bit only on a file they own, which grants nothing they did not already have, and cannot give the file to root:
The copy is SUID, but to deploy, and chown to root is refused (chown(2) also clears the SUID bit whenever an ordinary user changes a file's owner or group). So a SUID-root file always means that something running as root created it: a package, an administrator, or an intruder who held root at some point.
Why the same file does not work in /tmp: nosuid
Install the identical SUID-root file into /tmp and run it there:
The bit is set (the s is there in the listing) and the owner is root, yet the euid field is gone: the program ran as plain deploy. The reason is the mount, not the file.
On Ubuntu 26.04, /tmp is a separate tmpfs mounted with nosuid, which tells the kernel to ignore the SUID and SGID bits on any file there, and nodev. /dev/shm is the same. It is a mitigation for the places everyone can write to. An ordinary user cannot create a SUID-root file, as shown above, but an intruder who held root briefly can leave one in a world-writable directory as a way back in, and a user can leave a SUID copy of a program owned by themselves there, for example to keep access as that account after a password change. nosuid makes such files inert. /var/tmp, where the first demo worked, is on the root filesystem and has no nosuid option. On RHEL 10 /tmp is on the root filesystem too, so a SUID file there is honoured; the hardening course adds nosuid to /tmp as a control. nosuid narrows where SUID works, but it is not everywhere, so an unexpected SUID file anywhere is still worth investigating.
SGID: the same trick for groups, and for directories
SGID (set group ID) is the group version. On a program it runs with the file's group; the baseline below shows the few that use it. The genuinely useful job is on a directory: with SGID set, every new file created inside inherits the directory's group rather than the creator's own primary group. The ownership lesson used this for a team directory under /srv. Here it is in miniature, in your home directory, with adm, a group an Ubuntu administrator already belongs to:
The s where the group's execute letter sits is the SGID bit. report.txt, created inside, took the group adm; plain.txt, created next to the directory, got your own group deploy. Both are -rw-rw-r--: SGID supplied only the group, and the group write bit came from the Ubuntu login umask 0002. Under a 0022 umask, as on RHEL, the same file would be -rw-r--r-- and a team could read but not edit it.
The sticky bit: everyone writes, you delete only your own
A directory writable by everyone has a problem: by default anyone who can write a directory can delete anything in it, including files they did not create. The sticky bit changes that rule so that inside the directory you may delete or rename only files you own (the directory's owner and root may too). This is why /tmp is safe to leave world-writable. Its final letter is a t:
The trailing t in drwxrwxrwt is the sticky bit, set with a leading 1. Put it on any drop-box style directory you make world-writable.
-rwsr-xr-x has a working SUID bit. But -rwSr--r--, with a capital S, means the SUID bit is set while the owner's execute bit beneath it is off, so nothing runs elevated and the file is usually just misconfigured. A capital T likewise means the sticky bit is set on a directory whose "others" execute bit is off. When you set these bits by hand, check that the letter came back lowercase, or the effect you meant is not really there.File capabilities, the baseline, and the sweep
SUID-root is a blunt tool: it grants all of root's power for one program. The narrower alternative is file capabilities, which grant one specific privilege. ping is the common example on Ubuntu: instead of being SUID-root it carries cap_net_raw, the single capability it needs to open a raw socket. Every SUID-root file and every file capability is a way to more privilege, so learn the baseline set on your hosts. find -perm -4000 lists files with the SUID bit (the leading - means "this bit set, whatever the others are"), -perm -2000 the SGID ones, and getcap -r every file capability. On a pristine Ubuntu 26.04 server the lists are short:
The SUID list is the classic tools plus the two sudo-rs binaries under /usr/lib/cargo/bin and the old sudo kept as sudo.ws. The SGID programs run with a group such as shadow or crontab so they can read one protected file: chage and expiry read password ageing, unix_chkpwd checks a password. In the capability list, =ep means the capability is granted and switched on when the program runs; snap-confine, snapd's sandbox helper, holds several powerful ones as =p (permitted), which it switches on itself.
RHEL 10 has a different set. Its chage is SUID-root rather than SGID, and it adds crontab, pkexec and the polkit helper (absent on the Ubuntu server), grub2-set-bootflag, mount.nfs, pam_timestamp_check and unix_chkpwd; its sudo is the classic one at /usr/bin/sudo. Its ping has no file capability at all: RHEL lets ordinary users send pings through the net.ipv4.ping_group_range setting instead. Some entries are powerful and still legitimate, such as newuidmap with cap_setuid (it sets up user ID ranges for containers), which is exactly why you compare against a known list rather than judge each line alone.
cap_setuid, cap_sys_admin, cap_dac_override, cap_dac_read_search and cap_sys_ptrace each let a program become root or read and change any file. A copy of an interpreter given cap_setuid is as much a backdoor as a SUID-root shell, and find -perm -4000 does not see it. Treat a new capability in the list exactly like a new SUID file.The point of knowing the baseline is that a new entry stands out. A SUID-root file or a capability outside the system directories, especially in /tmp, /var/tmp, /home or /dev/shm, is a classic backdoor: someone who briefly held root leaves it behind so they can return to root without an exploit. To see the sweep catch one of each, give a copy of sleep a harmless capability (cap_net_bind_service, the right to listen on ports below 1024) with setcap:
Now sweep the writable places for both kinds. Note -xdev: it keeps find on each starting point's own filesystem, which is also why find / -xdev never looks inside /tmp or /dev/shm, separate filesystems on Ubuntu, so they are named here explicitly:
Every SUID file from this lesson is reported. The two root-owned copies are the alarm: the one in /tmp is disarmed by nosuid, but a SUID-root file nobody can explain is evidence that someone held root, wherever it sits. myid and inert belong to deploy and grant only that account's identity (and inert cannot even run), yet on a real server they would still need an explanation. getcap found the planted capability. On a real server, record where and when such a file appeared, then escalate rather than just remove it: someone who held root may have changed more than one file, and the usual answer is incident response and a rebuild from known-good sources. Run the sweep on a schedule and compare it with your known-good list.
Try this
On a lab machine, make a root-owned SUID copy with sudo install -o root -g root -m 4755 /usr/bin/gnuid /var/tmp/showid (on RHEL use /usr/bin/id) and run /var/tmp/showid: the output shows euid=0(root). Do the same into /tmp/showid and run it: on Ubuntu the euid field is gone, and findmnt -no OPTIONS /tmp shows why. Run sudo find /tmp /var/tmp /dev/shm /home -xdev -type f -perm -4000 and sudo getcap -r /tmp /var/tmp /dev/shm /home and check that your files are the only findings, then compare sudo find / -xdev -type f -perm -4000 | sort with the baseline in this lesson. Delete both copies with sudo rm /var/tmp/showid /tmp/showid.
Takeaway
Keep the set of SUID programs and file capabilities on a host small and known, and sweep for both, because find -perm -4000 alone misses a capability backdoor. A SUID-root file outside the system directories means someone had root: treat it as an incident, not as clutter, and remember that nosuid disarms such files only on /tmp and /dev/shm, not on /var/tmp or the root filesystem.