/etc/passwd and /etc/shadow Explained

Every user account on a Linux system is described by a line in /etc/passwd, and every password’s encrypted hash lives in a separate file, /etc/shadow. Splitting account metadata from password hashes is a deliberate security design: /etc/passwd must be world-readable so ordinary tools can look up usernames, but password hashes must not be, so they live in a file only root (and the shadow group) can read. Understanding these two files is the foundation for everything else in user and permission management on Linux.

Overview / How it works

When Linux was young, both the username and the encrypted password lived together in /etc/passwd. That file has to be world-readable because dozens of programs — ls -l, ps, id, login shells, file managers — need to translate numeric user IDs (UIDs) into human-readable names and back. But a world-readable password hash is a gift to attackers: with a copy of the hash, an offline brute-force or dictionary attack becomes possible with no rate limiting at all. The shadow password system, introduced in the 1980s and standard on every modern distribution, fixes this by moving the hash (and password-aging data) into /etc/shadow, a file readable only by root and processes running as the shadow group. /etc/passwd keeps a placeholder (x) in the password field instead of the real hash.

These files are read by the C library’s Name Service Switch (NSS), which is what actually resolves usernames to UIDs during login, su, sudo, and any syscall that needs to map an owner ID to a name. On most systems NSS is configured (in /etc/nsswitch.conf) to check local files first and can also consult LDAP, SSSD, or other identity backends — which is why the getent command, not a raw grep of /etc/passwd, is the correct way to look up account info when those backends might be in play.

When you log in, PAM (Pluggable Authentication Modules) reads the shadow entry for your username, extracts the algorithm identifier and salt from the stored hash, re-hashes the password you typed using that same algorithm and salt, and compares the result byte-for-byte with the stored hash. The plaintext password itself is never stored anywhere, and it never needs to be decrypted — hashing is a one-way function, so verification only requires computing the same hash again.

Modern shadow files use strong, salted hashing algorithms, most commonly SHA-512 (marked by a $6$ prefix) or, on newer systems, yescrypt ($y$). The salt is a random string mixed into the hash so that two users with the identical password end up with completely different hash strings, defeating precomputed rainbow-table attacks.

Syntax

Both files use the same simple format: one line per account, fields separated by colons. Nothing enforces this format except convention and the tools that parse it — that is exactly why hand-editing these files is risky (see Common Mistakes below).

/etc/passwd — 7 colon-separated fields:

username:password:UID:GID:GECOS:home_directory:shell
Field Meaning
username Login name, up to 32 characters on most systems
password Always x on a shadow-enabled system — the real hash is in /etc/shadow
UID Numeric user ID; 0 is always root, 1–999 are typically system/service accounts, regular users usually start at 1000
GID Numeric ID of the user’s primary group (defined in /etc/group)
GECOS Comma-separated comment field — conventionally full name, room number, work phone, home phone; often just the full name or left blank
home_directory Absolute path to the user’s home directory, e.g. /home/maria
shell Program run at login, e.g. /bin/bash; set to /usr/sbin/nologin or /bin/false to block interactive login

/etc/shadow — 9 colon-separated fields:

username:hashed_password:last_change:min:max:warn:inactive:expire:reserved
Field Meaning
username Must match the username in /etc/passwd
hashed_password The salted hash, e.g. $6$salt$hash; ! or * means the account is locked/has no password login; empty means no password required (dangerous)
last_change Days since Jan 1, 1970 (the Unix epoch) that the password was last changed
min Minimum days that must pass before the password can be changed again
max Maximum days the password is valid before it must be changed
warn Days before expiry that the user starts seeing a warning
inactive Days after expiry before the account is disabled entirely
expire Absolute date (days since epoch) the account itself expires, regardless of password changes
reserved Unused, reserved for future use

Examples

Example 1: Reading /etc/passwd

cat /etc/passwd | tail -n 4

Output:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin
maria:x:1001:1001:Maria Lopez,,,:/home/maria:/bin/bash

/etc/passwd is world-readable, so any user can run this. Notice root has UID 0, daemon and syslog are system accounts with nologin shells (they run services, not interactive sessions), and maria is a normal human user with UID 1001 and a real login shell.

Example 2: Reading /etc/shadow (requires root)

sudo cat /etc/shadow | grep maria

Output:

maria:$6$kR3fQzS1$9pLxN2vT8...redacted...hash:19570:0:90:7:14::

This line means: maria’s password is hashed with SHA-512 ($6$), it was last changed 19,570 days after the epoch, there’s no minimum age (0), it must be changed every 90 days (max), she’ll be warned 7 days before that, and the account is disabled 14 days after expiry if she never changes it. Without sudo, this command fails with Permission denied for a non-root, non-shadow-group user — that’s the file permissions doing their job.

Example 3: Looking up an account with getent

getent passwd maria

Output:

maria:x:1001:1001:Maria Lopez,,,:/home/maria:/bin/bash

getent asks NSS to resolve the name, so it works identically whether the account is defined locally in /etc/passwd or comes from LDAP/SSSD — unlike grep maria /etc/passwd, which only ever sees local file entries.

Example 4: Inspecting password aging with chage

sudo chage -l maria

Output:

