Linux & Shell

Users, Permissions and Ownership

Users and groups, reading rwx and octal modes, chmod and chown, sudo, umask, setuid in plain words, and common permission mistakes.

Lesson 0113 min read#linux#permissions#security

On this page

  1. Words used in this lesson
  2. Users and groups are numbers with names
  3. Reading ls -l permissions
  4. Octal numbers: 755 and 644
  5. chmod: change the mode
  6. Directories: the x bit is the door
  7. Root ignores the switches
  8. chown: who owns it
  9. umask: the default for new files
  10. Special bits: setuid, setgid and sticky
  11. sudo: borrow root for one command
  12. Common mistakes and how to read them
  13. ACLs and capabilities, in one paragraph
  14. Try it
  15. Interview questions
  16. Key takeaways
  17. What's next

In one line: Every file in Linux has an owner, a group and three sets of read, write and execute switches (for the owner, the group and everyone else). The kernel checks those switches on every access. If you can read -rwxr-x--- and know what chmod, chown, umask and sudo change, you can fix most "Permission denied" errors in one minute.

Permissions are the first thing that breaks when you deploy to a real server. Your app runs as one user, the files belong to another, and the log folder is not writable. This lesson explains the model slowly, and then proves each rule with a real experiment.

You should have read lesson 0 (the file tree, ls -l, paths). The next lesson, Processes, Signals and Job Control, shows that every running program also has a user, and that is why permissions apply to programs, not only to files.

What I ran / checked. All commands were run on Ubuntu 24.04.5 LTS (kernel 6.18), with GNU coreutils, util-linux (runuser, setpriv) and sudo 1.9.15p5. A catch: my sandbox runs as root, and root ignores permission switches (I prove this below). So to see real "Permission denied" errors, I ran test commands as other users with runuser -u nobody -- ... and with setpriv --reuid=1500 --regid=1500 .... setpriv lets me act as a made-up user ID (1500) without creating any account, so I did not add or change any real users, and I never touched real system files except to read /etc/passwd, /etc/group and the permissions of /etc/shadow (I never printed its contents). Everything was done in /tmp/lab-linux-a/l1. Group names, sizes and times will differ on your machine. Sources I read: the Art of Command Line README (jlevy/the-art-of-command-line) and the pure-bash-bible README, both from raw.githubusercontent.com. The man7.org, GNU coreutils and Filesystem Hierarchy Standard sites were blocked, and man pages are removed in this sandbox, so the exact rule wording is my own summary, tested by experiment, not a quote of any manual.

Words used in this lesson

  • User is an identity that owns files and runs programs. Inside the kernel a user is just a number, the UID.
  • Group is a named set of users. Inside the kernel it is a number, the GID. A user has one primary group and can have more supplementary groups.
  • Root (UID 0) is the superuser. The kernel skips most permission checks for it.
  • Permission bits (or mode) are the switches r, w, x for owner, group and others.
  • Owner is the user who owns a file. Group owner is the group attached to it.
  • Octal mode is the number form of the bits, such as 755. Each digit is r=4 + w=2 + x=1.
  • chmod changes the mode. chown changes the owner and group.
  • umask is a mask that removes bits from the mode of every new file and directory.
  • sudo runs one command as another user, usually root, if the policy allows it.
  • setuid is a special bit on a program: it runs as the file's owner, not as the person who started it. setgid does the same for the group. On a directory it makes new files inherit the group.
  • Sticky bit on a directory means: you can delete only your own files there, even if the directory is writable by all.
  • ACL (access control list) is an extra, finer permission list on top of the basic one. We only mention it.

Users and groups are numbers with names

The kernel does not know "ada" or "www-data". It knows numbers. A table maps names to numbers. Look at your identity:

Shell
idid nobody
TEXT
uid=0(root) gid=0(root) groups=0(root)uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

That is your UID, your primary GID, and all groups you belong to. The table of users is the plain-text file /etc/passwd. Each line has seven fields separated by colons:

Shell
head -3 /etc/passwdgetent passwd nobody | tr ':' '\n' | nl
TEXT
root:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinbin:x:2:2:bin:/bin:/usr/sbin/nologin     1	nobody     2	x     3	65534     4	65534     5	nobody     6	/nonexistent     7	/usr/sbin/nologin

The seven fields are: name, password placeholder (x means the real hash is elsewhere), UID, primary GID, a comment (the "GECOS" field, often the full name), home directory, and the login shell. The shell /usr/sbin/nologin means "this account cannot log in interactively". Service accounts such as www-data or postgres are made that way on purpose: a web server needs an identity to own files, but nobody should be able to log in as it.

(getent passwd name is better than grep on the file because it also asks other sources, like a company login directory, if the machine is set up that way.)

