Users, groups and the account files

passwd, shadow, group and creating accounts.

Beginner14 min · lesson 14 of 29

A Linux machine identifies you by a number, not a name. This lesson shows the three files that define every account (/etc/passwd, /etc/shadow and /etc/group), what UID 0 means, why service accounts have no login shell, how to create and remove a user with the standard tools, the difference between locking a password and expiring an account, and the three commands that tell you who can take over a machine you have just been handed. Reading these files is the first thing to do on any server you inherit or are asked to secure.

Who the machine thinks you are

id prints your identity as the kernel sees it: your UID, your primary group's GID, and every supplementary group you belong to.

deploy@web01 · Ubuntu 26.04 LTS
$ id
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),4(adm),27(sudo)
$ id root
uid=0(root) gid=0(root) groups=0(root)

This account is UID 1001, in the groups adm (which may read logs) and sudo (which may become root). One number is special. UID 0 is root, the superuser, the account that skips the permission checks the earlier lessons described. Root is defined by the number, not the name: give any account UID 0 and it is root, whatever you called it.

Where the accounts live: /etc/passwd

Every account is written in /etc/passwd, a world-readable file (it holds no secrets, only the facts needed to look an account up). Rather than read the raw file, ask the system with getent (get entries), which reads /etc/passwd and any networked directory the machine is joined to, and returns the same colon-separated format.

deploy@web01 · Ubuntu 26.04 LTS
$ getent passwd root daemon www-data sshd deploy
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin sshd:x:987:65534:sshd user:/run/sshd:/usr/sbin/nologin deploy:x:1001:1001:Deploy user:/home/deploy:/bin/bash

Split a line on its colons: username, an x where the password hash once sat (it now lives in /etc/shadow), the UID, the primary group's GID, a comment field, the home directory, and the login shell. Look at daemon, www-data and sshd: their shell is /usr/sbin/nologin, a small program whose only job is to refuse a login and exit. These are service accounts, one per running program, so that a broken web server leaves an attacker as the limited www-data and not as root. When you audit a box, a service account whose shell is /bin/bash instead of nologin is worth a hard look.

Where the secrets live: /etc/shadow

/etc/shadow holds the password hashes and the password-aging rules. Its permissions tell the story before you read a byte:

deploy@web01 · Ubuntu 26.04 LTS
$ ls -l /etc/passwd /etc/shadow
-rw-r--r-- 1 root root 1890 Sep 27 08:39 /etc/passwd -rw-r----- 1 root shadow 889 Sep 27 08:39 /etc/shadow

/etc/passwd is world-readable (644), but /etc/shadow is 640, readable only by root and the shadow group, so an ordinary user cannot copy the hashes and attack them offline. On RHEL 10 the file is stricter still, mode 0000 owned by root:root, which only root can read because root is not bound by permission bits. Each line splits on colons into the username, the hash, and the aging fields:

/etc/shadow fields (one account per line)
name : hash : last-change : min : max : warn : inactive : expire :

The dates are counted in days since 1 January 1970. The hash is self-describing: it begins with a marker between dollar signs that names the algorithm, and $y$ is yescrypt, the modern default on both Ubuntu and RHEL (both set ENCRYPT_METHOD YESCRYPT in /etc/login.defs). A single * means the account never had a usable password, normal for service accounts, and a leading ! means the password is locked. A leaked /etc/shadow lets an attacker grind guesses offline where no login prompt slows them, so the weak point becomes the password itself: keep the file root-only and treat a copy of it with the same care as the original.

The three files that define an account
/etc/passwd
World-readable
anyone may read it
name to UID/GID
the identity
home + shell
nologin blocks logins
/etc/shadow
root rw, shadow group r
RHEL: mode 0000
password hash
$y$ yescrypt today
aging + lock
expiry days, ! = locked
/etc/group
World-readable
anyone may read it
name to GID
the group
member list
supplementary members
Read left to right: who exists, the secrets and aging, who belongs to what.

Creating users and managing groups

useradd is the low-level tool and adduser is the friendlier Ubuntu wrapper. One default surprises people: on Ubuntu 26.04, useradd gives a new account the shell /bin/sh, not bash, so you pass -s /bin/bash when you want an interactive bash user.

