Users and Groups
Topic 24

Users and Groups

IdentityAccounts

Every process and every file on a Linux system is owned by a numeric user ID and a numeric group ID. The kernel knows nothing about the name www-data or postgres — it stores UID 33 and GID 33, and the human-readable name is a lookup performed by user-space tools against /etc/passwd and /etc/group. That separation is the whole point of this topic: identity is a number, the name is a convenience, and access control later in the chapter is decided entirely on the numbers.

For a server that distinction has hard consequences. If two machines disagree on which UID is deploy, a file owned by deploy on one host shows up owned by some unrelated account on the other after a restore or an NFS mount — the bytes never changed, only the number-to-name mapping did. Keeping UIDs and GIDs stable across a fleet is not housekeeping; it is what stops backups and shared storage from silently handing files to the wrong account.

The Account Databases

Three flat text files define local accounts, and reading them directly is faster than any tool. /etc/passwd holds one line per account with seven colon-separated fields: login name, a password placeholder (x, meaning "see shadow"), UID, primary GID, the GECOS comment, home directory, and login shell. /etc/group lists groups as name, password placeholder, GID, and a comma-separated member list. /etc/shadow holds the actual hashed passwords plus aging policy, and it is mode 0640 owned by root:shadow on Debian so ordinary users cannot read the hashes.

# /etc/passwd — name:x:UID:GID:comment:home:shell
root:x:0:0:root:/root:/bin/bash
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
deploy:x:1001:1001:Deploy User:/home/deploy:/bin/bash

# /etc/group — name:x:GID:member1,member2
sudo:x:27:deploy
docker:x:998:deploy

The shell field doubles as an access switch. A line ending in /usr/sbin/nologin or /bin/false still has a valid UID that owns files and runs daemons, but it cannot open an interactive session — once authentication succeeds the shell is launched, and that shell immediately prints a refusal and exits non-zero, so no session is ever handed over. That is the mechanism behind every service account on the box.

UID and GID Numbering

UID 0 is root, and root is special only because of that zero — the kernel grants it the bypass on permission checks. Everything else is policy expressed as ranges in /etc/login.defs. On Debian and Ubuntu, UIDs 1 through 999 are reserved for system accounts that daemons run as, and human accounts start at 1000 (UID_MIN 1000, UID_MAX 60000); on Red Hat the human range historically started at 500 and now also starts at 1000. GIDs follow the same split.

The ranges matter because tools key off them. adduser --system allocates from the system range and creates a nologin account; adduser without it allocates from the human range and sets up a home directory and a login shell. Pick the wrong one and a daemon ends up with a login-capable account at UID 1000, or a person ends up with no home directory — neither breaks immediately, which is exactly why it survives into production.

Primary versus Supplementary Groups

Each account has exactly one primary group, named in the fourth field of its /etc/passwd line, and any number of supplementary groups, listed against the account in /etc/group. The primary group is the one new files inherit by default — create a file and its group owner is your primary group, not whichever supplementary group seems relevant. Supplementary groups grant access to resources whose group is set to adm, docker, sudo, and so on; you carry all of them at once when checking access.

The trap is that group membership is fixed at login. The kernel stamps a process with its group set when the session starts, and it is inherited by every child; adding yourself to docker with usermod -aG docker deploy changes the file on disk but not the groups your current shell already holds. The new membership applies to the next login (or to a fresh newgrp docker shell), and running id versus id deploy shows the gap — the first reports the live process credentials, the second reports the database.

# live process group set vs the on-disk record
id
# uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),27(sudo)
usermod -aG docker deploy
id deploy   # now shows docker; current shell still does not

Managing Accounts

Debian and Ubuntu ship two layers of tooling, and mixing them up is the usual source of surprise. useradd, usermod, userdel, and groupadd are the low-level binaries from the shadow package — fast, scriptable, and dumb: useradd deploy creates no home directory and sets no password unless you pass -m and configure the rest. adduser and addgroup are interactive Perl wrappers with Debian-friendly defaults that create the home directory, copy /etc/skel, and prompt for a password. Red Hat has no adduser wrapper — there adduser is just a symlink to useradd, so a script that relies on the Debian wrapper behaves completely differently.

Always remember the -a on usermod -aG. Without -a, usermod -G docker deploy replaces the entire supplementary group set with just docker, quietly dropping the account from sudo and every other group — the command succeeds, and the loss surfaces only at the next login. For shared edits to the account files, never open them in a plain editor: vipw locks /etc/passwd and vigr locks /etc/group so a concurrent useradd cannot corrupt the file mid-write, and they run a consistency check on save.

Service Accounts and nologin

A daemon should run as its own dedicated system account with no login and no password — www-data for the web server, postgres for the database, a bespoke myapp for your service. The pattern is one command: adduser --system --group --no-create-home --shell /usr/sbin/nologin myapp allocates a system UID and a matching group, refuses interactive login, and gives the process an identity whose blast radius is exactly the files you grant it. If that account is compromised through the service, the attacker inherits only what myapp can touch — not the whole machine.

