Linux User Management: Users, Groups, sudo, and Security
A working mental model for Linux accounts: how /etc/passwd and /etc/shadow fit together, when to use a primary group versus a supplementary one, how sudo actually decides, and the full lifecycle of useradd / usermod / passwd / chage / userdel — including the PAM stack underneath.
If you only ever ran useradd and passwd on a single laptop, you can probably get away without thinking about any of this. The moment more than one human (or more than one service) shares a host, “user management” stops being paperwork and starts being the security model: it decides who can log in, which UID owns the files a process writes, which commands sudo will lift to root, and how long a stolen password remains useful.
This article walks the model end to end. We start with the raw shape of /etc/passwd and /etc/shadow — because every command in this space is just a wrapper around editing those files. Then we cover the user/group relationship (the bit people most often get backwards), the full lifecycle commands (useradd, usermod, passwd, chage, userdel), sudo and visudo done right, and finally the PAM stack that ties authentication, account policy, password rules and session setup together.
The mental model: accounts are rows in three text files
Before any command, the data model. Linux accounts live in three flat text files, one row per entity, fields separated by colons:
/etc/passwd— public: one row per account (humans and services). World-readable, root-writable./etc/shadow— secret: one row per account holding the password hash and the aging policy. Root-only./etc/group— one row per group, with a comma-separated member list at the end.
There is also /etc/gshadow (group passwords and group admins, rarely used in practice) and /etc/skel (the template directory copied into every new home).

The seven /etc/passwd fields, left to right:
- username — the login name. Unique per host.
- password placeholder — almost always
x, meaning “the real hash is in/etc/shadow”. A literal*or empty field means no usable password (different from “locked”). - UID — the numeric user id. The kernel only ever sees this number; the name is a convenience for humans.
0is root,1–999are reserved for services,1000+are humans on modern distros (CentOS 6 and earlier started humans at500). - GID — the primary group id. New files this user creates are owned by this group by default.
- GECOS — historically the General Electric Comprehensive Operating System full-name field. Today: free-form comment.
chfnedits it. - home directory —
$HOMEafter login. Created from/etc/skelif you pass-mtouseradd. - login shell — what
execs after authentication./sbin/nologinor/usr/sbin/nologinmakes the account non-interactive (the right setting for service accounts).
The corresponding /etc/shadow line carries: the hash (prefix tells you the algorithm — $6$ is SHA‑512, $y$ is yescrypt on newer Debian/Ubuntu, $1$ is MD5 and you should never see it on a modern host), the day-count of the last password change (days since 1970-01-01), and five aging fields: min, max, warn, inactive, expire. A leading ! or * on the hash means “locked” — the account exists but no password will ever match.
Never edit these files in a normal editor. Use
vipwandvigr, which take the right locks (/etc/passwd.lock,/etc/shadow.lock) so you can’t corrupt the file by saving whileuseraddis also running. Better yet, use the wrapper commands.
Users, primary group, supplementary groups
This is the part most tutorials get muddled. Every account has exactly one primary group (the GID in /etc/passwd) and zero or more supplementary groups (rows of /etc/group whose member list contains this user).

Why two kinds?
- The primary group decides default ownership: when
alicerunstouch foo, the file is ownedalice:alice(or whatever her primary group is). It also decides what the kernel sets as the process’segidat login. - Supplementary groups grant extra access. Adding
alicetodockerlets her talk to the docker socket; adding her tosudo(Debian) orwheel(RHEL) lets her escalate. None of this changes the default ownership of files she creates.
A common confusion: if alice’s primary group is alice, you will not see her name in the /etc/group row for alice — primary membership lives in /etc/passwd, not /etc/group. The member list in /etc/group only enumerates the supplementary members. To see the union, ask the kernel:
| |
The lifecycle commands
Every account passes through the same five stages. Each command edits a specific subset of the files above; once you know which, recovery and auditing become trivial.

