Mert Özen Further With Every Line
DevOps Linux and the Shell (for Developers) — #03

Users, Groups and Permissions: rwx, chmod, chown, umask, sudo and Service Accounts

Mert Özen Nov 14, 2026 14 min 4 views
Users, Groups and Permissions: rwx, chmod, chown, umask, sudo and Service Accounts

On Linux you run an application neither as root nor as your own user, but as a service user that belongs to it alone and cannot log in; for Bookstore that is a bookstore user, a separate deploy user that writes releases, and a secrets.env with mode 640. With real output we cover what rwx means on directories, chmod/chown/umask, setgid directories, the sticky bit, ACLs, a sudoers rule written with visudo that allows a single command, and the pitfalls of chmod 777 and sudo.

Version note: The commands in this post were run on a test VM with Ubuntu Server 24.04.5 LTS (kernel 6.8, sudo 1.9.15p5, coreutils 9.4, acl 2.3.2) and the ASP.NET Core 10.0.15 runtime.

Guess First

There is a folder named catalog in your home folder with books.txt inside. You change the folder's permissions twice; the file's own permissions never change (-rw-rw-r--):

$ chmod 600 ~/catalog; ls -ld ~/catalog
drw------- 2 ada ada 4096 Nov 14 14:40 /home/ada/catalog
$ chmod 300 ~/catalog; ls -ld ~/catalog
d-wx------ 2 ada ada 4096 Nov 14 14:40 /home/ada/catalog

Do ls ~/catalog and cat ~/catalog/books.txt work in each case? You own the folder, and in the first case you also have read permission. If your instinct says "both work with 600, neither works with 300", the first section of this post is for you.

In the second post we placed the files of Bookstore, the online bookstore used as the shared example across this site's series, but we ran the application as our own user. In this post we cover the Linux permission model, a separate service user for the application, separating the deploy user from it, secret files and sudo.

rwx: Different Meanings on Files and Directories

Every file has an owner (user), a group and three permission sets: owner, group and everyone else (others). Each set has three bits: r (read), w (write), x (execute). On an access, Linux looks at exactly one set: the owner set if you are the owner, otherwise the group set if you are in the group, otherwise the others set. Root skips most of these checks.

On a file the meanings are intuitive: r reads the content, w changes it, x runs it as a program. On a directory they are different, and this is where the answer to the guess is:

$ chmod 600 ~/catalog
$ ls ~/catalog
books.txt
$ cat ~/catalog/books.txt
cat: /home/ada/catalog/books.txt: Permission denied
$ chmod 300 ~/catalog
$ ls ~/catalog
ls: cannot open directory '/home/ada/catalog': Permission denied
$ cat ~/catalog/books.txt
Dune

On a directory, r means listing the names inside it. x means entering the directory and reaching a name inside it; to open a file you need x on every directory along the path. With 600 we could see the names but could not reach any file; with 300 we could not list, but we read the file whose name we knew. w on a directory means creating, deleting and renaming files in it; so to delete a file, what counts is the permission of the directory it is in, not of the file. That is why, when you get "Permission denied", you look at every directory in the path, not just the file.

chmod, chown and umask

chmod comes in two forms. In octal form each set is one digit: r=4, w=2, x=1 are added up; 640 means 6 (rw) for the owner, 4 (r) for the group and 0 for others. In symbolic form you write who (u, g, o, a) and what to do (+, -, =):

$ chmod 640 report.csv; ls -l report.csv
-rw-r----- 1 ada ada 0 Nov 14 14:40 report.csv
$ chmod u+x,g-r,o= report.csv; ls -l report.csv
-rwx------ 1 ada ada 0 Nov 14 14:40 report.csv
$ stat -c "%a %A %n" report.csv
700 -rwx------ report.csv

The octal form fits "make it exactly this", the symbolic form fits "add or remove this, leave the rest alone". stat -c "%a" prints a file's permissions in octal; it helps when you map the two forms in scripts and documentation. chown user:group file changes the owner and group, add -R for a whole folder; only root can change ownership.