Last password change                                   : Jun 03, 2026
Password expires                                       : Sep 01, 2026
Password inactive                                      : Sep 15, 2026
Account expires                                        : never
Minimum number of days between password change         : 0
Maximum number of days between password change         : 90
Number of days of warning before password expires      : 7

chage -l reads the same epoch-day numbers from /etc/shadow and translates them into human-readable dates — far easier than doing the arithmetic yourself.

How it works step by step

  1. You type your username and password at a login prompt (console, SSH, or a display manager).
  2. PAM’s pam_unix module looks up your username in /etc/passwd to confirm the account exists and to find your UID, home directory, and shell.
  3. PAM reads the matching line in /etc/shadow (requires root privilege, which the login process already has) and extracts the algorithm ID and salt from the stored hash string.
  4. PAM hashes the password you just typed using that exact algorithm and salt.
  5. PAM compares the freshly computed hash to the stored one. If they match, authentication succeeds; the plaintext password is discarded from memory immediately after.
  6. PAM also checks the aging fields: if max has been exceeded, you may be forced to change your password before continuing; if the account’s expire date has passed, login is refused outright.
  7. On success, the kernel starts your session as the UID/GID from /etc/passwd, sets $HOME to your home directory field, and execs your configured shell.

Common Mistakes

Mistake 1: Editing /etc/passwd or /etc/shadow directly with a plain text editor

Wrong:

sudo nano /etc/passwd

Opening these files in a regular editor is risky for two reasons: there’s no file locking, so if useradd or passwd writes to the file at the same moment you save, one set of changes can be lost or the file can end up corrupted; and there’s no syntax validation, so a typo (wrong number of colons, duplicate UID) can silently break login for that account or every account. Use the dedicated tools instead — they lock the file, validate syntax, and update both files consistently:

sudo useradd -m -s /bin/bash maria
sudo usermod -aG sudo maria
sudo passwd maria
# Only if you truly must hand-edit, use vipw / vipw -s, which lock
# the files and check syntax before saving:
sudo vipw
sudo vipw -s

Mistake 2: Loosening permissions on /etc/shadow

Wrong:

sudo chmod 644 /etc/shadow

This is a serious security regression: it makes every password hash on the system readable by any local user, turning what should be an impossible-without-root attack into a trivial offline brute-force or dictionary attack against every account. The default permissions (owner root, group shadow, mode 640) exist precisely to prevent this. Restore them:

sudo chmod 640 /etc/shadow
sudo chown root:shadow /etc/shadow

Mistake 3: Assigning a duplicate UID

Wrong (hand-edited line reusing an existing UID):

# /etc/passwd already has: maria:x:1001:1001:Maria Lopez,,,:/home/maria:/bin/bash
# Adding this second line gives 'carlos' the SAME UID:
carlos:x:1001:1001:Carlos Diaz,,,:/home/carlos:/bin/bash

The kernel identifies file ownership purely by numeric UID, not by name. Two usernames sharing a UID are treated as the exact same identity for every permission check — carlos would own maria’s files and vice versa, and tools like ls -l would show whichever name happens to be looked up first. Always let useradd assign the next free UID automatically, or explicitly pick an unused one with useradd -u after checking cut -d: -f3 /etc/passwd | sort -n for collisions.

Best Practices

  • Never hand-edit /etc/passwd or /etc/shadow; use useradd, usermod, userdel, passwd, and chage instead.
  • If you genuinely must edit by hand, use vipw and vipw -s, which lock the files and validate the format on save.
  • Keep /etc/shadow at permissions 640 owned by root:shadow, and /etc/passwd at the default 644 — don’t loosen either.
  • Use getent passwd <name> instead of grep <name> /etc/passwd when your system might use LDAP, SSSD, or another NSS backend, since grep only sees local entries.
  • Periodically check for duplicate UIDs with cut -d: -f3 /etc/passwd | sort | uniq -d; only root should ever have UID 0.
  • Set sensible password aging with chage for accounts that need it, rather than leaving max unset forever.
  • Give service/daemon accounts a nologin or false shell so they can own processes but cannot be used for interactive login.
  • Lock an account you’re not ready to delete with sudo passwd -l <user> (prefixes the hash with !) instead of removing it outright.

Practice Exercises

  • Run getent passwd "$(whoami)" and identify each of the 7 fields in your own entry: your UID, GID, home directory, and shell.
  • Run sudo chage -l "$(whoami)" and interpret the output — when does your password expire, and how many days of warning will you get?
  • Create a new system account for a hypothetical backup service with no login shell (hint: useradd -r -s /usr/sbin/nologin backupsvc), then confirm its entries in both /etc/passwd and /etc/shadow and verify /etc/shadow is still mode 640.

Summary

  • /etc/passwd stores account metadata (username, UID, GID, GECOS, home, shell) and is world-readable by design.
  • /etc/shadow stores the salted password hash and aging policy, and is readable only by root and the shadow group.
  • The x in the password field of /etc/passwd is a placeholder pointing to /etc/shadow, not the hash itself.
  • PAM authenticates by re-hashing the entered password with the stored algorithm and salt and comparing hashes — the plaintext is never stored.
  • Always manage accounts with useradd/usermod/userdel/passwd/chage, or vipw/vipw -s if hand-editing is unavoidable.
  • Never loosen /etc/shadow permissions, and never allow duplicate UIDs between accounts.