Users and Groups
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 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.
- Editing
/etc/passwdor/etc/shadowin a plain editor while auseraddruns — a half-written line corrupts the database and can lock every account out of login. Usevipwandvigr, which lock the file and validate on save. - Running a network daemon as root instead of a dedicated
nologinaccount — 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 -aGand expecting the running shell to gain access — the group set is fixed at login, so the change does nothing until the next login or anewgrp, and "permission denied" persists confusingly. - Dropping the
-afromusermod -aG—usermod -G docker deployoverwrites the whole supplementary set, silently removing the account fromsudoand 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
adduserinstead 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.
- Create one dedicated
nologinsystem account per daemon withadduser --system --group --shell /usr/sbin/nologin, and run it from asystemdunit withUser=— never as root. - Edit the account files only through
vipwandvigr; 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-aso you append rather than replace. - Pin UIDs and GIDs explicitly with
useradd -u/groupadd -gfor 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
idin a fresh login session, not justid usernameagainst 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 OLDUIDbefore recycling the number.
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 truthKnowledge 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 dockershell - The
-aflag silently prevented the membership change from ever being written into/etc/groupin the first place - Docker socket access requires the group to be your primary group rather than a supplementary one, so
usermod -gwas the flag needed here - The account also needs a matching supplementary entry in
/etc/shadowbefore 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
nologinaccount runs its daemons measurably faster because the kernel skips the permission checks it would otherwise have to run on each syscall - Only a dedicated
nologinaccount 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?
useraddis the low-level binary that by default creates no home directory or password;adduseris a Debian wrapper with friendlier defaults that sets both upadduseris strictly restricted to creating only system accounts, whereasuseraddis strictly restricted to creating only the ordinary human-facing login accountsuseraddwrites the account into the local/etc/passwd, whileadduserinstead 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/shadowto keep hashing consistently across every host in the fleet
You got correct