The permissions of a new file are set by the umask: files start from 666, directories from 777, and the bits in the umask are removed.

$ umask
0002
$ touch report.csv; mkdir exports; ls -ld report.csv exports
drwxrwxr-x 2 ada ada 4096 Nov 14 14:40 exports
-rw-rw-r-- 1 ada ada    0 Nov 14 14:40 report.csv
$ sudo bash -c "umask; touch /tmp/root-file; ls -l /tmp/root-file"
0022
-rw-r--r-- 1 root root 0 Nov 14 14:40 /tmp/root-file
$ grep -E "^(UMASK|USERGROUPS_ENAB)" /etc/login.defs
UMASK		022
USERGROUPS_ENAB yes

The default umask on Ubuntu is 022, but because of USERGROUPS_ENAB yes, regular users who have a private group with the same name as the user keep the group write bit (0002). That is safe because they are the only member of that group; but when you work in directories that belong to a shared group, as we will see shortly, it means files are created group-writable.

A Service User for Bookstore

Running a web application as root turns every hole in the application into a hole in the whole server. Running it as your own user is not good either: the application sits right next to your SSH keys, your home folder and your sudo rights. The right way is a system user created only for the application, one that cannot log in:

$ sudo useradd --system --no-create-home --shell /usr/sbin/nologin bookstore
$ id bookstore
uid=999(bookstore) gid=988(bookstore) groups=988(bookstore)
$ getent passwd bookstore
bookstore:x:999:988::/home/bookstore:/usr/sbin/nologin
$ sudo su - bookstore
su: warning: cannot change directory to /home/bookstore: No such file or directory
This account is currently not available.
$ sudo -u bookstore whoami
bookstore

--system picks a UID below 1000; since no password is given, the account's password stays locked (! in /etc/shadow); the /usr/sbin/nologin shell prevents interactive sessions as this user. useradd wrote a home folder path into the record, but because of --no-create-home the folder does not exist; that is where the warning comes from. Even so, we can run a single command as this user with sudo -u bookstore, because sudo starts the command directly, not through a shell. The systemd service will start it the same way with User=bookstore (fifth post).

The Deploy User, a Shared Group and setgid

The user that runs the application and the user that puts a new release on the server should not be the same. If the application can write to its own code, a hole in the application can change the code itself. So we set up the four folders like this: the deploy user writes releases and the bookstore group reads them; only bookstore writes to the state and log folders; root writes the configuration and the bookstore group reads it.

$ sudo useradd --create-home --shell /bin/bash deploy
$ sudo chown -R deploy:bookstore /opt/bookstore
$ sudo chmod 2755 /opt/bookstore/releases
$ sudo chown bookstore:bookstore /var/lib/bookstore /var/log/bookstore
$ sudo chmod 750 /var/lib/bookstore /var/log/bookstore
$ sudo chown root:bookstore /etc/bookstore && sudo chmod 750 /etc/bookstore
$ ls -ld /opt/bookstore/releases /etc/bookstore /var/lib/bookstore /var/log/bookstore
drwxr-x--- 2 root      bookstore 4096 Nov 14 14:44 /etc/bookstore
drwxr-sr-x 4 deploy    bookstore 4096 Nov 14 14:41 /opt/bookstore/releases
drwxr-x--- 2 bookstore bookstore 4096 Nov 14 14:42 /var/lib/bookstore
drwxr-x--- 2 bookstore bookstore 4096 Nov 14 14:30 /var/log/bookstore

The leading 2 in 2755 is the setgid bit; in ls it shows as the s in the group set. On a directory, setgid makes everything created inside it get the directory's group instead of the creator's group. I saw the difference with two release folders:

$ sudo -u deploy mkdir /opt/bookstore/releases/2026-11-14-3
$ ls -ld /opt/bookstore/releases/2026-11-14-3
drwxrwsr-x 2 deploy bookstore 4096 Nov 14 14:44 /opt/bookstore/releases/2026-11-14-3
$ sudo chmod g-s /opt/bookstore/releases
$ sudo -u deploy mkdir /opt/bookstore/releases/2026-11-14-4
$ ls -ld /opt/bookstore/releases/2026-11-14-4
drwxrwxr-x 2 deploy deploy 4096 Nov 14 14:44 /opt/bookstore/releases/2026-11-14-4

