Users, groups and sudo on Linux

Creating and removing users, granting sudo safely, reading permission bits, using chmod and chown, and why services should not run as root.

Linux10 min read

On this page
  1. Creating a user
  2. Accounts for services, not for people
  3. Removing a user
  4. Groups
  5. What sudo actually does
  6. Granting it
  7. Granting less than everything
  8. Where sudo use is recorded
  9. Reading permissions
  10. Directories work differently
  11. chmod
  12. Recursive changes
  13. chown
  14. Why running services as root is the mistake
  15. Related

Linux decides what every process may touch from the account it runs as. Getting accounts and permissions right is therefore not housekeeping, it is the difference between a bug in your game server costing you one directory and costing you the whole machine.

This page covers creating and removing users, group membership, what sudo actually does, the permission bits behind "permission denied", and the reasoning for running services under their own account. Commands are shown for Debian and Ubuntu and for RHEL, AlmaLinux and Rocky Linux where the two differ.

Creating a user

On Debian and Ubuntu, adduser is an interactive script that creates the account, its home directory, its group, and prompts for a password:

Shell
adduser deploy

On RHEL, AlmaLinux and Rocky Linux, adduser is the low-level useradd under another name and does none of the prompting, so it takes two commands:

Shell
useradd -m -s /bin/bash deploy
passwd deploy

-m creates the home directory and -s sets the login shell. Confirm what you got:

Shell
id deploy

That prints the numeric user ID, the primary group, and every supplementary group. The numeric ID is what the filesystem actually stores, which matters later when you delete an account.

Accounts for services, not for people

A user that exists to run a program does not need a password, a shell, or a home directory full of dotfiles. Create it so that nobody can sign in as it:

Shell
useradd --system --shell /usr/sbin/nologin --home-dir /opt/fivem fivem

On RHEL, AlmaLinux and Rocky the equivalent shell path is /sbin/nologin. --system allocates an ID from the range reserved for services, which keeps it out of the way of real people. The account still owns files and still runs processes, but it cannot be logged into, so a stolen password is not a way in because there is no password.

Removing a user

Deleting an account is not one action, it is a decision about the account and a separate decision about its files.

First make sure nothing is still running as that user, because processes survive the account they belong to and are then owned by a number with no name:

Shell
pgrep -u deploy -l

Then remove the account and its home directory:

Shell
userdel -r deploy

userdel -r works on both families. Debian and Ubuntu also ship deluser --remove-home deploy, which does the same thing.

To keep the person out without destroying anything, lock the account instead of deleting it. This is usually the right move when the files belong to work still in progress:

Shell
usermod -L deploy
usermod -s /usr/sbin/nologin deploy

Locking the password does not revoke an SSH key. If the account has a key in /home/deploy/.ssh/authorized_keys, that key still works after a lock, so remove the file as well.

After deleting a user, files they owned elsewhere on the disk are left behind with a bare numeric owner. Find them before that number gets reused by the next account you create:

Shell
find / -xdev -nouser -o -nogroup

Groups

A group is a named set of users, and it is how permission is granted to more than one account without editing every file. Membership comes in two kinds: one primary group, recorded against the account, and any number of supplementary groups.

Shell
groups deploy
getent group sudo

Adding someone to a group:

Shell
usermod -aG docker deploy
deploy, before the changedeploy (primary)sudousermod -G docker deploySets the list to exactly dockerdeploy (primary)dockersudosudo removed, with no promptusermod -aG docker deployAppends docker to the listdeploy (primary)sudodockerEvery group keptdeploy, before the changedeploy (primary)sudousermod -G docker deploySets the list to exactly dockerdeploy (primary)dockersudosudo removed, with no promptOr, from the same starting point, with -ausermod -aG docker deployAppends docker to the listdeploy (primary)sudodockerEvery group kept
The same account after each command. Without -a the supplementary list becomes exactly docker, so the account loses sudo; with -a, docker is added and nothing else changes. The primary group is not part of that list, so both keep it. Illustration.

Removing someone from a group:

Shell
gpasswd -d deploy docker

Group membership is read when a session starts. Someone already signed in keeps the groups they had until they log out and back in, which is why "I added them to the group and it still says permission denied" is almost always a stale session rather than a broken group.

What sudo actually does

