User and Group Accounts: passwd, shadow, useradd, and Groups
Linux is designed to run more than one user's work at the same time, and each account needs its own files kept separate from everyone else's. That separation is enforced by two numbers attached to every account: a numeric UID and a numeric GID. Every mechanism this module covers — permission bits, ownership, sudo, and umask — is built on top of these accounts, so this is where the module starts.
You will see where account data actually lives, how a primary group differs from a supplementary group, how a system account differs from a human one, and how to use the useradd, usermod, groupadd, and gpasswd command families safely.
Warning
Placeholders such as <username> and <group> in the commands below are not literal values — replace them with a real, verified value before running anything. Do not copy a command containing < or > straight into a shell; those characters have shell redirection meaning. Run every account- and group-creation or deletion example only on a disposable lab VM.
What an Account Is and Where It Lives
Every user account consists of a login name, a unique UID, a primary group GID, a comment field, a home directory, and a login shell. This data is stored in a handful of plain text files, each line made of colon-separated (:) fields.
Instead of reading these files directly with cat, use getent, which goes through the Name Service Switch (NSS). Account data does not have to come only from a local file — it can come from LDAP or another directory service instead, so a local grep is not a complete check.
/etc/passwd: the Core Account Record
A representative result:
The seven fields mean:
| # | Field | Meaning |
|---|---|---|
| 1 | login name | the username visible in ls -l output |
| 2 | password placeholder | x means the real hash lives in /etc/shadow |
| 3 | UID | the account's numeric identifier |
| 4 | GID | the primary group's numeric identifier |
| 5 | GECOS | a comment, usually the full name or the account's purpose |
| 6 | home | the directory entered at login |
| 7 | shell | the program started at login |
The x in the second field records an important piece of history: /etc/passwd is world-readable, so storing a password hash there directly used to be a real risk. The hash has since been moved to /etc/shadow, which only the superuser can read.
The seventh field is not always an interactive shell. Service accounts commonly set it to /usr/sbin/nologin or /bin/false, which blocks interactive login through that account without preventing a service from running its own process under that identity.
/etc/shadow: Password and Aging Data
/etc/shadow is readable only by root, and it holds the password hash and its aging information:
A representative, shortened result:
The second field is the password hash. $6$ marks SHA-512, $y$ marks yescrypt; current Ubuntu and Debian are moving toward yescrypt. If the field begins with ! or *, the password is locked and login by password is disabled. The remaining fields cover the last password change date, minimum and maximum age, a warning period, and an expiry date; chage is a more convenient way to read them.
Reading and Setting Password Aging with chage
Parsing the raw day-count fields in /etc/shadow by hand is error-prone — they are stored as a number of days since 1970-01-01, not a calendar date. chage -l translates them into a readable report:
Last password change : Jul 15, 2026
Password expires : never
Password inactive : never
Account expires : never
Minimum number of days between password change : 0
Maximum number of days between password change : 99999
Number of days of warning before password expires : 7
An ordinary user can read their own aging data with chage -l; reading another account's requires root. Setting a policy needs sudo:
-M 90sets the maximum password age to 90 days — after that many days without a change, the password is treated as expired.-W 14sets a 14-day warning period before expiry, during which the user is reminded at login but can still authenticate normally.-E <YYYY-MM-DD>sets a hard account expiration date, independent of the password itself — useful for a contractor or temporary account that must stop working entirely after a known date, even if the password was never expired.
Danger
Changing password-aging policy on a real account can lock out its own owner, including an administrator, if the new -M value is shorter than the time since the last password change. Check chage -l first, apply a policy change to a test account before a production one, and keep a second authenticated session or console access open in case a lockout needs to be reversed with sudo chage -M 99999 <username>.
What the User Actually Sees at Login
Once a password has expired (its age has passed -M, or the account itself has passed -E), the behavior differs depending on which limit was hit:
- password expired (
-Mexceeded): at the next successful authentication, PAM forces the user to set a new password before the session is allowed to continue — the login prompt does not simply fail, it interrupts with "Your password has expired" and a mandatory password-change prompt. - account expired (
-Eexceeded): the login is refused outright with a message such as "Your account has expired," and no password prompt is even offered — a-Eexpiry is a hard stop, not something the user can resolve themselves.
This distinction is one interview question in itself: a locked-out user reporting "I can't log in" needs the administrator to check which of the two actually happened, using chage -l, before deciding whether the fix is "let them set a new password" or "the account itself needs its expiration date extended first."
Danger
Do not edit /etc/passwd, /etc/shadow, or /etc/group by hand in a plain text editor. One broken line can lock out every login, or break sudo entirely. Use vipw and vigr when a manual edit is unavoidable — they lock the file and validate its syntax on save. For everyday changes, use the useradd/usermod family described below instead.
/etc/group and /etc/gshadow: Groups
A representative result:
Four fields: group name, a password placeholder (x means gshadow is in use), the GID, and a comma-separated member list. Notice that the ali group's member list is empty, yet the ali account is still in that group — because its primary GID is recorded as 1000 in /etc/passwd. Primary group membership is not repeated in the member list.
Primary and Supplementary Groups
An account belongs to exactly one primary group at a time, and any number of supplementary groups. Keep the distinction clear:
- The primary group comes from the GID field in
/etc/passwd. A newly created file belongs to this group, unless it is created inside asetgiddirectory. - Supplementary groups come from the member list in
/etc/group. They grant access, but they do not decide a new file's group on their own.
Seeing this directly:
A representative result:
id -gn shows only the primary group (ali); id -Gn shows every group. groups <username> returns the same list.
Note
A user who is added to a new group does not gain that group in an already-open shell session immediately. Group membership is computed at login time. If id <username> shows the new group but id -Gn in the current session does not, open a new login session. Do not paper over this with sudo chmod or a wider permission — the real cause is simply that membership has not been loaded yet.
If you need to create a file under a different primary group temporarily, newgrp <group> opens a new shell with that group as primary; exit returns you to the previous shell. This is not a permanent change.
System Accounts versus Human Accounts
Not every account represents a person. A web server, a database, or another application process should run under its own account — otherwise it runs as root and widens the attack surface unnecessarily. System and human accounts are separated by UID range, which is configured in /etc/login.defs:
A representative result (Ubuntu/Debian):
Human accounts usually start at UID 1000; system accounts fall in the 100–999 range. UID 0 is always root. This boundary is not a convention — it is a real setting that tools such as useradd -r read to decide which range to draw a number from. The RHEL family follows the same structure, though the exact numbers can differ, so read login.defs instead of assuming.
lslogins gives a convenient overview:
--user-accs lists human accounts, --system-accs lists system accounts. lslogins ships in the util-linux package and is normally available on both Ubuntu and the RHEL family.
Lab Environment
The commands below change system state: they create and delete accounts and groups. Run them only on a disposable Ubuntu Server LTS VM or container, and take a VM snapshot beforehand so you can return to a clean state afterward. These commands need sudo because account management requires superuser authority.
Confirm the starting state first:
getent passwd deploy || echo 'deploy does not exist'
getent group reporters || echo 'reporters does not exist'
If both names come back empty, the lab starts clean. If either name is already taken, pick a different, unused name for the examples below.
useradd versus adduser: Which One, and When
There are two distinct approaches, and they should not be confused:
useradd: a low-level command available on every distribution. It is not interactive and does exactly what its options say — no more, no less. It fits scripts and automation.adduser: an interactive, high-level script on Debian and Ubuntu. It creates a home directory, prompts for a password, sets up a private group, and is generally more convenient for creating a human account by hand. On the RHEL family,adduseris often just a symlink touseradd, so it is not interactive there.
The two are not equivalent: useradd -m -s /bin/bash <name> and adduser <name> can produce a similar result, but adduser performs extra setup on its own. Use useradd in a portable script, and document the exact behavior you rely on.
Key useradd Options
| Option | Purpose |
|---|---|
-m |
create the home directory and copy the /etc/skel template |
-s <shell> |
set the login shell |
-c "<comment>" |
set the GECOS comment |
-g <group> |
set the primary group |
-G <g1,g2> |
set the list of supplementary groups |
-u <UID> |
request a specific UID |
-d <path> |
set the home directory path |
-r |
create it as a system account (UID from the system range) |
useradd does not create a home directory by default; -m is required for that (some RHEL configurations may enable CREATE_HOME by default). With -m, the shell configuration files under /etc/skel are copied into the new home directory.
Creating a Human Account
sudo useradd -m -s /bin/bash -c "Dilnoza Karimova" -G reporters dkarimova
getent passwd dkarimova
sudo passwd dkarimova
useradd creates the account without a password, leaving login by password locked. passwd <username> sets a password and unlocks password login. -G reporters adds the user to the reporters supplementary group; that group must already exist (created below).
Verification:
id shows the UID and both primary and supplementary groups. Since this is a human account, the UID is expected to be above 1000.
Creating a System (Service) Account
An application account does not need interactive login and should come from the system UID range:
sudo useradd -r -s /usr/sbin/nologin -d /var/lib/reportd -c "report daemon" reportd
getent passwd reportd
A representative result:
-r draws the UID from the system range, and -s /usr/sbin/nologin blocks interactive login. -m was deliberately not used here: an application account has no use for the shell configuration files in /etc/skel. If a runtime data directory is needed, create it explicitly with install -d, giving it a specific owner, group, and mode — the same tool used in Ownership.
usermod: Changing an Existing Account
usermod changes the comment, shell, primary group, and supplementary groups. The most dangerous, and most commonly misused, part is changing supplementary groups.
-aG: the Detail That Matters Most
-a (append) adds to the existing supplementary groups. Without -a, -G replaces all of the user's existing supplementary groups with only the ones listed. This can accidentally remove someone from sudo or another important group.
Danger
When adding a supplementary group, write usermod -aG almost without exception. Dropping the -a and running usermod -G docker <username> can remove the user from the sudo group and take away their administrator access. Record the current groups with id <username> before the change, and check again afterward.
Other useful changes:
sudo usermod -c "Dilnoza K." dkarimova # change the comment
sudo usermod -s /usr/sbin/nologin dkarimova # block the shell
sudo usermod -L dkarimova # lock login by password
sudo usermod -U dkarimova # unlock it
-L prepends ! to the password hash to block password login, and -U reverses that. This does not automatically close SSH keys or other authentication methods — review every access path separately if the goal is to fully disable an account.
userdel: Removing an Account
This removes the account from /etc/passwd, /etc/shadow, and the group lists, but leaves the home directory in place. To also remove the home directory and mail spool:
Warning
Files owned by the account outside its home directory (for example, under /srv or /var) are not removed, and its UID no longer resolves to a name. If that UID is later assigned to a different account, the old files can appear to belong to the new one. Because of that, audit an account's files before deleting it: sudo find / -xdev -uid <UID> -printf '%p\n'. Before removing the home directory with -r, confirm no data that matters is left inside it — this step cannot be undone.
Managing Groups
Group commands mirror the account commands:
-g requests a specific GID; without it, groupadd automatically picks the next free one. Changing group properties:
Removing a group:
A group cannot be deleted while it is still some user's primary group — change that user's primary group first.
gpasswd: Managing Membership Precisely
gpasswd is a precise way to manage group members:
sudo gpasswd -a dkarimova reporters # add one user
sudo gpasswd -d dkarimova reporters # remove one user
sudo gpasswd -M ali,dkarimova reporters # set the full member list
getent group reporters
gpasswd -a adds one membership, just like usermod -aG, but because it targets a single group, it cannot accidentally affect other groups the way an unqualified -G can. gpasswd -M replaces the entire list — record the current members with getent group before using it.
Practical Scenario: A Service Account and a Team Group
Requirement: the reportd service runs under its own account; several human accounts in the reporters group can read and write report files; new files automatically pick up the reporters group and are group-writable. This scenario ties together useradd, groupadd, setgid, and umask in one practical task.
Do this only on a lab VM, after taking a snapshot.
1. Create the Group and the Service Account
sudo groupadd reporters
sudo useradd -r -s /usr/sbin/nologin -d /var/lib/reportd -g reportd \
-G reporters reportd 2>/dev/null || \
sudo useradd -r -s /usr/sbin/nologin -d /var/lib/reportd \
-G reporters reportd
getent group reporters
id reportd
The first attempt sets a dedicated reportd group as primary; if it does not exist yet, useradd -r usually creates a same-named group for the user automatically, which is why the second form is included as a fallback. Either way, reportd ends up in the reporters supplementary group.
2. A Shared Directory with setgid
install -d creates the directory with an explicit owner, group, and mode. The 2 in 2775 is setgid: new files inherit the reporters group. The 775 grants write access to group members. The difference between setgid and group write is covered in Special Permissions.
3. Using umask to Guarantee Group Write
setgid inherits the group, but whether the new file is actually group-writable depends on umask. Both the group members and the service should operate with umask 0002. For human members this is often already the default; for the service, set it in the unit file:
Add the following to the editor:
How umask and setgid interact is covered in detail in umask.
4. Verification
sudo -u reportd -- sh -c 'umask; : > /srv/reports/probe.txt'
stat -c '%A %a %U:%G %n' /srv/reports/probe.txt
sudo rm -i -- /srv/reports/probe.txt
probe.txt is expected to be in the reporters group with mode 0664: setgid supplied the group, and umask 0002 left group write in place. This check only inspects metadata, not file content.
5. Rollback
In a lab, the most reliable rollback is returning to the VM snapshot. For a manual cleanup:
Run userdel -r first, then delete the group. Before removing /srv/reports, confirm with stat that it is only the lab directory.
Common Mistakes
Dropping -a from usermod -G
usermod -G <group> replaces the existing supplementary groups. When adding a group, write usermod -aG, or you risk removing the user from sudo or another important group.
Editing the passwd File by Hand
One mistake in a plain text editor can break login or sudo entirely. Use the useradd/usermod family, or the locking vipw/vigr tools.
Deleting an Account and Forgetting Its Files
userdel leaves files outside the home directory in place, and if the UID is reused, they can appear to belong to the new account. Audit with find -uid <UID> before deleting.
Mixing System and Human Accounts
Giving an application account an interactive shell and a password opens an unnecessary attack surface. Use -r and nologin for services; give human accounts an appropriate shell and password.
Confusing Primary and Supplementary Groups
A new file's group comes from the primary group (or a setgid directory), not from a supplementary group. Adding a supplementary group does not fix "files are created in the wrong group."
Expecting Group Membership Immediately
A new group does not apply to an already-open session. If id shows the new membership but the current shell does not, open a new login session — do not work around it with sudo or a wider permission.
Trusting a Numeric UID/GID Without Checking the Name
UID 1001 can be ali on one host and deploy on another. Verify the mapping with getent, as shown in Ownership, before creating accounts or groups, or before running chown.
Exercises
- List every field from
getent passwd "$(id -un)"andgetent group "$(id -gn)", and write down what each field means. - From
id,id -gn, andid -Gn, separate the primary group from the supplementary groups. Explain which one decides a new file's group. - On a lab VM, create one human account and one service account. Compare UID range, home directory, and shell using
getent passwd. - Add a user to a supplementary group with
usermod -aG, then remove them withgpasswd -d. Record theidandgetent groupoutput after each step. Do not test-Gwithout-aon a real account. - Combine a
setgiddirectory, thereportersgroup, andumask 0002to prove that a new file is created in thereportersgroup with mode0664. Separate out how each component — primary group,setgid,umask— contributed to the result.
Verification criterion: in every answer, show the UID/GID number rather than just the name, distinguish primary from supplementary group, and identify exactly where a file's group came from.
After finishing the lab, return to the VM snapshot, or clean up the created accounts and groups with the rollback steps above. Check the target before cleaning up:
References
- Linux man-pages:
passwd(5): https://man7.org/linux/man-pages/man5/passwd.5.html - Linux man-pages:
shadow(5): https://man7.org/linux/man-pages/man5/shadow.5.html - Linux man-pages:
group(5): https://man7.org/linux/man-pages/man5/group.5.html - Linux man-pages:
login.defs(5): https://man7.org/linux/man-pages/man5/login.defs.5.html - Linux man-pages:
useradd(8): https://man7.org/linux/man-pages/man8/useradd.8.html - Linux man-pages:
usermod(8): https://man7.org/linux/man-pages/man8/usermod.8.html - Linux man-pages:
userdel(8): https://man7.org/linux/man-pages/man8/userdel.8.html - Linux man-pages:
groupadd(8): https://man7.org/linux/man-pages/man8/groupadd.8.html - Linux man-pages:
gpasswd(1): https://man7.org/linux/man-pages/man1/gpasswd.1.html - Debian manual:
adduser(8): https://manpages.debian.org/stable/adduser/adduser.8.en.html - Linux man-pages:
chage(1): https://man7.org/linux/man-pages/man1/chage.1.html