deploy@web01 · Ubuntu 26.04 LTS
$ useradd -D | grep ^SHELL
SHELL=/bin/sh
$ sudo useradd -m -s /bin/bash ess-acct-alice id ess-acct-alice ls -ld /home/ess-acct-alice
uid=1002(ess-acct-alice) gid=1002(ess-acct-alice) groups=1002(ess-acct-alice) drwxr-x--- 2 ess-acct-alice ess-acct-alice 4096 Sep 27 08:39 /home/ess-acct-alice

The home directory came out drwxr-x--- (mode 0750), from HOME_MODE in /etc/login.defs, so other ordinary users cannot look inside it; RHEL 10 uses 0700 and defaults the shell to /bin/bash. Give every person their own named account, never a shared login, so the logs can tell who did what.

Every user has one primary group (the GID in their passwd line, which owns files they create) and any number of supplementary groups. groupadd creates a group, and getent group NAME shows its line from /etc/group: the name, an x, the GID and the member list, empty for a new group.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo groupadd ess-acct-devs getent group ess-acct-devs
ess-acct-devs:x:1003:

usermod -aG adds a user to a supplementary group; the -a (append) is not optional, as the quiz shows. The member list in /etc/group then names the user:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo usermod -aG ess-acct-devs ess-acct-alice id ess-acct-alice
uid=1002(ess-acct-alice) gid=1002(ess-acct-alice) groups=1002(ess-acct-alice),1003(ess-acct-devs)
$ getent group sudo adm ess-acct-devs
sudo:x:27:deploy adm:x:4:syslog,deploy ess-acct-devs:x:1003:ess-acct-alice

groupdel NAME removes a group again, once no user has it as their primary group.

Membership in sudo (or wheel on RHEL) is what lets an account become root through sudo, the subject of the next lesson. Some other groups are just as powerful without looking it: docker, lxd and incus can each start a container that mounts the host and take it over, and disk can read and write the raw disk. When you review who can administer a machine, read those groups the same way you read sudo.

Locking a password versus expiring an account

When someone leaves, you have two different tools and they do different things. To see both, create a second account, ess-acct-bob, and give it a password. passwd -S prints an account's password status: a new account shows L (locked), because no password has been set, and after chpasswd sets one (here a random one from openssl rand that nobody knows; interactively you would run sudo passwd NAME) it shows P, a usable password.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo useradd -m -s /bin/bash ess-acct-bob sudo passwd -S ess-acct-bob echo "ess-acct-bob:$(openssl rand -base64 18)" | sudo chpasswd sudo passwd -S ess-acct-bob
ess-acct-bob L 2026-09-27 0 99999 7 -1 ess-acct-bob P 2026-09-27 0 99999 7 -1

The other fields are the date of the last password change, the minimum and maximum days between changes (0 and 99999, the defaults from /etc/login.defs, which means no forced expiry), the warning period (7 days) and the inactivity period (-1, none). passwd -l locks the password by putting a ! in front of the hash:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo passwd -l ess-acct-bob sudo passwd -S ess-acct-bob
passwd: password changed. ess-acct-bob L 2026-09-27 0 99999 7 -1

passwd -S reports L again. But locking the password does not stop a login with an SSH key, because key logins never consult the password: the lab gave bob a key and logged in with it after this step, and it still worked. To block every login method, including keys, expire the account by setting an expiry date in the past:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo usermod --expiredate 1 ess-acct-bob sudo chage -l ess-acct-bob | grep "Account expires"
Account expires : Jan 02, 1970

usermod --expiredate 1 sets the expiry to day 1, 2 January 1970, counted in days since 1 January 1970. Use 1 rather than 0: shadow(5) warns that 0 is read either as "never expires" or as 1 January 1970, depending on the program. At login, PAM (the library every login goes through, used by sshd with its default UsePAM yes) checks the expiry for password and key logins alike: in the lab, bob's next key login was refused with Your account has expired, and sshd logged account ess-acct-bob has expired. Expiry does not end sessions that are already open. Proper offboarding does more (end their running sessions, remove their authorized_keys and group memberships, check their cron jobs and timers); the hardening lesson "Users, groups and account lifecycle" runs the full sequence. Lock or expire first and delete much later, because deleting an account destroys the record of what it did.

