SUID, SGID and the sticky bit

The special bits, their risks and nosuid.

Beginner14 min · lesson 13 of 29

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.

deploy@web01 · Ubuntu 26.04 LTS
$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 142552 Feb 2 2026 /usr/bin/passwd

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.)

deploy@web01 · Ubuntu 26.04 LTS
$ sudo mkdir -p /var/tmp/ess-special sudo install -o root -g root -m 4755 /usr/bin/gnuid /var/tmp/ess-special/showid ls -l /var/tmp/ess-special/showid
-rwsr-xr-x 1 root root 68232 Sep 27 08:33 /var/tmp/ess-special/showid
$ /var/tmp/ess-special/showid
uid=1001(deploy) gid=1001(deploy) euid=0(root) groups=1001(deploy),4(adm),27(sudo)

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:

deploy@web01 · Ubuntu 26.04 LTS
$ mkdir -p ~/special && cp /usr/bin/gnuid ~/special/myid chmod 4755 ~/special/myid ls -l ~/special/myid chown root ~/special/myid
-rwsr-xr-x 1 deploy deploy 68232 Sep 27 08:33 /home/deploy/special/myid chown: changing ownership of '/home/deploy/special/myid': Operation not permitted (os error 1)

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:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo install -o root -g root -m 4755 /usr/bin/gnuid /tmp/ess-special-showid ls -l /tmp/ess-special-showid
-rwsr-xr-x 1 root root 68232 Sep 27 08:33 /tmp/ess-special-showid
$ /tmp/ess-special-showid
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),4(adm),27(sudo)

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.

deploy@web01 · Ubuntu 26.04 LTS
$ findmnt -no OPTIONS /tmp
rw,nosuid,nodev,nr_inodes=1048576,inode64,usrquota

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:

deploy@web01 · Ubuntu 26.04 LTS
$ mkdir -p ~/special/shared chgrp adm ~/special/shared chmod 2775 ~/special/shared ls -ld ~/special/shared
drwxrwsr-x 2 deploy adm 4096 Sep 27 08:33 /home/deploy/special/shared
$ touch ~/special/shared/report.txt ~/special/plain.txt ls -l ~/special/shared/report.txt ~/special/plain.txt
-rw-rw-r-- 1 deploy deploy 0 Sep 27 08:33 /home/deploy/special/plain.txt -rw-rw-r-- 1 deploy adm 0 Sep 27 08:33 /home/deploy/special/shared/report.txt

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:

deploy@web01 · Ubuntu 26.04 LTS
$ ls -ld /tmp
drwxrwxrwt 13 root root 340 Sep 27 08:33 /tmp
$ sudo mkdir -p /srv/ess-special-drop sudo chmod 1777 /srv/ess-special-drop ls -ld /srv/ess-special-drop
drwxrwxrwt 2 root root 4096 Sep 27 08:33 /srv/ess-special-drop

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.

Lowercase means live, uppercase means idle
-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.
deploy@web01 · Ubuntu 26.04 LTS
$ cd ~/special touch inert chmod 4644 inert ls -l inert
-rwSr--r-- 1 deploy deploy 0 Sep 27 08:33 inert

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:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ sudo find / -xdev -type f -perm -4000 | sort
/usr/bin/chfn /usr/bin/chsh /usr/bin/fusermount3 /usr/bin/gpasswd /usr/bin/mount /usr/bin/newgrp /usr/bin/ntfs-3g /usr/bin/passwd /usr/bin/su /usr/bin/sudo.ws /usr/bin/umount /usr/lib/cargo/bin/su /usr/lib/cargo/bin/sudo /usr/lib/dbus-1.0/dbus-daemon-launch-helper /usr/lib/openssh/ssh-keysign
$ sudo find / -xdev -type f -perm -2000 | sort
/usr/bin/chage /usr/bin/crontab /usr/bin/expiry /usr/bin/ssh-agent /usr/sbin/pam_extrausers_chkpwd /usr/sbin/unix_chkpwd
$ sudo getcap -r /
/usr/bin/ping cap_net_raw=ep /usr/bin/mtr-packet cap_net_raw=ep /usr/lib/aarch64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper cap_net_bind_service,cap_net_admin,cap_sys_nice=ep /usr/lib/snapd/snap-confine cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_setgid,cap_setuid,cap_sys_chroot,cap_sys_ptrace,cap_sys_admin,cap_sys_resource=p

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.