sudo runs a single command as another user, root unless you say otherwise, after checking the rules in /etc/sudoers and the files in /etc/sudoers.d/. It authenticates you with your own password, not root's, it records what was run, and it applies per-command rules. That is the whole difference from knowing the root password: sudo is auditable and can be narrowed, a shared root password is neither.

See what you are allowed to run:

Shell
sudo -l

Granting it

Both families ship a rule that grants full sudo to a group, so adding someone to that group is the normal way to grant administrative access. On Debian and Ubuntu the group is sudo, on RHEL, AlmaLinux and Rocky it is wheel:

Shell
usermod -aG sudo deploy
Shell
usermod -aG wheel deploy

Granting less than everything

Full sudo means the account can become root, so it is worth avoiding for automation that only needs one command. Write a narrow rule in its own file under /etc/sudoers.d/, and always use visudo to edit it:

Shell
visudo -f /etc/sudoers.d/deploy-restart

A rule that lets one account restart one service and nothing else:

deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart fivem.service

Where sudo use is recorded

Every invocation is logged. On Debian and Ubuntu that lands in /var/log/auth.log, on RHEL, AlmaLinux and Rocky in /var/log/secure, and on both in the journal:

Shell
journalctl -t sudo --since "1 hour ago"

Reading permissions

Every file has an owner, a group, and three sets of permission bits. ls -l shows them:

Shell
ls -l /opt/fivem

The first column is ten characters. The first is the type, - for a regular file and d for a directory. The remaining nine are three groups of three: permissions for the owner, then for the group, then for everyone else. In each group, r is read, w is write, and x is execute. A dash means that permission is absent. The two names after the link count are the owning user and the owning group.

Only one set applies to you. If you are the owner, the owner bits are used and the group bits are never consulted, even if they are more permissive.

One line of ls -l /opt/fivemdrwxr-x--- 2 fivem fivem 4096 Aug 18 20:00 logsowning userowning groupdTypeA directory.A dash here is aregular file.rwxOwnerRead, write, execute4 + 2 + 1 = 7Used if you are fivemr-xGroupRead, execute4 + 0 + 1 = 5Used if you are ingroup fivem and notthe owner---OthersNothing0 + 0 + 0 = 0Used for every otheraccountMode 750Owner 7, group 5, others 0: the table's directory shared with a service group.One line of ls -l /opt/fivemdrwxr-x--- 2 fivem fivem 4096 Aug 18 20:00 logsowning userowning groupdTypeA directory.A dash here is aregular file.rwxOwnerRead, write, execute4 + 2 + 1 = 7Used if you arefivemr-xGroupRead, execute4 + 0 + 1 = 5Used if you are ingroup fivem and notthe owner---OthersNothing0 + 0 + 0 = 0Used for every otheraccountMode 750Owner 7, group 5, others 0: the table'sdirectory shared with a service group.
One line of ls -l, taken apart. The first character is the type, then three sets of three: the owner's, the group's and everyone else's, each read only for the accounts it applies to. Each set adds up to one digit: 7, 5 and 0 make mode 750. Illustration.

Numerically, r is 4, w is 2, and x is 1, added together per set. That is where the familiar three-digit numbers come from:

ModeOwnerGroupOthersTypical use
600read, writenonenonePrivate keys, config files with credentials
644read, writereadreadOrdinary files, static web content
700read, write, executenonenoneThe .ssh directory, a private script
750read, write, executeread, executenoneA directory shared with a service group
755read, write, executeread, executeread, executeDirectories, executables

Directories work differently

On a directory the same three bits mean something else, and this is where most confusion starts:

  • r lets you list the names inside it.
  • w lets you create and delete entries inside it.
  • x lets you traverse into it and reach things by name.

The consequence people trip over: whether you can delete a file is decided by the permissions on its directory, not on the file. A file you cannot write can still be deleted by anyone who can write to the directory holding it. That is also why /tmp carries the sticky bit, shown as t in the last position of ls -ld /tmp, which restricts deletion inside a world-writable directory to each file's own owner.

chmod

chmod changes the bits. It takes either a number, which sets all three sets at once, or a symbolic form, which adjusts one part and leaves the rest alone:

Shell
chmod 600 /home/deploy/.ssh/id_ed25519
chmod +x /opt/scripts/backup.sh
chmod g+w /opt/fivem/logs
chmod o-rwx /opt/fivem/server.cfg