When the time does come, sudo userdel -r NAME removes the account with its home directory and mail spool; on a server without local mail it prints a harmless warning that no spool was found. First run sudo find / -xdev -user NAME to find files the account owns elsewhere, because anything left behind becomes a file with no owner, the red flag from the ownership lesson.

Three checks on a strange machine

Three short commands answer the questions that matter first: who is secretly root, who has no password, and who can become root. awk splits each line on colons and prints the field you ask for.

deploy@web01 · Ubuntu 26.04 LTS
$ awk -F: '($3 == 0) {print $1}' /etc/passwd
root
$ sudo awk -F: '($2 == "") {print $1}' /etc/shadow
$ getent group sudo
sudo:x:27:deploy

The first prints every account with UID 0; a healthy system prints only root, and a second name is a backdoor superuser. The second prints any account with an empty password field, which should print nothing. The third shows who holds admin rights through the sudo group.

The group is not the whole answer. Ubuntu's /etc/sudoers also grants full rights to %admin (% marks a group name), a group kept from older releases, and reads every file in /etc/sudoers.d/, where a rule can name a single user. sudo -l -U NAME, covered in the next lesson, reports what one account can really run. It also helps to know the numbering: /etc/login.defs sets UID_MIN 1000, so accounts for people start at 1000 and system accounts such as sshd (UID 987 here) sit below it. A human-looking name with a low UID, or a service name with a UID of 1000 or more, is worth a second look.

Try this

Create a throwaway user with sudo useradd -m -s /bin/bash tester and confirm with id tester and ls -ld /home/tester that the home directory is drwxr-x---. Create a group with sudo groupadd testers, add the user with sudo usermod -aG testers tester, and check that id tester shows both groups and getent group testers lists the member. Set a password with sudo passwd tester, lock it with sudo passwd -l tester and read sudo passwd -S tester, then expire the account with sudo usermod --expiredate 1 tester and read sudo chage -l tester. Run awk -F: '($3 == 0) {print $1}' /etc/passwd and confirm only root appears. Remove the user with sudo userdel -r tester and the group with sudo groupdel testers.

Takeaway

Root is whoever has UID 0, and admin power comes from the sudo, wheel, docker and similar groups, so read /etc/passwd, /etc/shadow permissions and getent group on any box you inherit. To take away access, expire the account with usermod --expiredate 1 rather than only locking the password, because passwd -l leaves SSH-key logins working.

Quick check
01getent group docker on a server shows docker:x:998:deploy,sam, and neither user is in the sudo group. What is the real security picture?
Incorrect — sudo is not the only route; docker membership is another, because a container can mount the whole host.
Incorrect — A container can bind-mount the host filesystem, so that isolation does not hold against a determined member.
Incorrect — docker access grants full control of the host, not merely read access to one file.
Correct — Membership in docker is in practice equal to root, so it must be reviewed exactly like the sudo group.
02A departing admin's password is locked with passwd -l, but they still log in over SSH the next day. How?
Incorrect — passwd -l takes effect immediately for password logins; the issue is that this login did not use a password.
Correct — Key authentication never consults the password, so passwd -l does not stop it; expiring the account (usermod --expiredate 1) does.
Incorrect — passwd -l does change the shadow file, putting a ! before the hash; it simply does not affect key logins.
Incorrect — A duplicate UID 0 would be a separate problem; here the ordinary account logs in by key, which the lock does not touch.
03You mean to add alice to the docker group and run sudo usermod -G docker alice, without -a. alice was already in the sudo group. What happens?
Incorrect — That is what -aG does; without -a there is no appending.
Incorrect — usermod runs happily; -G without -a is valid and simply replaces the supplementary set.
Correct — -G sets the complete supplementary list, so without -a it wipes her other groups, including sudo.
Incorrect — usermod does modify group membership, and docker is not special; the missing -a is what causes the damage.

Related