With setgid on, the new release came with the bookstore group and the application can read it; with it off, it came with the deploy group and fell into the others set for the application. Two more details about permissions. First, because deploy's umask is 0002, new files are created as -rw-rw-r--, that is, group-writable, which means the application could write to its own Bookstore.Api.dll. Watch out for rsync here: because rsync -a copies permissions and group from the source, it dropped the setgid bit on the new release folder and the files came with the deploy group; since deploy is not in the bookstore group, it could not put it back with --chmod either. Running rsync -rlt, which copies neither permissions nor group, with a umask of 022 for deploy gave a drwxr-sr-x folder and files with the bookstore group and 644 (755 for the program itself). Second, in my first attempt I had added deploy to the bookstore group and saw that it could also read the secret file below. Group membership means reading everything the group can read; deploy owns the folder so it does not need the group, and I removed it with sudo gpasswd -d deploy bookstore.

The same logic for the secret file: owner root, group bookstore, mode 640. Created with sudo tee, the file first came as 644 because of root's umask of 022, so everyone could read it; I fixed it right away.

$ echo "ConnectionStrings__Default=Server=127.0.0.1;Database=BookstoreDb;User Id=bookstore_app;Password=<secret>" | sudo tee /etc/bookstore/secrets.env > /dev/null
$ sudo ls -l /etc/bookstore/secrets.env
-rw-r--r-- 1 root root 105 Nov 14 14:44 /etc/bookstore/secrets.env
$ sudo chown root:bookstore /etc/bookstore/secrets.env && sudo chmod 640 /etc/bookstore/secrets.env
$ sudo ls -l /etc/bookstore/secrets.env
-rw-r----- 1 root bookstore 105 Nov 14 14:44 /etc/bookstore/secrets.env
$ sudo -u bookstore cat /etc/bookstore/secrets.env
ConnectionStrings__Default=Server=127.0.0.1;Database=BookstoreDb;User Id=bookstore_app;Password=<secret>
$ sudo -u deploy cat /etc/bookstore/secrets.env
cat: /etc/bookstore/secrets.env: Permission denied
$ cat /etc/bookstore/secrets.env
cat: /etc/bookstore/secrets.env: Permission denied

Note the sudo tee pattern: sudo echo ... > /etc/... does not work, because the redirection is done by your shell with your rights, not by sudo. When we ran the application as the service user, the result was what we expected:

$ ps -o user,pid,cmd -C dotnet
USER         PID CMD
booksto+    2269 dotnet Bookstore.Api.dll --urls http://127.0.0.1:5000
$ sudo -u bookstore touch /opt/bookstore/current/hacked.txt
touch: cannot touch '/opt/bookstore/current/hacked.txt': Permission denied
$ sudo -u bookstore touch /var/lib/bookstore/state.txt && sudo ls -l /var/lib/bookstore
total 0
-rw-rw-r-- 1 bookstore bookstore 0 Nov 14 14:42 state.txt
$ sudo -u bookstore cat /home/ada/.ssh/authorized_keys
cat: /home/ada/.ssh/authorized_keys: Permission denied

The application cannot write to its own code, can write to its state folder and cannot enter your home folder (drwxr-x---). ps shortens user names longer than eight characters to booksto+; use ps -o user:20 for the full name.

The Sticky Bit and ACLs

/tmp is a directory everyone can write to; so can everyone delete everyone else's files too? No, because it has the sticky bit:

$ ls -ld /tmp
drwxrwxrwt 12 root root 4096 Nov 14 14:41 /tmp
$ sudo -u deploy rm /tmp/ada-notes.txt
rm: cannot remove '/tmp/ada-notes.txt': Operation not permitted

The trailing t says that everyone who can write to the directory may create files in it, but may delete and rename only their own. In the same test, deploy could not even write to this file with mode 666; Ubuntu's fs.protected_regular = 2 kernel setting blocks opening files owned by others that way in sticky directories.