deploy@rocky10 · Rocky Linux 10.2
$ sudo find / -xdev -type f -perm -4000 | sort
/usr/bin/chage /usr/bin/chfn /usr/bin/chsh /usr/bin/crontab /usr/bin/fusermount3 /usr/bin/gpasswd /usr/bin/mount /usr/bin/newgrp /usr/bin/passwd /usr/bin/pkexec /usr/bin/su /usr/bin/sudo /usr/bin/umount /usr/lib/polkit-1/polkit-agent-helper-1 /usr/sbin/grub2-set-bootflag /usr/sbin/mount.nfs /usr/sbin/pam_timestamp_check /usr/sbin/unix_chkpwd
$ sudo getcap -r /
/usr/bin/arping cap_net_raw=p /usr/bin/clockdiff cap_net_raw=p /usr/bin/newgidmap cap_setgid=ep /usr/bin/newuidmap cap_setuid=ep /usr/libexec/sssd/krb5_child cap_dac_read_search,cap_setgid,cap_setuid=p /usr/libexec/sssd/ldap_child cap_dac_read_search=p /usr/libexec/sssd/sssd_pam cap_dac_read_search=p /usr/sbin/mtr-packet cap_net_raw=ep
Some capabilities are as good as root
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:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo cp /usr/bin/gnusleep /var/tmp/ess-special/netcheck sudo setcap cap_net_bind_service=ep /var/tmp/ess-special/netcheck getcap /var/tmp/ess-special/netcheck
/var/tmp/ess-special/netcheck cap_net_bind_service=ep

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:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo find /tmp /var/tmp /dev/shm /home -xdev -type f -perm -4000 sudo getcap -r /tmp /var/tmp /dev/shm /home
/tmp/ess-special-showid /var/tmp/ess-special/showid /home/deploy/special/myid /home/deploy/special/inert /var/tmp/ess-special/netcheck cap_net_bind_service=ep

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.

The three special bits
SUID (4000)
On a program
runs as the file's owner, not you
On a directory
ignored on Linux
Spot it
the s in -rwsr-xr-x
SGID (2000)
On a program
runs as the file's group
On a directory
new files inherit its group
Spot it
the s in the group field
Sticky (1000)
On a directory
delete only files you own
Used on
/tmp and drop-boxes
Spot it
the t in drwxrwxrwt
Octal values stack: 4000 + 2000 + 1000. A special bit on the wrong kind of target is ignored, not an error.

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.

Quick check
01On an Ubuntu 26.04 server, a normal user finds /tmp/helper owned by root with mode -rwsr-xr-x and runs it. Do they get root?
Incorrect — The bit is set, but /tmp is mounted nosuid on Ubuntu 26.04, and nosuid tells the kernel to ignore it.
Incorrect — Being writable and nosuid are separate properties; writability does not enable SUID, and here nosuid disables it.
Correct — On Ubuntu 26.04 /tmp is a nosuid tmpfs, so the SUID bit is ignored and the program runs as the caller.
Incorrect — The listing shows the bit is present; the reason it does not take effect is the nosuid mount, not a lost bit.
02As an ordinary user you copy a program into your home directory, run chmod 4755 on it, and it keeps working. What did you gain?
Correct — SUID borrows the file's owner, and you own the copy; only root can make a file root-owned.
Incorrect — SUID runs a program as the file's owner, which here is you, not root.
Incorrect — chown to another user needs root, and the kernel clears SUID when an ordinary user changes a file's owner.
Incorrect — The bit stays; /home is not nosuid by default. It simply grants your own identity.
03Your nightly sweep has two new lines: a root-owned -rwsr-xr-x file at /var/tmp/.cache, and getcap shows /home/sam/.local/bin/python3 cap_setuid=ep. How should you treat them?
Incorrect — cap_setuid lets the program become any user, including root, so this capability is as dangerous as SUID-root.
Incorrect — Neither is nosuid by default on Ubuntu, and nosuid would not stop a file capability shown here from being a finding.
Correct — Only root can create either, so someone held root; capture the evidence and treat the host as compromised.
Incorrect — The kernel honours SUID on any filesystem without nosuid, and /var/tmp has none.

Related