Why nine bits are not quite enough
How does an ordinary user run sudo and end up as root? How do you make a team folder where everyone's new files belong to the team? How does /tmp let everyone write but stop people deleting each other's files? Three extra bits - plus umask, which decides the mode a brand-new file starts with.
What you need to know already: 4.5 (bits and octal), 4.7 (w on a directory means delete), 4.3 (primary and supplementary groups), 2.26 (NoNewPrivileges=).
Three extra bits, one octal digit in front
4000 setuid run as the FILE'S OWNER, not as you
2000 setgid on a file: run as the file's group
on a DIRECTORY: new entries inherit the directory's group
1000 sticky on a directory: only the owner of a file may delete it
They combine like the others: 2775 is setgid + rwxrwxr-x, 3770 is sticky + setgid + rwxrwx---, 4755 is setuid + rwxr-xr-x. A three-digit mode like 755 is the same as 0755: no special bits.
They show up in ls -l in place of an x:
$ ls -ld /usr/bin/sudo /usr/bin/passwd /tmp
drwxrwxrwt 3 root root 4096 Sep 22 20:00 /tmp
-rwsr-xr-x 1 root root 64152 Sep 14 17:43 /usr/bin/passwd
-rwsr-xr-x 1 root root 277936 Sep 14 17:43 /usr/bin/sudo
$ stat -c '%a %n' /usr/bin/sudo /tmp
4755 /usr/bin/sudo
1777 /tmp
s in the owner's x slot = setuid; s in the group's x slot = setgid; t in other's x slot = sticky. A capital S or T means the bit is set but the x underneath is not:
$ touch f; chmod 4644 f; ls -l f
-rwSr--r-- 1 learner learner 0 Sep 22 20:00 f
setuid on a file nobody can execute does nothing - almost always a mistake.
setuid
/usr/bin/sudo is owned by root with the setuid bit, which is how an ordinary user's command can run as root at all. Same for passwd (it must write /etc/shadow).
Every setuid-root program is a possible privilege escalation - a way for an ordinary user to gain root through a bug in it - so this audit one-liner is worth remembering:
$ find /usr/bin -perm -4000 -type f
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/su
/usr/bin/mount
/usr/bin/chfn
/usr/bin/newgrp
find DIR walks everything under DIR and prints what matches the tests after it: -perm -4000 "has at least the setuid bit", -type f "is a regular file" (4.15 is all about find). An unexpected entry in that list is an incident.
NoNewPrivileges=yes in a systemd unit (2.26) is the directive that neuters setuid for a service: nothing it starts can gain privileges through the bit. Linux ignores setuid on scripts entirely - too easy to abuse.
Two more protections: a filesystem mounted with the nosuid option ignores the bit (common for /tmp and USB sticks), and chown clears setuid and setgid, so nobody can make a setuid-root program by copying one and changing its owner.
Later (Ch 15): Kubernetes has the same switch as
NoNewPrivileges=, calledallowPrivilegeEscalation: false.
setgid on a directory: the shared-folder fix
Normally a new file gets your primary group, so files in a team directory end up in whoever created them's group and the rest of the team cannot write them. Setgid on the directory makes new files inherit the directory's group:
$ mkdir /tmp/team
$ chgrp ops /tmp/team
$ chmod 2775 /tmp/team # 2 = setgid, 775 = rwxrwxr-x
$ ls -ld /tmp/team
drwxrwsr-x 2 learner ops 4096 Sep 22 20:00 /tmp/team
$ touch /tmp/team/a; mkdir /tmp/team/sub; ls -l /tmp/team
total 4
-rw-r--r-- 1 learner ops 0 Sep 22 20:00 a
drwxr-sr-x 2 learner ops 4096 Sep 22 20:00 sub
(A scratch directory in /tmp, so you can try it now; the mission below builds the real one, /srv/shared, owned by root.)
The file is group ops, not learner. New subdirectories inherit the setgid bit too (drwxr-sr-x), so the whole tree keeps the behaviour. Existing files are not changed - after turning it on, fix those with chgrp -R ops.
Note a is -rw-r--r--: the group is right but it is not group-writable. That part is umask's job, below.
sticky: /tmp
/tmp is 1777 - everyone can write, but the sticky bit means you can only delete your own files. Without it, any user could delete anyone else's temporary files, because delete permission comes from the directory (4.7).
$ sudo -u appuser rm /tmp/learner-notes.txt
rm: cannot remove '/tmp/learner-notes.txt': Operation not permitted
"Operation not permitted" (EPERM) rather than "Permission denied" (EACCES): the directory bits allowed it, the sticky rule refused it.
umask: the bits that are taken away
When a program creates a file it asks for a mode - normally 666 for files and 777 for directories. The umask is a per-process mask of bits that are then removed from that request:
files start at 666, directories at 777
umask 022 -> files 644, dirs 755 (the Ubuntu default)
umask 027 -> files 640, dirs 750 (group read, other nothing)
umask 077 -> files 600, dirs 700 (private)
umask 002 -> files 664, dirs 775 (group-writable - for shared dirs)
$ umask
0022
$ umask -S
u=rwx,g=rx,o=rx
$ umask 027; mkdir /tmp/um; touch /tmp/um/f; ls -ld /tmp/um /tmp/um/f
drwxr-x--- 2 learner learner 4096 Sep 22 20:00 /tmp/um
-rw-r----- 1 learner learner 0 Sep 22 20:00 /tmp/um/f
umask alone prints the current mask; -S shows what it allows, symbolically. It is a mask, not a subtraction: 666 with umask 027 removes group-w (already absent) and other-rwx, leaving 640. (Subtracting would give 637, which is not a mode anyone wants.)
Files never start with execute, which is why a new script is not runnable until you chmod +x it - that is umask doing its job, not a mistake.
umask is per process and inherited by children, so setting it in your shell affects only that shell. Set it in /etc/login.defs or /etc/profile for logins, or in a service's unit (UMask=0027) rather than hoping everyone remembers. A setgid team directory plus umask 002 for the team is the classic pairing.
What you can now do
- Spot setuid, setgid and sticky in
ls -land in octal. - List every setuid program on a box.
- Build a team directory with setgid and predict new files' modes from the umask.