Sometimes the owner, group and others triple is not enough: "let one more person read this file, without joining the group". That is what access control lists, ACLs, are for. setfacl and getfacl were not on the cloud image; I installed them with sudo apt install acl:

$ sudo setfacl -m u:ada:r /etc/bookstore/secrets.env
$ sudo getfacl /etc/bookstore/secrets.env
getfacl: Removing leading '/' from absolute path names
# file: etc/bookstore/secrets.env
# owner: root
# group: bookstore
user::rw-
user:ada:r--
group::r--
mask::r--
other::---

$ sudo ls -l /etc/bookstore/secrets.env
-rw-r-----+ 1 root bookstore 105 Nov 14 14:40 /etc/bookstore/secrets.env
$ sudo setfacl -b /etc/bookstore/secrets.env

The + in the ls output is the only sign that the file has an ACL; someone who reads permissions only with ls -l will not see the extra entry. So use ACLs for exceptions and groups for structure. setfacl -b removes all extra entries.

sudo, sudoers and visudo

sudo runs a command as another user, root by default, and /etc/sudoers together with /etc/sudoers.d/ decides who may run what. sudo -l shows what you are allowed:

$ sudo -l
Matching Defaults entries for ada on bookstore-dev:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User ada may run the following commands on bookstore-dev:
    (ALL : ALL) ALL
    (ALL) NOPASSWD: ALL
$ sudo cat /etc/sudoers.d/90-cloud-init-users
# Created by cloud-init v. 26.1-0ubuntu1~24.04.1 on Sat, 14 Nov 2026 14:04:52 +0000

# User rules for ada
ada ALL=(ALL) NOPASSWD:ALL

The first line comes from the sudo group (%sudo ALL=(ALL:ALL) ALL in /etc/sudoers), the second from the file cloud-init added. The default user on cloud images comes with passwordless sudo; this VM is no exception. secure_path is the PATH used to look up commands run with sudo; if /usr/local/bin were not in it, sudo mytool would say "command not found".

We want the deploy user to be able to restart only the Bookstore service, not to become root. Never change a sudoers file directly with an editor: one typo makes sudo unusable, and on Ubuntu, where root has no password, you need the console to recover. visudo checks the file before saving; for drop-in files you use sudo visudo -f /etc/sudoers.d/bookstore-deploy. I first wrote the file to a temporary place and checked it with -c:

$ cat /tmp/bookstore-deploy
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart bookstore-api, /usr/bin/systemctl status bookstore-api
$ sudo visudo -cf /tmp/bookstore-deploy
/tmp/bookstore-deploy: parsed OK
$ sudo visudo -cf /tmp/bad
/tmp/bad:1:68: syntax error
deploy ALL=(root) NOPASSWD /usr/bin/systemctl restart bookstore-api
                                                                   ^
$ sudo install -m 440 -o root -g root /tmp/bookstore-deploy /etc/sudoers.d/bookstore-deploy
$ sudo -u deploy sudo -l
...
User deploy may run the following commands on bookstore-dev:
    (root) NOPASSWD: /usr/bin/systemctl restart bookstore-api, /usr/bin/systemctl status bookstore-api
$ sudo -u deploy sudo -n /usr/bin/systemctl status bookstore-api
Unit bookstore-api.service could not be found.
$ sudo -u deploy sudo -n cat /etc/bookstore/secrets.env
sudo: a password is required

The broken file is missing only the colon after NOPASSWD, and visudo caught it. The allowed command ran (we have not created the service yet; we do that in the fifth post); for the command that is not allowed, sudo asked for a password, and since deploy has none, that is a refusal. Writing the commands with full paths and arguments matters: if only /usr/bin/systemctl were written, deploy could stop every service, or even open an editor as root with systemctl edit. The drop-in file name must not contain a dot; @includedir skips files whose names contain a dot or end with ~. So that the rule is not written by hand on the server and forgotten, we keep it in the repository as deploy/linux/sudoers.d/bookstore-deploy.