Running the daemon as root instead is the lazy alternative that defeats the entire permission model. A bug in a root-owned service is a root compromise; the same bug in a nologin service is contained to that service's files. systemd makes the dedicated account trivial to wire up with User=myapp in the unit, so there is no operational excuse for the root shortcut on anything that faces the network.

Primary vs Supplementary Groups

Primary group — the single group in the account's /etc/passwd line. New files you create are owned by this group by default. Choose it for the identity a process should carry by default; on Debian the user-private-group scheme gives each account its own primary group named after the user.

Supplementary groups — the extra memberships listed in /etc/group. They grant access to shared resources (docker, adm, sudo) and are all active at once for permission checks. Choose them to share read or write access across several accounts without changing anyone's primary group — but remember the membership only takes effect at the next login.

Common Mistakes
  • Editing /etc/passwd or /etc/shadow in a plain editor while a useradd runs — a half-written line corrupts the database and can lock every account out of login. Use vipw and vigr, which lock the file and validate on save.
  • Running a network daemon as root instead of a dedicated nologin account — a single bug in that service becomes a full root compromise rather than damage scoped to one account's files.
  • Adding a user to a group with usermod -aG and expecting the running shell to gain access — the group set is fixed at login, so the change does nothing until the next login or a newgrp, and "permission denied" persists confusingly.
  • Dropping the -a from usermod -aGusermod -G docker deploy overwrites the whole supplementary set, silently removing the account from sudo and everything else, with the loss invisible until re-login.
  • Reusing a UID that previously belonged to a deleted account — every file the old account owned is now owned by the new one, handing it data it was never meant to see, with no warning from the filesystem.
  • Creating a daemon with plain adduser instead of --system — the service gets a login-capable account in the human UID range with a home directory and a real shell, expanding its attack surface for no reason.
Best Practices
  • Create one dedicated nologin system account per daemon with adduser --system --group --shell /usr/sbin/nologin, and run it from a systemd unit with User= — never as root.
  • Edit the account files only through vipw and vigr; they lock against concurrent writers and refuse to save a malformed file.
  • Grant shared access with a supplementary group (usermod -aG sharedgrp user) instead of widening permissions on the files themselves — and always keep the -a so you append rather than replace.
  • Pin UIDs and GIDs explicitly with useradd -u / groupadd -g for any account that owns files on shared storage or in backups, so the same number means the same identity on every host.
  • Confirm an effective change took hold with id in a fresh login session, not just id username against the database — the two will disagree until the session is restarted.
  • Never reuse a freed UID without first re-homing or removing the old files it owned; run find / -uid OLDUID before recycling the number.
Comparable toolsWindows — SIDs identify principals (not reusable integers), with local SAM accounts and Active Directory for the domainmacOS — Open Directory and dscl back accounts instead of flat /etc/passwd files, though the UID/GID model is the same Unix oneLDAP / AD — centralized identity (via SSSD or nslcd) that replaces per-host files when a fleet needs one source of truth

Knowledge Check

You run usermod -aG docker deploy, then immediately try to use the Docker socket in your current shell and still get "permission denied". Why?

  • The supplementary group set is fixed when the session starts; the new membership only applies to a new login or a newgrp docker shell
  • The -a flag silently prevented the membership change from ever being written into /etc/group in the first place
  • Docker socket access requires the group to be your primary group rather than a supplementary one, so usermod -g was the flag needed here
  • The account also needs a matching supplementary entry in /etc/shadow before the new group membership will ever take effect

Why is running a public-facing daemon as a dedicated nologin account instead of root the safer default?

  • A compromise of the service is contained to the files that one account can touch, rather than escalating to control of the whole machine
  • A nologin account runs its daemons measurably faster because the kernel skips the permission checks it would otherwise have to run on each syscall
  • Only a dedicated nologin account is ever permitted to bind a listening socket to the privileged network ports numbered below 1024
  • Root-owned processes are blocked from writing anywhere under /var, so a root-run service would silently fail to log

What is the difference between useradd and Debian's adduser?

  • useradd is the low-level binary that by default creates no home directory or password; adduser is a Debian wrapper with friendlier defaults that sets both up
  • adduser is strictly restricted to creating only system accounts, whereas useradd is strictly restricted to creating only the ordinary human-facing login accounts
  • useradd writes the account into the local /etc/passwd, while adduser instead registers it directly in a central LDAP directory service
  • They are completely identical on every Linux distribution; the two different names are purely cosmetic and behave the same way

Why does keeping UIDs stable across a fleet of servers matter?

  • File ownership is stored as the numeric UID, so a mismatched mapping makes a restored or NFS-mounted file appear owned by an unrelated account
  • The kernel outright refuses to mount any filesystem whose files reference a UID that the local host has never seen mapped before
  • Login is rejected outright whenever the same username happens to map to two different numeric UIDs on two separate hosts anywhere within the fleet
  • Stable UIDs are required for the passwords stored in /etc/shadow to keep hashing consistently across every host in the fleet

You got correct