The cases that come up in practice:

  • A config file with a password or an API token in it. chmod 600, owned by the account that reads it. Anything world-readable is readable by every process on the machine.
  • A script that will not run. If it says "permission denied" when you run it directly, it is missing x. chmod +x fixes it.
  • A service that cannot write to its own directory. The directory needs w and x for the account the service runs as, which usually means fixing ownership rather than loosening the bits. See chown below.
  • SSH refusing a key. SSH will not use a private key that others can read and prints a loud warning about an unprotected private key file. The server side is quieter: sshd ignores authorized_keys if the home directory or .ssh is writable by group or others, and gives you a plain Permission denied with no explanation. 700 on .ssh and 600 on authorized_keys is the fix.

Recursive changes

chmod -R 755 on a tree marks every ordinary file executable, which is untidy and defeats the point of the bit. Set directories and files separately:

Shell
find /opt/fivem -type d -exec chmod 755 {} +
find /opt/fivem -type f -exec chmod 644 {} +

chown

chown changes who owns a file, and it is the tool for the single most common permission problem on a new server: you unpacked something as root, so every file is owned by root, and the service that runs as its own account cannot write to any of it.

Shell
chown -R fivem:fivem /opt/fivem

The form is user, colon, group. Either half can be omitted: chown fivem file changes only the user, and chown :fivem file changes only the group, which chgrp fivem file also does.

Only root can give a file away to another user, so these commands run under sudo.

A useful pattern for a directory that a service writes to and a person needs to read: set the owner to the service account, the group to a group you are in, and the directory to 750. You get to read the logs without the service files being readable by every account on the machine.

Why running services as root is the mistake

If a program has a bug that lets an attacker run commands, those commands run as whatever the program runs as. That single sentence is the whole argument.

As root, an attacker who found a flaw in your game server, in a resource or plugin you installed, or in a web panel you exposed, can install a persistent backdoor, read every file including your keys and database credentials, modify the logs that would have shown you what happened, and use the machine to attack others. As an unprivileged service account, the same flaw gets them the files that account can touch, and no ability to change the system underneath it. It is the difference between an incident and a rebuild.

The game server runs as rootA flaw in it gives the attacker rootThis service's own filesReachedOther services' dataReachedKeys and credentials elsewhereReachedThe logsReachedSystem binaries, startup scriptsReachedA rebuildNothing on the disk can be trusted afterwardsIt runs as fivem, its own accountThe same flaw gives the attacker fivemThis service's own filesReachedOther services' dataOut of reachKeys and credentials elsewhereOut of reachThe logsOut of reachSystem binaries, startup scriptsOut of reachAn incidentLimited to the files fivem can touchThe game server runs as rootA flaw in it gives the attacker rootThis service's own filesReachedOther services' dataReachedKeys and credentials elsewhereReachedThe logsReachedSystem binaries, startup scriptsReachedA rebuildNothing on the disk can be trusted afterwardsIt runs as fivem, its own accountThe same flaw gives the attacker fivemThis service's own filesReachedOther services' dataOut of reachKeys and credentials elsewhereOut of reachThe logsOut of reachSystem binaries, startup scriptsOut of reachAn incidentLimited to the files fivem can touch
The same flaw in the same game server, under two accounts. As root it reaches everything on the machine, including the logs that would show what happened and the system files a backdoor would live in. As fivem it reaches only what fivem can touch. Illustration.

The practical version:

  • Give each service its own account, as shown above, rather than sharing one. Two services under one account can reach each other's data.
  • Run it under that account with systemd. User= and Group= in a unit file do this properly, and drop privileges before the program starts. See Managing services with systemd.
  • Own the data with the same account. chown -R the service's directory to it, and give it write access only where it genuinely writes.
  • Do not use root to work around a port. Ports below 1024 need privilege to bind, which is the usual excuse for running as root. systemd grants exactly that one capability with AmbientCapabilities=CAP_NET_BIND_SERVICE, and a reverse proxy in front is the other answer.

If the worst has already happened and you think a server has been compromised, changing passwords is not enough, because you cannot trust anything on the disk after the fact. Rebuild it from a known-good state, see Reinstalling your server, and open a ticket at Opening a ticket if you need help working out what happened.

Need a server, or a hand with one?

Pick a plan to deploy a server, or open a ticket in the panel. Support runs 24/7/365.