07 — Users, Groups & Authentication
Linux is a multi-user system. Every process runs as a user, every file is owned by a user, and access is controlled by comparing the process's identity to the file's ownership. This chapter covers the UID/GID model, the PAM authentication stack, sudo, SSH key authentication, and the gotchas that cause "permission denied" mysteries.
Users and UIDs
A user is identified by a UID (user ID, an integer). The username is just a human-friendly label mapped to a UID via /etc/passwd.
| UID Range | Purpose |
|---|---|
| 0 | root (the superuser) |
| 1–999 | System accounts (daemon, bin, www-data, nobody) |
| 1000–60000 | Regular users (default range on most distros) |
UID 0 is root: the kernel checks uid == 0 for privileged operations. The name "root" is convention; what matters is UID 0.
A process has three UIDs:
- real UID — who launched the process.
- effective UID — whose permissions it currently runs with (changed by setuid).
- saved UID — allows switching back (used by setuid programs).
id # uid=1000(alice) gid=1000(alice) groups=1000(alice),27(sudo)
whoami # alice (effective user)
echo $UID # 1000 (real UID, shell variable)
/etc/passwd — User Database
Format: username:password:UID:GID:GECOS:home:shell
cat /etc/passwd
# root:x:0:0:root:/root:/bin/bash
# daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
# alice:x:1000:1000:Alice Smith,,,:/home/alice:/bin/bash
- password —
xmeans the hash is in/etc/shadow.*or!means the account is locked. - shell —
/usr/sbin/nologinand/bin/falseprevent interactive login (used for service accounts).
/etc/shadow — Password Hashes
Only root can read this. Format: username:hash:lastchange:min:max:warn:inactive:expire:reserved
sudo cat /etc/shadow
# root:$y$j9T$...:19500:0:99999:7::: ← yescrypt hash
# bob:!:19500:0:99999:7::: ← locked (no hash)
- hash —
$y$= yescrypt (modern),$6$= SHA-512,$5$= SHA-256,$1$= MD5 (obsolete),$2b$= bcrypt.*or!= locked.
Managing Users
# Complex Implementation: create a service account with minimal privileges
sudo useradd -r -s /usr/sbin/nologin -M -c "App Service Account" appuser
# -r: system account (UID < 1000)
# -s /usr/sbin/nologin: no interactive login
# -M: no home directory
# -c: GECOS (description)
# Create a regular user with home + groups:
sudo useradd -m -s /bin/bash -G sudo,developers alice
# -m: create home
# -G: supplementary groups (comma-separated)
Caveat & Anti-Pattern: usermod -G vs -aG
# NAIVE: -G (without -a) REPLACES the supplementary group list
sudo usermod -G docker alice
# Alice is now ONLY in docker — removed from sudo, audio, etc.
# PRODUCTION: -aG (append) adds the group while keeping existing ones
sudo usermod -aG docker alice
# Alice is now in docker AND sudo AND everything else she had
# This is the #1 usermod footgun — always use -aG
sudo — Delegate Privilege
sudo command # run as root
sudo -u alice command # run as alice
sudo -i # interactive root shell (login)
sudo -s # interactive root shell (non-login)
sudo -l # list what you're allowed to do
/etc/sudoers — The Rules
Never edit /etc/sudoers directly — a syntax error can lock you out of root. Use visudo:
sudo visudo
# Better: use /etc/sudoers.d/ for custom rules
sudo visudo -f /etc/sudoers.d/alice
# alice ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
sudo chmod 440 /etc/sudoers.d/alice # sudoers files must be mode 440
Caveat & Anti-Pattern: sudo echo > /file Fails
# NAIVE: the redirect runs as YOU, not root
sudo echo "x" > /etc/protected # Permission denied (the > is yours, echo runs as root)
# PRODUCTION: use tee (runs as root via sudo)
echo "x" | sudo tee /etc/protected > /dev/null
# Or:
sudo bash -c 'echo "x" > /etc/protected'
SSH Authentication
SSH Keys
# Complex Implementation: modern SSH key setup with Ed25519
ssh-keygen -t ed25519 -C "alice@workstation"
# -t ed25519: modern, fast, secure (preferred over RSA)
# Set a passphrase for the private key (recommended)
# Copy the public key to a server:
ssh-copy-id alice@server
# SSH requires strict permissions (silently refuses if wrong):
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/id_ed25519 # private key — must be 600
chmod 644 ~/.ssh/id_ed25519.pub # public key — can be readable
sshd_config Hardening
PermitRootLogin no # disable root login via SSH
PasswordAuthentication no # require key auth (after setting up keys!)
PubkeyAuthentication yes
AllowUsers alice bob # whitelist (optional)
MaxAuthTries 3 # limit password guesses
ClientAliveInterval 300 # idle timeout (5 min)
sudo sshd -t # test config syntax (don't restart if errors!)
sudo systemctl reload sshd # reload without dropping connections
PAM — Pluggable Authentication Modules
PAM is the authentication framework. Login, sudo, su, ssh, cron — all call PAM. Config in /etc/pam.d/:
cat /etc/pam.d/common-auth # the password-checking stack
# auth [success=1 default=ignore] pam_unix.so nullok
# auth requisite pam_deny.so
# auth required pam_permit.so
Each line: type control module [args]
- type:
auth(verify identity),account(is account valid?),password(change password),session(setup/teardown). - control:
required(must pass, continue),requisite(must pass, stop on fail),sufficient(pass = success, stop).
NSS — Name Service Switch
/etc/nsswitch.conf controls where user/group lookups happen — local files, LDAP, etc.:
cat /etc/nsswitch.conf
# passwd: files systemd
# group: files systemd
# hosts: files dns
# "files" → /etc/passwd, /etc/group; "dns" → DNS for hostnames
getent passwd alice # find alice (in files OR LDAP — works for any source)
getent group developers # find group
💡 Tips & Tricks
- Idiom: always use
usermod -aG(not-G) to add groups —-Galone replaces the supplementary group list, removing the user from all other groups. This is the #1usermodfootgun. - Idiom: use
visudo(never edit/etc/sudoersdirectly) —visudochecks syntax before saving. A syntax error insudoerscan lock you out of root entirely. - Idiom: use
sudo -i(notsudo su -) for a root shell —sudo -iis the idiomatic way; it's a single command, uses PAM properly, and logs the session. - Idiom: use
echo "x" | sudo tee /etc/file(notsudo echo "x" > /etc/file) — the redirect runs as you, not root, sosudo echo > /etc/filefails. - Idiom: use
ed25519SSH keys (not RSA) — Ed25519 is faster, shorter, and more secure. Only fall back torsa -b 4096if the server is ancient. - Debug: use
sudo -lto see what a user can do — lists all sudoers rules matching the current user.
⚠️ Edge Cases & Gotchas
usermod -G(without-a) replaces groups:sudo usermod -G docker aliceremoves alice from sudo, audio, etc. — she's now only in docker. Always use-aG.sudo echo > /filefails:sudo echo "x" > /etc/protectedrunsechoas root but the>redirect runs as you. Useecho "x" | sudo tee /etc/protected.suwithout-keeps your environment:su root(no dash) keeps your PATH, HOME — root commands may not be found. Alwayssu - rootfor a login shell.- SSH key permissions must be strict:
~/.sshmust be700,authorized_keys600, private key600. SSH silently refuses keys that are group/world-readable. - Service accounts should have
/usr/sbin/nologin: setting a shell like/bin/bashonwww-dataallows interactive login (if someone gets the password). - Passwordless sudo (
NOPASSWD:) is a security hole: any compromise of that user = instant root. Limit it to specific commands. - Group membership changes need re-login: if you add alice to
docker, her existing shells still have the old groups. She must log out and back in (ornewgrp docker). - Deleting a user doesn't kill their processes:
userdel aliceremoves the account, but her running processes keep going. Kill them first:sudo pkill -u alice.
🧠 Spot the Bug
An admin adds alice to the docker group so she can run docker without sudo:
sudo usermod -G docker alice
Alice logs out, logs back in, and docker ps works — but she can no longer use sudo. What happened?
Answer
-G without -a replaces the supplementary group list. Before, alice was in alice, sudo, developers. After usermod -G docker alice, she's in only docker — sudo and developers are gone. She lost her sudo access.
Fix — always use -aG (append):
sudo usermod -aG docker alice
Now alice is in docker and all her existing groups (sudo, developers, etc.).
The -a flag means "append" — it adds to the supplementary group list instead of replacing it. Without -a, -G sets the group list to exactly what you specify, dropping everything else.