Count the shells on this machine:

Shell
awk -F: '{print $7}' /etc/passwd | sort | uniq -c | sort -rn
TEXT
     32 /sbin/nologin     23 /usr/sbin/nologin      5 /bin/bash      1 /bin/sync

Most accounts (55 of 61) cannot log in. That's normal. The UID ranges are by convention: 0 is root, low numbers (1 to 999) are system accounts, and 1000 and up are normal people. The file /etc/login.defs on my machine says UID_MIN 1000.

Groups live in /etc/group:

Shell
head -4 /etc/group
TEXT
root:x:0:daemon:x:1:bin:x:2:sys:x:3:

Each line is name:x:GID:member,member. Groups are how you share: put ada and bob in the group devs, give the folder to devs, and both can write.

The password hashes are not in /etc/passwd, because everybody can read that file (it must be readable so ls can turn UID 1001 into a name). They are in /etc/shadow. Look at the permissions, not the content:

Shell
ls -l /etc/passwd /etc/group /etc/shadowrunuser -u nobody -- cat /etc/shadow
TEXT
-rw-r--r-- 1 root root   1044 Oct 12 03:42 /etc/group-rw-r--r-- 1 root root   3528 Oct 12 03:42 /etc/passwd-rw-r----- 1 root shadow 1470 Oct 12 03:42 /etc/shadowcat: /etc/shadow: Permission denied

Anyone can read passwd. Only root and the group shadow can read shadow. That is the permission system doing its job, and it is a nice first example, so let's read it properly.

Reading ls -l permissions

One line of ls -l has ten characters at the start:

TEXT
-rwxr-x---│└┬┘└┬┘└┬┘│ │  │  └── others (everyone else): --- = nothing│ │  └───── group:  r-x = read and execute│ └──────── owner:  rwx = read, write, execute└────────── type: - file, d directory, l symlink, c/b device

So -rwxr-x--- says: a normal file; the owner can read, write and run it; the group can read and run it; everyone else gets nothing.

Which set applies to you? The kernel picks one set, in this order:

  1. If you are the owner, the owner set applies. (Even if the group set is more generous. Only the owner set counts.)
  2. Else, if you are in the file's group, the group set applies.
  3. Else, the others set applies.

This "first match wins" rule has a surprising effect: an owner with --- and a group with rwx means the owner is locked out, while other group members can get in. It is rare, but it shows the logic.

What do r, w and x mean? It depends on whether it is a file or a directory.

BitOn a fileOn a directory
r readread the contentslist the names inside (ls)
w writechange the contentscreate, rename and delete entries inside
x executerun it as a programenter it (cd) and reach things inside by name

The directory column surprises almost everyone. We test it in a minute.

Octal numbers: 755 and 644

Each triple is a number. Read is 4, write is 2, execute is 1. Add them up:

LettersMathDigit
---00
--x11
-w-22
-wx2+13
r--44
r-x4+15
rw-4+26
rwx4+2+17

Three digits: owner, group, others. So 755 is rwxr-xr-x and 644 is rw-r--r--. Here is the mapping, produced by chmod and printed by stat:

Shell
for m in 600 640 644 664 700 750 755 777 400; do chmod $m report.txt; stat -c '%a %A' report.txt; done
TEXT
600 -rw-------640 -rw-r-----644 -rw-r--r--664 -rw-rw-r--700 -rwx------750 -rwxr-x---755 -rwxr-xr-x777 -rwxrwxrwx400 -r--------

(stat -c '%a %A' prints the octal form and the letter form. It is the quickest way to learn both.) Numbers worth knowing by heart:

  • 644: normal file. The owner edits it, everyone can read it.
  • 600: private file, such as an SSH private key.
  • 755: program or directory. Everyone can run or enter it, only the owner can change it.
  • 700: private directory.
  • 664 / 775: shared with the group.
  • 777: everyone can do everything. Almost always wrong. See the mistakes below.

chmod: change the mode

chmod has two forms. The octal form sets all nine bits: chmod 640 file. The symbolic form changes only some bits. It reads as who (u user/owner, g group, o others, a all), an operator (+ add, - remove, = set exactly), and what (r, w, x).

Here is a run, starting at 644:

Shell
chmod o-r report.txt;  ls -l report.txt     # remove read from otherschmod o+r report.txt; chmod g+w report.txt; ls -l report.txtchmod u-w report.txt; ls -l report.txtchmod u=rw,g=r,o= report.txt; ls -l report.txt
TEXT
-rw-r----- 1 root root 6 Oct 12 03:44 report.txt-rw-rw-r-- 1 root root 6 Oct 12 03:44 report.txt-r--rw-r-- 1 root root 6 Oct 12 03:44 report.txt-rw-r----- 1 root root 6 Oct 12 03:44 report.txt

