👥 Users, Groups and sudo

Accounts, groups, shared folders, sudoers rules that are safe, audits and offboarding

Video: Users, Groups and sudo (sysadmin video)
Prefer to watch? This video runs the guide's main commands on a real Ubuntu 24.04 machine, with on-screen captions and no voiceover. Click to play, or watch it on YouTube. It is part of the Services, Storage and Security playlist.
Users, Groups and sudo
id; groups; sudo -l

Every file, process and login on a Linux system belongs to a user and a group, and most security incidents are, at heart, a permissions story: someone could do more than they should. This guide covers where accounts live, how to create and retire them safely, how groups really work, how sudo is configured (and the mistakes that turn it into a back door), and how to audit all of it.
Examples marked with "$ " were run for real on Ubuntu 24.04 (account, sudoers and umask checks work on sample files, so nothing here changed this machine's accounts). Commands that create or delete real accounts are shown without output: run them on a test machine first.

Where accounts live

/etc/passwd      one line per account, readable by everyone:  name:x:UID:GID:comment:home:shell
/etc/shadow      the password hashes and ageing data, readable by root only:  name:hash:last-change:min:max:warn:inactive:expire
/etc/group       groups and their members:  name:x:GID:member1,member2
/etc/gshadow     group passwords and administrators (rarely used)
/etc/login.defs  defaults for new accounts: UID ranges, password ageing, umask, hash algorithm
/etc/skel/       files copied into every new home directory (.bashrc, .profile)
Never edit these by hand while others are using the system. Use the tools (useradd, usermod, passwd, chage), or vipw and vigr, which lock the files while you edit.
Read the fields with a tiny awk (here on a typed-in sample, so the output is the same everywhere)
$ awk -F: '{printf "%-8s uid=%-5s gid=%-5s home=%-18s shell=%s\n", $1, $3, $4, $6, $7}' passwd.sample
root     uid=0     gid=0     home=/root              shell=/bin/bash
alice    uid=1001  gid=1001  home=/home/alice        shell=/bin/bash
svc-app  uid=998   gid=998   home=/var/lib/svc-app   shell=/usr/sbin/nologin
bob      uid=1002  gid=1002  home=/home/bob          shell=/bin/bash
UID ranges: 0 is root (always, and nothing else should have UID 0), 1-999 are system and service accounts, 1000 and up are people. The exact boundaries are in /etc/login.defs:
$ grep -E '^(UID_MIN|UID_MAX|UMASK|ENCRYPT_METHOD)' /etc/login.defs
UMASK		022
UID_MIN			 1000
UID_MAX			60000
ENCRYPT_METHOD SHA512
(On Debian and Ubuntu the hash actually used for new passwords is set in /etc/pam.d/common-password (yescrypt on current releases), which takes precedence over ENCRYPT_METHOD here.)
Look accounts up the right way: getent asks every source the system uses (local files, LDAP, Active Directory through sssd), while grep /etc/passwd only sees local accounts.
$ getent passwd root
root:x:0:0:root:/root:/bin/bash
$ getent passwd alice          # also finds domain users
$ getent group sudo            # who may use sudo on Debian and Ubuntu (the group is called wheel on RHEL, Fedora and friends)
$ id alice                     # uid, primary group, every group
$ groups alice                 # just the group names
$ last -n 10; lastlog | head   # recent logins; when each account last logged in
The password field in /etc/shadow is a hash with a prefix that names the algorithm: $6$ is SHA-512 (the usual default), $y$ is yescrypt (newer Debian and Ubuntu), $5$ SHA-256, $1$ MD5 (broken, replace). A "!" or "*" in front of or instead of the hash means "no password login is possible" (locked or never set).
$ openssl passwd -6 -salt abcdefgh 'S3cret!' | cut -c1-30
$6$abcdefgh$N0v5eprOrPGdoyXDOI
Never type real passwords on a command line you keep in shell history; use passwd to set them interactively, or pipe them from a file with chpasswd.

Create, change and remove users

Debian and Ubuntu also have adduser, a friendlier wrapper that asks questions; useradd exists everywhere and does only what you tell it.
$ sudo adduser alice                                         # interactive: home directory, password, details
$ sudo useradd -m -s /bin/bash -c "Alice Doe" -G sudo alice  # the portable way: -m create home, -s shell, -c comment, -G extra groups
$ sudo passwd alice                                          # set (or change) the password interactively
$ sudo passwd -e alice                                       # expire it NOW: she must choose a new one at first login
$ sudo usermod -aG docker alice                              # ADD to a group. The -a matters (see the warning below)
$ sudo usermod -s /usr/sbin/nologin alice                    # no interactive login any more
$ sudo usermod -L alice; sudo usermod -U alice               # lock / unlock the PASSWORD (ssh keys still work when locked: see offboarding)
$ sudo usermod -e 2026-12-31 alice                           # the account expires on a date;  -e "" removes the expiry
$ sudo chage -l alice                                        # password ageing and account expiry at a glance
$ sudo chage -M 90 -m 1 -W 14 alice                          # password must change every 90 days, minimum 1 day, warn 14 days before
$ sudo userdel -r alice                                      # delete the account AND her home folder and mail (back up first); without -r the files stay
$ sudo deluser --remove-home alice                           # the Debian equivalent
WARNING: usermod -G docker alice (without -a) REPLACES all of alice's supplementary groups with just docker and silently removes her from sudo. Always -aG to add.
Group membership is read at LOGIN: a new group does not apply to sessions that are already open. Log out and in, or start a shell with the group: newgrp docker  (or sg docker -c 'command').
Service accounts: programs should run as their own unprivileged user
$ sudo useradd --system --no-create-home --home-dir /var/lib/myapp --shell /usr/sbin/nologin myapp
$ sudo install -d -o myapp -g myapp -m 750 /var/lib/myapp            # a private data folder
$ sudo -u myapp -s /bin/bash                                         # a shell AS that account for testing (it has nologin, so -s names a shell explicitly)
$ sudo runuser -u myapp -- /usr/local/bin/myapp --check              # run one command as it
For systemd services you can skip creating accounts at all: DynamicUser=yes in the unit gives the service a throw-away user (see the systemd guide).

Groups: shared access done right

$ sudo groupadd webadmins
$ sudo usermod -aG webadmins alice; sudo gpasswd -a bob webadmins     # two ways to add a member;  sudo gpasswd -d bob webadmins removes
$ getent group webadmins
$ id -Gn                       # the groups of the CURRENT session
A shared project folder: members of one group can read and write each other's files, and new files automatically belong to the group. Three ingredients: the group owns the folder, the group has write permission, and the setgid bit makes new files inherit the group instead of the creator's own group.
$ sudo install -d -o root -g webadmins -m 2775 /srv/project     # 2 = setgid, 775 = rwx for owner and group
$ ls -ld /srv/project
drwxrwsr-x 2 root webadmins 4096 Mar  8 10:00 /srv/project
The "s" in the group's execute position is the setgid bit. Without it, files created in /srv/project belong to each creator's primary group and the team cannot edit them. The default permissions of new files come from the umask, so members also need umask 002 (group-writable) in their session, otherwise their files are created group-read-only:
$ ( umask 022; touch f-022; mkdir d-022; stat -c '%a %n' f-022 d-022 )
644 f-022
755 d-022
$ ( umask 002; touch f-002; mkdir d-002; stat -c '%a %n' f-002 d-002 )
664 f-002
775 d-002
$ ( umask 077; touch f-077; mkdir d-077; stat -c '%a %n' f-077 d-077 )
600 f-077
700 d-077
Files start from 666 and folders from 777, and the umask removes bits: umask 022 gives 644/755, umask 002 gives 664/775 (team work), umask 077 gives 600/700 (private). Set it for a user in ~/.profile, or for a whole service with UMask= in its systemd unit. ACLs (setfacl -m g:webadmins:rwx dir, setfacl -d for defaults) are the finer tool when one group is not enough.

sudo: controlled root

sudo runs one command as another user (root by default) after checking the policy in /etc/sudoers and /etc/sudoers.d/*. It logs every use. The point is to give people exactly the rights they need, no more.
$ sudo -l                          # what may I run? (the first thing to try when "sudo" says no)
$ sudo -u postgres psql            # run one command as another user
$ sudo -i                          # a root login shell (reads root's profile);  sudo -s keeps your own environment
$ sudoedit /etc/hosts              # edit a protected file with YOUR editor, as yourself, then copy it back (safer than sudo vim)
$ sudo -k                          # forget the cached password timestamp now
$ sudo !!                          # repeat the previous command with sudo
The sudoers syntax, one rule per line:   WHO  WHERE=(RUN AS)  OPTIONS:  WHAT
$ cat <<'EOF'
> # who      where   (as whom)        what
> alice      ALL=(ALL:ALL)            ALL
> %webadmins ALL=(root)               /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx
> deploy     ALL=(root) NOPASSWD:     /usr/local/bin/deploy.sh
> EOF
# who      where   (as whom)        what
alice      ALL=(ALL:ALL)            ALL
%webadmins ALL=(root)               /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx
deploy     ALL=(root) NOPASSWD:     /usr/local/bin/deploy.sh
%name means a group. (ALL:ALL) means any target user and any target group. NOPASSWD: skips the password prompt (use it only for specific, safe commands, never for ALL). Specify full paths. Edit the policy ONLY with visudo, which locks the file and checks the syntax before saving: a syntax error in sudoers can lock every admin out.
$ sudo visudo                                  # edits /etc/sudoers safely
$ sudo visudo -f /etc/sudoers.d/webadmins      # a separate file for one topic: the preferred way, easy to add and remove
Test a file BEFORE it goes live (this works on any file, without root). A good one, with aliases and a restricted rule:
$ visudo -c -f good.sudoers
good.sudoers: parsed OK
And one with a typo (the colon after NOPASSWD is missing: the checker points at the exact spot):
$ visudo -c -f bad.sudoers
bad.sudoers:1:26: syntax error
alice ALL=(ALL) NOPASSWD ALL
                         ^~~
# (exit status 1)
Making the checks part of your routine: after creating a file in /etc/sudoers.d run sudo visudo -c, and keep a root shell open in another window while you change sudoers, until you have tested it.
Rules that bite
  1. FILE NAMES in /etc/sudoers.d are IGNORED if they contain a dot or end in ~ (so ops.conf and ops~ silently do nothing). Name them ops or 10-ops. The files must be owned by root with mode 0440.
  2. The LAST matching rule wins: a later broad line can override an earlier restriction. Put specific rules AFTER general ones.
  3. NOPASSWD with a wildcard or an editor is a root shell: sudo vim, sudo less, sudo find, sudo awk, sudo nano and many others can run commands (shell escapes). Do not allow editors or interpreters; use sudoedit instead.
  4. Wildcards in command arguments are dangerous: "/usr/bin/systemctl * nginx" also matches "systemctl edit nginx", which opens an editor as root and therefore gives a root shell. Allow exact command lines.
  5. A script that sudo is allowed to run must itself be owned by root and not writable by the user, and so must every folder above it, otherwise the user edits the script and becomes root.
  6. Do not give sudo ALL to a service account. If a program needs one privileged action, allow that single command, or use a capability or a systemd unit instead.
Useful Defaults (global options)
  Defaults use_pty                     run commands in a pseudo terminal (harder to hijack the terminal)
  Defaults logfile="/var/log/sudo.log" an extra log file (also logged to the journal or auth.log anyway)
  Defaults timestamp_timeout=5         minutes before the password is asked for again (0 = every time)
  Defaults passwd_tries=3
  Defaults secure_path="..."           the PATH sudo uses: leave it alone
  Defaults:alice !authenticate        a per-user default (use sparingly)
Who used sudo, and what happened?
$ sudo journalctl _COMM=sudo --since "1 day ago" --no-pager | tail
$ sudo grep 'sudo:' /var/log/auth.log | tail            # Debian and Ubuntu;  /var/log/secure on RHEL
$ sudo grep -c 'incorrect password attempts' /var/log/auth.log
The log shows the user, the working directory, the target user and the exact command (USER=root ; COMMAND=/usr/bin/...), which is the audit trail that su and shared root passwords never gave you.

su, root and root logins

$ su - alice                   # become alice with her environment (asks alice's password, or root's if you are root)
$ sudo passwd -l root          # lock the root password (Ubuntu does this by default: use sudo instead);  sudo passwd -u root  to unlock
$ grep -E '^PermitRootLogin' /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf     # root over SSH should be "no" or "prohibit-password"
$ sudo sshd -T | grep -i permitrootlogin
Prefer sudo to a root password: individual accounts mean individual audit trails, and you can remove one person without changing a shared secret.

Password and login policy

$ sudo passwd -S alice                      # status: L locked, P has a password, NP none; date of last change
$ sudo chage -l alice
$ grep -E 'minlen|dcredit|ucredit|retry' /etc/security/pwquality.conf        # complexity rules (package libpam-pwquality)
$ sudo faillock --user alice                 # failed-login lockouts (RHEL family; Ubuntu uses pam_faillock too on current releases)
$ sudo faillock --user alice --reset         # unlock after too many failures
$ sudo lastb | head                          # recent FAILED logins
Passwords are the weak link. For servers, prefer SSH keys with a passphrase, disable password logins (PasswordAuthentication no) once keys work, add MFA for admin access where you can, and let a central directory (LDAP or Active Directory with sssd: see the Active Directory guides) own the accounts instead of local files.

Audit: who can do what?

Run these regularly (and after any incident). Shown here on typed-in sample files so the output is the same everywhere:
$ awk -F: '$3 == 0 {print "UID 0 account: " $1}' passwd.audit
UID 0 account: root
UID 0 account: toor
$ awk -F: '$7 !~ /(nologin|false)$/ {print "can log in: " $1 " (" $7 ")"}' passwd.audit
can log in: root (/bin/bash)
can log in: toor (/bin/bash)
can log in: alice (/bin/bash)
$ awk -F: '$2 == "" {print "EMPTY PASSWORD: " $1}' shadow.audit
EMPTY PASSWORD: alice
$ awk -F: '$2 ~ /^!/ {print "locked: " $1}' shadow.audit
locked: bob
The first one finds a second root: "toor" has UID 0 (a classic backdoor). On a real machine use sudo awk ... /etc/shadow for the shadow checks. More real-machine audits:
$ getent group sudo wheel admin | cut -d: -f1,4        # who has administrative rights
$ sudo grep -rhE '^[^#]' /etc/sudoers /etc/sudoers.d/ | grep -v '^Defaults' | sort -u     # every active sudo rule in one list
$ sudo find / -xdev -perm -4000 -type f 2>/dev/null | sort                # SUID programs (run as their owner): compare against a known-good list
$ sudo find / -xdev \( -nouser -o -nogroup \) -ls 2>/dev/null | head     # files owned by a deleted account
$ awk -F: '$2 !~ /^[!*]/ {print $1}' /etc/shadow 2>/dev/null; sudo awk -F: '$2 !~ /^[!*]/ {print $1}' /etc/shadow   # accounts that CAN log in with a password
$ sudo ls -l /home/*/.ssh/authorized_keys; sudo wc -l /home/*/.ssh/authorized_keys      # who can log in by key, and how many keys each

Offboarding: when someone leaves

Do it in this order, the same day:
  1. Lock the password AND remove the SSH keys (locking a password does NOT stop key logins):
$ sudo usermod -L -e 1 alice                                      # lock and expire now
$ sudo mv /home/alice/.ssh/authorized_keys /root/alice-authorized_keys.removed       # keep a copy for the record
$ sudo chsh -s /usr/sbin/nologin alice
  2. End her sessions and processes:
$ sudo loginctl terminate-user alice; sudo pkill -u alice
  3. Remove her from groups and sudoers (grep -rn alice /etc/sudoers /etc/sudoers.d) and from shared access (VPN, Git hosting, cloud consoles, shared passwords: rotate them).
  4. Remove her scheduled jobs: sudo crontab -l -u alice; sudo crontab -r -u alice; systemctl --user list-timers (lingering?)
  5. Find what she owns and decide: sudo find / -xdev -user alice -ls 2>/dev/null | head ; hand over with sudo chown -R newowner /path
  6. Keep the home folder backed up for a retention period, then delete: sudo tar czf /backup/alice-home.tgz /home/alice && sudo userdel -r alice
Checklists beat memory: keep this list in your onboarding/offboarding procedure.

Distro differences at a glance

Task                          Debian, Ubuntu                    RHEL, Fedora, Rocky, Alma
admin group                   sudo                              wheel
create a user (friendly)      adduser alice                     useradd alice (adduser is just a link to useradd)
delete a user                 deluser --remove-home alice       userdel -r alice
default password hash         yescrypt ($y$) on new releases    SHA-512 ($6$)
sudo log                      /var/log/auth.log or the journal  /var/log/secure or the journal
password quality              libpam-pwquality                  pam_pwquality (installed by default)
failed login lockout          pam_faillock (current)            pam_faillock (faillock command)
SELinux / AppArmor            AppArmor                          SELinux: an unprivileged account can also be confined (id -Z shows the context)

Common problems

"alice is not in the sudoers file. This incident will be reported."   She is not in the sudo (wheel) group and has no rule: sudo usermod -aG sudo alice (as root), then she must log out and in.
Group change does not apply                    Session started before the change: log out and in, or newgrp GROUP.
A rule in /etc/sudoers.d does nothing          The file name contains a dot, or the mode is not 0440, or a later rule overrides it. Run sudo visudo -c; ls -l /etc/sudoers.d.
"sudo: unable to resolve host NAME"            The hostname is missing from /etc/hosts (127.0.1.1 NAME): a warning, but it also slows every sudo.
sudo asks for the password every time          timestamp_timeout=0, or each command runs in a new terminal/ssh session (the timestamp is per terminal).
Permission denied although the file is 777     A FOLDER on the path lacks execute permission: namei -l /full/path shows the permission of every part.
"user is currently used by process"            usermod/userdel refuse while she is logged in: loginctl terminate-user alice, or pkill -u alice, then retry.
su: Authentication failure                     Wrong password, a locked account ("!" in shadow), or su restricted to a group (pam_wheel): use sudo -i.
New user cannot log in by SSH                  Home or ~/.ssh permissions (700 and 600 for authorized_keys), account locked/expired, shell is nologin, or AllowUsers/AllowGroups in sshd_config.

Tips

Give every person their own account and every program its own unprivileged account. Nothing shared, nothing root.
Use groups for access (add people to groups, give groups the permissions) so offboarding is one command.
Always usermod -aG, never usermod -G. Always visudo, never a plain editor on /etc/sudoers.
Allow exact commands in sudoers, avoid wildcards, editors and interpreters. Test files with visudo -c.
Locking a password is not offboarding: remove SSH keys, sessions, cron jobs and group memberships too.
Audit regularly: UID 0 accounts, empty passwords, sudo members, SUID files, authorized_keys. Five minutes a month finds surprises.
The most dangerous account on a system is the one nobody remembers creating.