OnCallReady

Lesson 4.9 · Filesystem, Permissions, Disk · 20 min read

setuid, setgid, sticky and umask

In plain words

Imagine three special stickers you can put on doors and tools. The first, on a tool, says "whoever uses me works with the owner's powers": that is setuid, how sudo and passwd can do root things for you. The second, on a room, says "everything put in here belongs to this room's team": setgid on a directory, so new files get the directory's group. The third, on a shared room, says "you may only throw away your own things": the sticky bit on /tmp.

And there is a rule about new things: every new file starts with a default set of permissions, and umask is a stencil that blocks some of them. umask 022 gives 644 files; umask 027 gives 640. find /usr/bin -perm -4000 lists every tool carrying the first sticker.

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=, called allowPrivilegeEscalation: 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

Why it helps

Setuid binaries are a standard privilege-escalation path, and "list the setuid binaries and explain each" is a common audit and interview task. Knowing that NoNewPrivileges=yes neutralises them for a service is how you harden units.

Setgid directories plus a suitable umask are the real fix for the most common shared-folder complaint, "my teammate cannot edit files I created", on shared build servers, NFS exports and deploy directories. The sticky bit explains the "Operation not permitted" when deleting in /tmp. And UMask= in systemd units decides whether a service creates world-readable files, a frequent finding when logs or uploads are readable by every user.

Commands in this lesson

ls stat touch find mkdir chgrp chmod rm umask

FAQ

Why does setuid not work on my shell script?

Linux ignores the setuid and setgid bits on interpreted scripts, anything started through a #! line. There are race conditions between the kernel checking the file and the interpreter opening it, plus environment-variable tricks, which make setuid scripts easy to exploit. If a script must run with privileges, grant it through sudo rules (/etc/sudoers.d) or run it as a systemd service with the right User=.

What does a capital S or T mean in ls -l?

The special bit is set but the execute bit underneath is not. -rwSr--r-- means setuid is on but the owner cannot execute, so setuid has no effect. drwxrwxrwT means sticky is on but others lack x. Lowercase s and t mean both bits are set. A capital letter is usually a mistake from an octal mode like 4644.

Is umask a subtraction?

No, it is a bit mask: bits set in the umask are cleared from the requested mode. Files are normally requested as 666 and directories as 777, so umask 022 yields 644 and 755. With subtraction, 666 minus 027 would be 637, which is not what happens; masking gives 640. That is also why new files never get x: the program did not request it.

Does setgid on a directory change existing files?

No. It only affects entries created after the bit was set: new files get the directory's group, and new subdirectories inherit both the group and the setgid bit. Existing files keep their groups, so after enabling it run chgrp -R team dir and set group permissions on the existing content. Files moved in with mv also keep their original group.

Why is it Operation not permitted, not Permission denied, when deleting in /tmp?

The directory's permission bits (1777) would allow the deletion, so it is not an access-denied case (EACCES). The sticky bit rule then refuses it because you do not own the file, and the kernel reports that as EPERM, "Operation not permitted". The different message is a useful clue that a sticky bit or ownership rule, not a missing permission, is in play.

In an interview Junior

What are setuid, setgid and the sticky bit, and where are they used?

Three extra bits, written as a fourth octal digit in front:

ls -l shows them as s/t; a capital S/T means the bit is set without the x underneath - almost always a mistake.

Also asked: What is umask, and what mode does a new file get with umask 027? · How would you set up a directory a whole team can share and edit? · What does NoNewPrivileges=yes protect against?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.