useradd — create
| |
The flags worth memorising:
| Flag | Effect |
|---|---|
-m | Create the home directory (and copy /etc/skel into it). Without -m you get an account with $HOME set to a path that doesn’t exist. |
-s SHELL | Login shell. /bin/bash, /bin/zsh, or /sbin/nologin for service accounts. |
-g GROUP | Primary group. Default is to create a same-name group with the same GID (this is “user private groups” — the standard layout on every modern distro). |
-G g1,g2 | Supplementary groups, comma-separated. Don’t forget the , between them. |
-u UID | Pin a UID. Useful for keeping NFS-shared filesystems consistent across hosts. |
-r | Make a system user (UID below the normal range, no aging, no /home). |
-c "..." | The GECOS / comment field. |
Two common shapes:
| |
-M says “do not create the home directory” — many service accounts don’t need one because their state lives somewhere else (/var/lib/<service>).
usermod — modify in place
The trap with -G is that it replaces the existing supplementary list. Use -aG to append:
| |
Other useful invocations:
| |
A locked account still exists, still owns its files, and can still be the target of su - alice from root. It just can’t be authenticated against by password. This is exactly what you want when an employee leaves but their files might still be needed for a few weeks.
passwd and chage — secrets and their expiry
passwd sets passwords; chage sets the policy that decides when a password must change.
| |
chage -l shows the current policy; chage sets it:
| |
Note that “max age” alone is not the security control most people think it is — modern guidance (NIST SP 800-63B) actually argues against forced periodic rotation for human passwords because it pushes people to predictable patterns. Use it for service accounts and for compliance regimes that demand it; rely on MFA, length and breach detection for humans.
userdel — and why you should lock first
| |
The hazard: any file alice owned that lived outside $HOME becomes an orphan owned by a bare UID. The next account that gets that UID silently inherits those files. The fix is procedural, not technical:
usermod -L alice(orpasswd -l alice) — lock immediately, no more logins.- Wait. Cron jobs need to drain, open shells need to close, file ownership audits need to run.
find / -uid $(id -u alice) -printto see what she owned outside$HOME. Reassign withchownor archive.- Only then
userdel -r alice.
Group management
| |
gpasswd is the dedicated wrapper for /etc/group and /etc/gshadow; it is generally safer than poking those files via usermod -G, and it lets you delegate group membership management to a non-root user via the “group administrator” role.
sudo: how a single command becomes root
Logging in directly as root is wrong for two reasons. First, rm -rf / deserves friction. Second, the audit log just shows “root did things” — completely useless when more than one person has the password. sudo fixes both: each call is logged with the real user, and the policy file lets you grant exactly the privilege needed and no more.

A sudoers rule has to match on five dimensions before the command runs:
user_or_%group host=(runas_user:runas_group) TAG= command_list
Read out loud: “who, on which host, may run as whom, with what tags, which commands.”
Edit with visudo, never your editor
| |
visudo takes the lock, runs sudoers syntax validation on save, and refuses to install a broken file. Editing /etc/sudoers directly with vim is one of the few mistakes that can lock you out of a server with no way back short of single-user mode — because if sudo itself can’t parse its config, nothing will let you fix it.
Prefer drop-ins under /etc/sudoers.d/ (they’re loaded by an @includedir line in the main file). They version, package, and review more cleanly than a single monolithic file.
The shapes you actually need
# Full root, password required.
alice ALL=(ALL:ALL) ALL
# Group-based: members of `wheel` (RHEL) or `sudo` (Debian) get root.
%wheel ALL=(ALL:ALL) ALL
# Narrow: bob can ONLY restart nginx, no password prompt.
bob ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx, \
/usr/bin/systemctl status nginx
# Aliases keep large policies readable.
Cmnd_Alias NGINX_CTL = /usr/bin/systemctl restart nginx, \
/usr/bin/systemctl reload nginx, \
/usr/bin/systemctl status nginx
User_Alias ONCALL = alice, bob, carol
ONCALL ALL=(root) NOPASSWD: NGINX_CTL
A few non-obvious rules:
- The command list must be absolute paths.
bob ... NOPASSWD: nginxdoes nothing useful and may even open a hole, because users could arrangenginxto mean something else on$PATH. - Commands are matched as prefixes unless you pin arguments.
/usr/bin/systemctl restart nginxallows exactly that;/usr/bin/systemctlallows anything systemctl can do (includingpoweroff). NOPASSWD:is convenient and dangerous. Reserve it for non-interactive automation; for humans, ask for the password.Defaults requiretty(sometimes shipped on RHEL) breakssudoover non-tty channels. Disable it for automation accounts with a per-userDefaults:bot !requiretty.
What sudo reads on disk
In order: /etc/sudoers, then everything under /etc/sudoers.d/ in lexical order, then group memberships are resolved, then Defaults are applied. Use sudo -ll to dump the full effective policy for the calling user — far more reliable than re-reading the files by hand.
su versus sudo
su - opens a shell as another user (default: root) given that user’s password. sudo runs a command as another user given your password. Almost always prefer sudo: the audit trail is better, and you never have to share root’s password.
PAM: the layer underneath everything
sudo, sshd, login, gdm, cron, su, passwd, crond — none of them implement password checking themselves. They all delegate to PAM (Pluggable Authentication Modules), a stack of .so libraries configured per service in /etc/pam.d/. Understanding PAM is what turns “I don’t know why this account can’t log in” into a five-second debugging exercise.