The difference between su and sudo is whose password is asked. su - asks for the target user's password, that is, root's; on Ubuntu the root account comes locked, so this does not work:

$ su - -c whoami < /dev/null
Password: su: Authentication failure
$ sudo passwd -S root
root L 2026-10-17 0 99999 7 -1
$ sudo -i whoami
root

L means locked. If you need a root shell, use sudo -i (root's login shell) or sudo -s; but for a single command, sudo command is both less risky and a clearer trace in the log. On Ubuntu 26.04 the sudo command is now backed by sudo-rs, written in Rust, while the original sudo stays in the repository; sudoers files work the same way in most cases. We collect the differences in the last post of the series.

Production Notes

One service user per application. If Bookstore.Api runs on the same server as a background worker or another application, do not run them all as the same user; a hole in one should not expose the secrets of another. systemd settings such as ProtectSystem=strict and ReadWritePaths=/var/lib/bookstore back up the permissions in this post; they do not replace each other, they add up as layers. The hardening side of the unit file is in the systemd post of the Web Servers and Hosting series.

Read the sudo records. Every sudo call is written to /var/log/auth.log and to the journal: who ran which command in which folder. The first answer to "who did what in production" is there; we cover reading logs in the sixth post. That is another reason why logging in with personal accounts and using sudo is better than a shared root session.

Common Pitfalls

Fixing "Permission denied" with chmod 777. It hides the problem and widens the hole: every user and every process on the server can now change the file. First check who is trying to access it (ps -o user), then the permissions of every directory in the path; namei -l /etc/bookstore/secrets.env lists the path step by step with permissions, and run as ada it showed directly which directory it got stuck on (drwxr-x--- root bookstore bookstore). Then give the right user or group the least permission needed.

Files created with sudo staying owned by root. If you copy something in your own home folder with sudo out of habit, you cannot write to it in the next step:

$ sudo cp -r ~/bookstore-api ~/staging
$ ls -ld ~/staging
drwxr-xr-x 2 root root 4096 Nov 14 14:42 /home/ada/staging
$ cp ~/bookstore-api/Bookstore.Api.dll ~/staging/
cp: cannot create regular file '/home/ada/staging/Bookstore.Api.dll': Permission denied
$ sudo chown -R ada:ada ~/staging

The same happens with sudo dotnet publish, sudo npm install and sudo git pull: files in bin/, obj/, node_modules or .git become root's, and the next normal run fails. Use sudo only for the step that really needs root.

Leaving the secret file at 644. A file created with sudo tee or an editor comes as 644 with umask 022, and everyone on the server reads the connection string. chown root:bookstore and chmod 640 should come right after the command that creates the secret file; or create an empty file with the right permissions from the start with sudo install -m 640 -o root -g bookstore /dev/null /etc/bookstore/secrets.env and fill it afterwards.

Expecting group membership to apply immediately. When you add a user to a group with usermod -aG or gpasswd -a, that user's open sessions keep the old group list; the change applies in a new session (log out of SSH and back in) or when the service restarts. Also, running usermod -G without -a removes the user from every group not in the list, sudo included.

A Small Experiment

★☆☆ On your own VM, create a folder and set its permissions to 700, 600, 300 and 100 in turn. In each case, write down in a table what you can do to a file inside it with ls, cat, touch and rm.

★★☆ Create the bookstore service user and the deploy user, set up the four folders as in this post and run Bookstore.Api with sudo -u bookstore. Verify yourself that the application can write to /var/lib/bookstore, cannot write to the release folder, and that deploy cannot read the secret file.

★★★ Create /etc/sudoers.d/bookstore-deploy with visudo -f and give deploy permission only for systemctl restart bookstore-api. Then loosen the rule on purpose (write /usr/bin/systemctl without arguments) and see what happens when deploy runs sudo systemctl edit bookstore-api. This scenario shows why rules should be written with their arguments.

The application now runs as the right user, but we still start and stop it by hand. In the next post we look at processes: reading ps and top output, shutting down a .NET application cleanly with signals, why a process dies when SSH drops, and zombies.