Symbolic mode is nice when you only want to "add execute" without caring about the rest: chmod +x script.sh. Octal is nice when you want one exact result, which is better for documentation and deploy scripts.

Two details that bite:

  • chmod +x file adds x for all classes, but it is limited by your umask (see below) in some cases. To be exact, write chmod u+x file.
  • The capital X in chmod -R u=rwX,go=rX dir means "add execute only to directories (and to files that already have an execute bit)". That fixes a whole tree without making every plain file executable:
Shell
chmod -R 600 treechmod -R u=rwX,go=rX treefind tree -exec stat -c '%a %n' {} + | sort -k2
TEXT
755 tree644 tree/a755 tree/sub644 tree/sub/b

Directories got 755 (they needed x to be usable) and files got 644. The other classic way to do the same is two find commands: find tree -type d -exec chmod 755 {} + and find tree -type f -exec chmod 644 {} +.

chmod -R 777 is not a fix. It is a new problem. If a deploy fails with "Permission denied", the cause is that the wrong user owns the files, or a directory lacks x. Opening everything to the world hides the cause, lets any account on the machine (and any compromised web process) modify your code, and some tools (like SSH) will refuse to work with files that are too open.

Directories: the x bit is the door

Let me prove the directory column of the table. I made a directory d (owned by root) with one file, d/file.txt (mode 644, so the file itself is readable by everyone). Then I changed only the directory's bits and tried to read as nobody:

Shell
N="runuser -u nobody --"# 755: r and x for others$N ls d ; $N cat d/file.txt# 704: others have r but not xchmod 704 d; $N ls d; $N cat d/file.txt; $N ls -l d# 701: others have x but not rchmod 701 d; $N ls d; $N cat d/file.txt# 700: others have nothingchmod 700 d; $N cat d/file.txt
TEXT
file.txtsecretfile.txtcat: d/file.txt: Permission deniedls: cannot access 'd/file.txt': Permission deniedtotal 0-????????? ? ? ? ?            ? file.txtls: cannot open directory 'd': Permission deniedsecretcat: d/file.txt: Permission denied

(Read the output in order: the first two lines are mode 755, the next four lines plus the question-mark row are 704, then 701, then 700.) Here is what each shows:

  • 755 (r-x for others): can list and can read the file.
  • 704 (r--, no x): ls d shows the name file.txt, but you cannot open it, and ls -l prints question marks, because getting size or owner needs access to the entry, which needs x.
  • 701 (--x, no r): ls d fails (cannot list), but cat d/file.txt works if you already know the name. This is a "drop box" style trick, and it surprises people.
  • 700: nothing works.

So x on a directory means "may pass through". It is checked on every directory in the path. If /home/ada is 700, then /home/ada/site/index.html is unreachable for a web server user, even if index.html is 644 and site is 755. Tool to debug it: namei -l /full/path/to/file prints the permissions of each path part (it comes with util-linux).

Who can delete a file? The directory decides

Another surprise. To delete or rename a file you need w (and x) on the directory that contains it. The file's own permissions do not matter. I tested:

Shell
chmod 777 d; chmod 444 d/file.txt        # file is read-only for everybodyrunuser -u nobody -- rm -f d/file.txt    # but the directory is openecho "rm status=$?"
TEXT
rm status=0

The read-only file was deleted. (Plain rm without -f would ask "remove write-protected file?" first, but the system allows it.) Reverse case: a directory without w for the user protects every file inside it from deletion, even files the user "owns" with 666:

Shell
chmod 755 d; echo again > d/f2.txt; chmod 666 d/f2.txtrunuser -u nobody -- rm d/f2.txt
TEXT
rm: cannot remove 'd/f2.txt': Permission denied

Root ignores the switches

Everything above applied to a normal user. Root (UID 0) is special: the kernel lets root read and write nearly everything, no matter the mode.

Shell
chmod 400 report.txt                    # read-only for the owner, nothing for othersecho "root writes anyway" >> report.txt && echo "root: write ok"
TEXT
root: write ok

Even with -r--------, root could append. (Root can still be blocked by a few things: a read-only mounted file system, the "immutable" attribute set by chattr +i, or security modules such as SELinux and AppArmor.) This is why I always test permission fixes as the real service user, not as root. If you test as root, everything works, and then production fails.

Tools for testing as another user:

  • sudo -u www-data some-command: run one command as that user (needs sudo rights).
  • runuser -u nobody -- cmd or su -s /bin/sh -c 'cmd' nobody: when you are root.
  • setpriv --reuid=1500 --regid=1500 --clear-groups cmd: run as an arbitrary numeric ID, even one with no account.