A PAM service file has up to four stacks:
- auth — prove identity.
pam_unix.sochecks/etc/shadow;pam_sss.sotalks to SSSD/LDAP/AD;pam_google_authenticator.soadds TOTP. - account — even if the password is right, is this account allowed to log in now? Aging policy,
nologinflag, time-of-day, source host. - password — only consulted on
passwd. Strength rules (pam_pwquality.so), reuse rules (pam_pwhistory.so), then write the new hash (pam_unix.so). - session — set up the working environment after a successful login: rlimits (
pam_limits.so), systemd user slice (pam_systemd.so), create the home dir on first login (pam_mkhomedir.so), recordlastlog.
Each line carries a control flag that decides how its result combines with the rest of the stack:
required— must succeed; if it fails the whole stack fails, but PAM keeps running the rest so the user can’t tell which line failed (that’s intentional — telling them leaks information).requisite— must succeed; on failure the stack aborts immediately.sufficient— if it succeeds and no earlierrequiredfailed, the stack passes right away.optional— result is ignored unless it’s the only module in the stack.
A practical example — turning on a strong password policy on Debian. Edit /etc/pam.d/common-password:
password requisite pam_pwquality.so retry=3 minlen=12 \
dcredit=-1 ucredit=-1 ocredit=-1 lcredit=-1 \
difok=4 enforce_for_root
password required pam_pwhistory.so remember=5 use_authtok
password [success=1 default=ignore] pam_unix.so obscure use_authtok yescrypt
What this says: at least 12 characters, requiring at least one digit / upper / symbol / lower (-1 means “credit at most one of these”); reject anything that shares 4+ characters with the old password; remember the last 5 hashes and refuse reuse; finally write with the modern yescrypt hash. Test with passwd <yourself> as a non-root account before logging out.
The single most useful debugging tool here is the journal:
| |
If a login fails with the password definitely correct, it’s almost always one of: account locked (! in /etc/shadow), shell set to /sbin/nologin, pam_nologin blocking everyone because /etc/nologin exists, AllowUsers/DenyUsers in sshd_config, or password aged past inactive.
Patterns from the field
A shared project directory
Goal: /srv/project is read/write for everyone in developers, invisible to anyone else, and any new file inside it stays group-owned by developers so coworkers don’t accidentally lock each other out.
| |
The 2 in 2770 is the SGID bit on a directory: new files inherit the directory’s group instead of the creator’s primary group. Without it, when alice (primary group alice) creates a file, it lands as alice:alice, and bob can’t edit it.
For finer control — say, “developers can write but carol is read-only” — reach for POSIX ACLs:
| |
A service account, done right
| |
--system gets a UID below the human range and skips aging. --user-group makes the same-name group. --no-create-home because the state directory is created explicitly, with the right mode. The systemd unit then runs as User=myapp Group=myapp, which is the only identity that should ever own the app’s data on disk.
Tiered sudo
Cmnd_Alias READ_LOGS = /usr/bin/journalctl, /usr/bin/tail, /usr/bin/less
Cmnd_Alias NGINX_CTL = /usr/bin/systemctl restart nginx, \
/usr/bin/systemctl reload nginx, \
/usr/bin/systemctl status nginx
# Full admin
alice ALL=(ALL:ALL) ALL
# On-call: only nginx, no password (paged at 3am, can't fumble auth)
bob ALL=(root) NOPASSWD: NGINX_CTL
# Support: read logs, with password
carol ALL=(root) READ_LOGS
Bulk creation from a CSV
| |
chpasswd reads user:password lines and is the right tool for batch updates; passwd --stdin is RHEL-only. chage -d 0 is the trick that forces a reset on first login without giving the user a non-expiring random secret.
Where to go next
This article gave you the operational model: the file shapes, the lifecycle commands, the sudo policy language and the PAM stack. The next two pieces in the series build on it directly:
- Linux File Permissions
—
rwx, the SUID/SGID bits we used for the shared directory above, and POSIX ACLs. - Linux System Service Management
— how the
User=/Group=/DynamicUser=directives in systemd units make service accounts mostly redundant.
Worth reading on the side: man 5 sudoers, man 8 pam.d, man 5 shadow, and the FreeIPA / SSSD documentation if you ever need centralised identity for more than a handful of hosts.