chown: who owns it

chown user:group file changes the owner and group. Only root can give a file away to somebody else. (A normal user cannot give away files, which prevents dodging disk quotas and planting files with another name.) A normal user can change the group of their own file, but only to a group they belong to.

I tested with made-up IDs, because chown accepts numbers:

Shell
chown 1001:1002 report.txtls -l report.txtrunuser -u nobody -- sh -c 'echo x >> report.txt'runuser -u nobody -- chmod 777 report.txt
TEXT
-rw-r--r-- 1 1001 1002 25 Oct 12 03:44 report.txtsh: 1: cannot create report.txt: Permission deniedchmod: changing permissions of 'report.txt': Operation not permitted

Look at the owner column: 1001 1002. There is no user with that number, so ls prints the raw numbers. This also happens when you copy files from another machine where the UIDs map to other names. (Docker bind mounts show this a lot.) Then the two errors:

  • Writing failed: nobody is not the owner, not in group 1002, so the "others" set r-- applies.
  • chmod failed with a different message: "Operation not permitted". That means: you are not the owner. Only the owner (or root) may change the mode.

Notice the two messages: "Permission denied" is about reading, writing or running. "Operation not permitted" is about an action that needs ownership or a privilege (chmod, chown, delete from a sticky directory, kill another user's process).

As the owner, the same user can change the mode:

Shell
chown 65534:65534 report.txtls -l report.txtrunuser -u nobody -- chmod 600 report.txtls -l report.txt
TEXT
-rw-r--r-- 1 nobody nogroup 25 Oct 12 03:44 report.txt-rw------- 1 nobody nogroup 25 Oct 12 03:44 report.txt

chown -R user:group dir changes a whole tree. Use it carefully: aim it at the exact directory. A typo like chown -R www-data / on a real server changes the owner of the whole system and can break sudo, ssh and logins.

Groups in action

Groups give shared access. I made a file owned by user 1001 and group 2000 with mode 660 (owner and group can read and write, others nothing) and tried it as a made-up user 1500:

Shell
chown 1001:2000 report.txt; chmod 660 report.txtsetpriv --reuid=1500 --regid=1500 --clear-groups cat report.txtsetpriv --reuid=1500 --regid=1500 --groups=2000 cat report.txtsetpriv --reuid=1500 --regid=1500 --groups=2000 id
TEXT
cat: report.txt: Permission deniedhelloroot writes anywayokuid=1500 gid=1500 groups=1500,2000

Without group 2000, the user was denied. With group 2000 in the list, the user could read. That is all a "group" is.

Group changes need a new login. If you add yourself to a group with sudo usermod -aG docker ada, your running shell does not see it. Group membership is read at login and copied into each process. Log out and in again (or run newgrp docker) before testing. Also always use -a with -G. Without -a, usermod -G replaces all your supplementary groups.

umask: the default for new files

Why is a new file 644 and not 666? Because of the umask. It is a mask of bits to remove from the maximum mode. For files, the maximum is 666 (new files never start with execute). For directories it is 777.

TEXT
umask 022   means remove 022 (group w, others w)files:        666 - 022 = 644directories:  777 - 022 = 755

(It is a bitwise "and not", not real subtraction, but for common values the subtraction trick works.) I tried several values in subshells (the parentheses make the change temporary):

Shell
umaskumask -S( umask 022; touch f022; mkdir d022; stat -c '%a %n' f022 d022 )( umask 002; touch f002; mkdir d002; stat -c '%a %n' f002 d002 )( umask 077; touch f077; mkdir d077; stat -c '%a %n' f077 d077 )( umask 000; touch f000; mkdir d000; stat -c '%a %n' f000 d000 )
TEXT
0022u=rwx,g=rx,o=rx644 f022755 d022664 f002775 d002600 f077700 d077666 f000777 d000
  • 022: the common default. Others can read your new files.
  • 002: used on systems where each user has a private group, so group members can write too.
  • 077: private by default. Good for secrets and in cron jobs that write sensitive files.
  • 000: nothing is removed. New files are 666. It is a bad idea except in tests.

The umask command shows or sets the value for the current shell and its children. To make it permanent, put it in ~/.bashrc or in the file that your service manager reads (a systemd unit has UMask=). If your files come out 664 on one server and 644 on another, the umask differs.

An important detail: umask never adds x. Even with umask 0, a new file is 666:

Shell
( umask 0; touch fx; stat -c '%a' fx )printf '#!/bin/sh\necho running as $(id -un)\n' > run.sh./run.shsh run.shchmod u+x run.sh./run.sh
TEXT
666/bin/bash: line 33: ./run.sh: Permission deniedrunning as rootrunning as root

A new script is not executable. ./run.sh fails. Two fixes: run it through the interpreter (sh run.sh, which only needs read permission), or chmod u+x run.sh. Git remembers the execute bit, so git update-index --chmod=+x file fixes it for a repo.

Special bits: setuid, setgid and sticky

There are three extra bits, written as a fourth, leading octal digit: setuid = 4, setgid = 2, sticky = 1. So 4755 is rwsr-xr-x. In the letter form they replace the x position (a lowercase s or t means x is also set; uppercase S or T means it is not).

Sticky bit: shared folders

/tmp is writable by everybody. What stops me from deleting your files there? The sticky bit (t at the end):

Shell
ls -ld /tmpmkdir shared; chmod 1777 sharedrunuser -u nobody -- touch shared/nobody.txtsetpriv --reuid=1500 --regid=1500 --clear-groups touch shared/u1500.txtls -l sharedsetpriv --reuid=1500 --regid=1500 --clear-groups rm shared/nobody.txtsetpriv --reuid=1500 --regid=1500 --clear-groups rm shared/u1500.txt && echo "own file removed"
TEXT
drwxrwxrwt 88 root root 20480 Oct 12 03:44 /tmp-rw-r--r-- 1 nobody nogroup 0 Oct 12 03:44 nobody.txt-rw-r--r-- 1 1500 1500 0 Oct 12 03:44 u1500.txtrm: cannot remove 'shared/nobody.txt': Operation not permittedown file removed

(Whitespace in the ls -l of the second file was tidied for width.) With the sticky bit, user 1500 could delete only their own file. Without it (chmod 777 shared), the same user deleted nobody's file. I tried: the output was "without sticky: deleted someone else's file". Remember: /tmp-like shared folders need 1777, not 777.

Setgid on a directory: team folders

On a directory, setgid (s in the group position) makes new files take the directory's group instead of the creator's primary group. This is what you want for a shared project folder:

Shell
mkdir team; chown 0:2000 team; chmod 2775 teamsetpriv --reuid=1500 --regid=1500 --groups=2000 touch team/new.txtsetpriv --reuid=1500 --regid=1500 --groups=2000 mkdir team/subls -ln teammkdir plain; chown 0:2000 plain; chmod 775 plainsetpriv --reuid=1500 --regid=1500 --groups=2000 touch plain/new.txtls -ln plain
TEXT
total 4-rw-r--r-- 1 1500 2000    0 Oct 12 03:44 new.txtdrwxr-sr-x 2 1500 2000 4096 Oct 12 03:44 subtotal 0-rw-r--r-- 1 1500 1500 0 Oct 12 03:44 new.txt

In team (setgid), the new file got group 2000, and even the new subdirectory sub inherited the s bit. In plain (no setgid), the file got group 1500, the creator's own group, so the rest of the team might not be able to edit it. (Note the files are 644, because the umask is 022. For full team editing you also want umask 002.)

Setuid: run as the owner

Normal programs run with the identity of whoever started them. A setuid program runs with the identity of the program file's owner. That is how a normal user can change their own password: only root can write /etc/shadow, but passwd is owned by root with the setuid bit, so it runs as root for that one job.

Shell
stat -c '%a %A %U %n' /usr/bin/passwd /usr/bin/sudo /usr/bin/sufind /usr/bin /usr/sbin -xdev -perm -4000 -type f | sort
TEXT
4755 -rwsr-xr-x root /usr/bin/passwd4755 -rwsr-xr-x root /usr/bin/sudo4755 -rwsr-xr-x root /usr/bin/su/usr/bin/chfn/usr/bin/chsh/usr/bin/gpasswd/usr/bin/mount/usr/bin/newgrp/usr/bin/passwd/usr/bin/su/usr/bin/sudo/usr/bin/umount

That s where x would be is the setuid bit. This short list is the whole set of setuid-root programs on this minimal image. Here is the effect, shown with a copy of the id program in my scratch folder:

Shell
cp /usr/bin/id ./myid; chown 0:0 myid; chmod 4755 myidsetpriv --reuid=1500 --regid=1500 --clear-groups ./myidchmod u-s myidsetpriv --reuid=1500 --regid=1500 --clear-groups ./myid
TEXT
uid=1500 gid=1500 euid=0(root) groups=1500uid=1500 gid=1500 groups=1500

In the first run id shows euid=0(root). The real UID is still me (1500), but the effective UID, the one used for permission checks, is root. Remove the bit and the effective UID goes back to 1500. (I deleted the copy right after.) A setuid-root program is powerful, so any bug in it can become a way to gain root. That is why the list is small and kept up to date. Two safety rules:

  1. Linux ignores setuid on scripts. I tested a #!/bin/sh script with 4755. It ran as 1500 anyway. (Printing the name failed because 1500 has no account, but the number was right: 1500.) Do not try to make setuid scripts. Use sudo with a narrow rule instead.
  2. Be suspicious of unknown setuid files. Auditors run find / -xdev -perm -4000 -type f for this very reason, as I did above.

/etc/shadow stayed unreadable for user 1500, as expected:

Shell
setpriv --reuid=1500 --regid=1500 --clear-groups sh -c 'id -u; ls -l /etc/shadow; cat /etc/shadow'
TEXT
1500-rw-r----- 1 root shadow 1470 Oct 12 03:42 /etc/shadowcat: /etc/shadow: Permission denied

sudo: borrow root for one command

sudo ("superuser do") runs a command as another user (by default root) after checking the policy in /etc/sudoers and /etc/sudoers.d/. You normally type your own password, not root's. Some good habits:

  • sudo -l lists what you may run.
  • sudo -u www-data command runs as that user, which is the best way to test file access for a service.
  • Edit the policy only with sudo visudo. It checks the syntax before saving. A broken sudoers file can lock you out of admin access.
  • Do not sudo your whole day. Use it per command, so mistakes stay small. sudo -i or sudo su - gives a root shell, which is easy to forget you are in.

The policy of my sandbox, for the user root, as printed by sudo -n -l:

TEXT
Matching Defaults entries for root on vm:    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_ptyUser root may run the following commands on vm:    (ALL : ALL) ALL

env_reset means sudo wipes most of your environment variables. secure_path means commands run with a fixed PATH, so a malicious folder in your own PATH cannot trick sudo. A test shows that the environment really changes:

Shell
sudo -n -u nobody sh -c 'echo user=$(id -un) home=$HOME'
TEXT
user=nobody home=/nonexistent

The redirect trap

This one fools experienced people. Say a file is not writable by you, and you try sudo echo hello > /root/file. The redirect > is done by your shell, before sudo runs. So the file is opened with your permissions, and it fails. I reproduced it with a user that cannot write to a folder:

Shell
runuser -u nobody -- sh -c 'sudo echo x > ro/f'
TEXT
sh: 1: cannot create ro/f: Permission denied

The error came from the shell opening ro/f before sudo was even started. Fixes: let the privileged side do the writing, with echo x | sudo tee ro/f > /dev/null (the tee runs as root), or sudo sh -c 'echo x > ro/f' (the whole redirect runs inside the root shell). We study redirection in lesson 3.

Common mistakes and how to read them

A short field guide:

SymptomLikely causeFix
Permission denied on a file you can seeWrong class: you are in "others". Or a directory in the path lacks xls -l, id, namei -l path, then chown/chmod the right thing
Permission denied running ./script.shNo x bit (or the file system is mounted noexec)chmod u+x script.sh
Operation not permitted on chmod/chownYou do not own itUse sudo, or fix who the owner is
App cannot write to logs/Folder owned by root (maybe created by sudo mkdir)chown appuser: logs, and create directories as the service user
Files in a repo owned by rootYou ran sudo npm install or sudo git pullsudo chown -R "$USER": project-dir once, then stop using sudo for it
SSH says UNPROTECTED PRIVATE KEY FILEKey file is readable by otherschmod 600 ~/.ssh/id_ed25519 and chmod 700 ~/.ssh
New team files not editable by teammatesGroup is wrong, or umask is 022setgid directory plus umask 002
Odd numbers in ls -l instead of namesUID not known on this machine (copied or mounted from elsewhere)Map the IDs, or chown to a local user

The table's SSH item is described from general knowledge. I did not run an SSH server here. The rest I reproduced above.

The "just sudo it" habit creates root-owned files. If you create a folder with sudo, later non-root commands will fail to write into it. In my test, a root-created oops file inside a folder owned by nobody could not be edited by nobody (cannot create proj/oops: Permission denied), but nobody could delete it, because deleting needs w on the directory, which nobody owns. Two different rules at work, and both come from this lesson.

A clean way to find stray root-owned files is find . -user root inside the project. And chown --from=OLD NEW file changes the owner only if the current owner is OLD, which is a safe guard for bulk fixes.

ACLs and capabilities, in one paragraph

The basic model has just three classes. If you need "Ada may write but Bob may only read, and the rest of the team nothing", you need ACLs (getfacl / setfacl). A + at the end of the permission string in ls -l shows that a file has an ACL. They are not installed on my minimal sandbox, so I did not run them. Capabilities split root's power into small pieces (for example "may bind to port 80" without being full root). A web server can get CAP_NET_BIND_SERVICE instead of running as root. Docker and systemd use both ideas heavily. You will meet them again if you work on container security; for daily work, the basic model plus sudo covers most cases.

Try it

Make a scratch folder. If you are not root, you can run every step directly. If you are root (inside Docker, for example), use runuser -u nobody -- or setpriv --reuid=1500 --regid=1500 --clear-groups in front of the "as another user" steps.

  1. Create secret.txt. Set the mode to 640. Write the letter form without looking, then check with stat -c '%a %A' secret.txt.
  2. What octal mode gives rwxr-x---? What mode gives rw-rw-r--?
  3. Create a directory box with a file box/note.txt inside. Remove the r bit from the directory for others (chmod o-r box) while keeping x. Can another user still cat box/note.txt? Can they ls box?
  4. Run umask, then ( umask 077; touch private; ls -l private ). What mode do you get, and why not 600 from 666 - 077? (Try the maths.)
  5. Run find /usr/bin -xdev -perm -4000 -type f. Pick one result and say why it needs the setuid bit.
  6. Explain why sudo echo hi > /root/test.txt fails for a normal user, and give two commands that work.
  7. A file is -rw-r--r-- 1 ada devs. You are bob, in group devs. Can you write to it? Why?
  8. A deploy script does chmod -R 777 /var/www/app. Name two problems with this.

Answers

  1. 640 is rw-r-----. stat prints 640 -rw-r-----.
  2. rwxr-x--- is 750. rw-rw-r-- is 664.
  3. Yes, another user can still cat box/note.txt (they have x on the directory, so they can reach the file by name, and the file is readable). They cannot ls box (that needs r).
  4. You get 600 (-rw-------). The mask 077 has the group and other bits set, and the rule clears exactly those bits from rw-rw-rw-, which leaves rw-------. Digit subtraction (6-0, 6-7, 6-7) would give nonsense for the last two digits, which shows that "subtract" is only a shortcut that works when the mask digits are not bigger than the mode digits.
  5. For example passwd. It must write /etc/shadow, which only root can write, so it runs as root for the one task of changing your hash. (Other answers: sudo switches users, mount mounts file systems.)
  6. The > redirect is performed by your shell as you, before sudo runs, so opening /root/test.txt fails. Working versions: echo hi | sudo tee /root/test.txt > /dev/null and sudo sh -c 'echo hi > /root/test.txt'.
  7. No. You are not the owner (ada), so the owner set does not apply. You are in devs, so the group set applies, and it is r--. You can read but not write. The "others" write bit is also off. Fix: chmod g+w file.
  8. It gives every user and every compromised process on the machine write access to your code, and it makes all files executable, which is wrong for data files. It also hides the real cause (usually the wrong owner) and some tools complain about group- or world-writable files.

Interview questions

Explain the rwx permissions on files and on directories. (Must know)

For a file, r lets me read the contents, w lets me change them and x lets me run it. For a directory, r lets me list the names, w lets me create, rename or delete entries inside, and x lets me enter it and reach things by name. The directory x bit is checked on every folder in a path. I proved it by taking r away from a directory while keeping x: the file could still be read by name, but ls failed.

A user can read a file but cannot delete it, or can delete it but cannot read it. How is that possible? (Senior signal)

Deleting is controlled by the directory, not the file. To remove a file I need w and x on the folder that contains it. So I can delete a 000 file if the folder is writable by me, and I cannot delete a 666 file if the folder is not. The sticky bit adds one more rule: in a sticky directory only the file's owner (or the directory owner or root) can delete it.

How does the kernel decide which of owner, group or other applies? (Senior signal)

It checks in order and uses the first match: if my UID is the owner, the owner bits decide; otherwise, if one of my groups is the file's group, the group bits decide; otherwise the others bits decide. It does not combine them. So an owner with fewer rights than "others" is still limited by the owner bits.

What does chmod 755 mean, and what is 644? What would you use for an SSH private key? (Must know)

755 is rwxr-xr-x: owner can do everything, others can read and run. I use it for programs and directories. 644 is rw-r--r--: the owner can edit, everyone can read. I use it for normal files. A private key should be 600 (and ~/.ssh should be 700), because SSH refuses keys that other users can read.

What is the umask and why do new files get 644? (Must know)

The umask lists permission bits to remove from the default modes: 666 for files and 777 for directories. With 022 the result is 644 for files and 755 for directories. New files never get x automatically, which is why I must chmod +x a new script. I would use 077 for secrets and 002 for team folders. The umask is per process, so cron jobs and systemd units can have a different one from my login shell.

What is the difference between Permission denied and Operation not permitted? (Senior signal)

Permission denied (EACCES) usually means the mode bits (or a directory x bit on the way) block reading, writing or running. Operation not permitted (EPERM) means the action needs ownership or a privilege, for example chmod on a file I do not own, chown by a non-root user, or deleting another user's file in a sticky directory. In my test, chmod 777 by nobody on a file owned by uid 1001 gave Operation not permitted, while writing to the same file gave Permission denied.

What is setuid? Why does passwd need it, and what are the risks? (Must know)

A setuid program runs with the effective UID of its file owner instead of the user who started it. passwd is owned by root and has setuid, so an ordinary user can update their own entry in /etc/shadow, which only root can write. I saw it with a copy of id: the real UID stayed 1500 but euid=0. The risk is that a bug in a setuid-root program can give an attacker root, so the list should be short. I can audit it with find / -xdev -perm -4000 -type f. Also, Linux ignores setuid on shell scripts.

What do the sticky bit and setgid on a directory do? Give a use for each. (Senior signal)

The sticky bit (1777, shown as t) lets everyone write to a shared directory but lets each user delete only their own files. /tmp is the standard example. Setgid on a directory (2775, shown as s) makes new files and subdirectories inherit the directory's group, which is useful for a shared project folder, together with umask 002. I tested both: without sticky, one user deleted another's file; without setgid, new files got the creator's own group.

Why does sudo echo hi > /etc/some-file fail, and how do you do it correctly? (Trap)

The shell performs the redirect before sudo starts, so the file is opened as my normal user and the open fails. Only echo would run as root, and that does not need privileges anyway. The fix is to make the privileged process do the write: echo hi | sudo tee /etc/some-file > /dev/null, or sudo sh -c 'echo hi > /etc/some-file'. For an append, use sudo tee -a.

You added yourself to the docker group but still get "permission denied". Why? (Trap)

Group membership is read when a session starts and is then copied into each process, so my current shell still has the old list. I need to log out and in again, or start a shell with newgrp docker, and then check with id. Also I should check I used usermod -aG; without -a it replaces my groups. And I should remember that the docker group is effectively root access, so I should add only trusted people.

A web app gets 403 on /home/ada/site/index.html, though that file is 644. What do you check? (Senior signal)

The file mode is not the whole story. The web server's user needs x on every parent directory: /home and /home/ada and site. A home directory is often 700 or 750, which blocks the server. I run namei -l /home/ada/site/index.html to see all the modes along the path, and I test as the real user with sudo -u www-data cat /home/ada/site/index.html. The better fix is to serve files from a dedicated directory such as /srv/site or /var/www, instead of opening up a home folder. If SELinux or AppArmor is active, I also check their logs.

Why is chmod -R 777 a bad fix? What would you do instead? (Must know)

It removes all protection: any local user or compromised process can change or replace the files, it marks plain files as executable, and it hides the real cause. The right way is to find the real problem: who runs the app (ps, id), who owns the files (ls -l), and which directory in the path blocks (namei -l). Then set the right owner (chown appuser:appgroup), and use 755 for directories and 644 for files, with 640 or 600 for secrets. For writable data folders I give the app user ownership of only those folders.

Key takeaways

  • Users and groups are numbers (UID, GID) mapped to names by /etc/passwd and /etc/group. Hashes live in /etc/shadow, which only root and the shadow group can read.
  • -rwxr-x--- means type, then owner, group and others. The kernel picks the first matching class and uses only that one.
  • Octal: r=4, w=2, x=1. 644 for files, 755 for programs and directories, 600 and 700 for private things. Avoid 777.
  • On a directory, r lists, w creates and deletes, and x lets you pass through. Deleting a file depends on the directory, not on the file.
  • Root ignores the mode bits, so test permission fixes as the real service user (sudo -u, runuser, setpriv).
  • chown needs root to give files away. chmod needs ownership. Permission denied and Operation not permitted are different errors.
  • umask removes bits from new files: 022 gives 644 and 755. New files never start with x.
  • Special bits: setuid (run as owner, as in passwd), setgid on directories (inherit the group), sticky (/tmp: delete only your own). Setuid is ignored on scripts.
  • sudo cmd > file does not work, because your shell does the redirect. Use tee or sudo sh -c.

What's next

Next: Processes, Signals and Job Control, Lesson 2. You will see that every running program is a process with a user and a parent, learn ps and top, run commands in the background, send signals such as SIGTERM and SIGKILL, and read exit codes. Practise now: run id and ls -l on three files in your home directory, and say out loud which class (owner, group or others) applies to you for each.

Published by Jainik · Last updated · How these lessons are made

Share this lessonWhatsAppLinkedInX

Keep learning

Related lessons

Part of the study guide

Related series