Shadow Passwords Explained
Learn why /etc/shadow exists, how to read its nine fields, which hashing algorithms are current, and how to manage password aging with chage, pwconv, and pwck.
Before you start
- ▸Root or sudo access on the target system
- ▸Basic familiarity with reading Linux text files and running commands as root
- ▸The shadow-utils package installed (present by default on all major distros)
Early Unix stored hashed passwords directly in /etc/passwd, a world-readable file. Any local user could copy it and run an offline dictionary attack at leisure. The shadow password suite moves the sensitive data to /etc/shadow, readable only by root (and on many systems by the shadow group). Understanding the file's structure, the hashing algorithms it supports, and the aging controls it enforces is foundational security knowledge for any Linux administrator.
The Two Files and Why They Split
/etc/passwd must remain world-readable because many programs — ls, id, anything resolving UIDs to usernames — read it constantly. Putting a password hash there is therefore unsafe. /etc/shadow is the companion file that holds only what needs to stay private: the hash, plus password-expiry metadata. Permissions are strict by design:
ls -l /etc/passwd /etc/shadow
Typical output:
-rw-r--r-- 1 root root 1892 May 3 09:11 /etc/passwd
-rw-r----- 1 root shadow 950 May 3 09:11 /etc/shadow
Permissions will vary slightly by distro, but /etc/shadow is never world-readable.
Anatomy of an /etc/shadow Entry
Each line has nine colon-separated fields:
username:$hash_id$salt$hash:lastchange:min:max:warn:inactive:expire:
- username — must match a name in
/etc/passwd - hash field — the encoded password (details below)
- lastchange — days since epoch (1970-01-01) the password was last changed
- min — minimum days before the user can change the password again (0 = no minimum)
- max — maximum days the password is valid before a forced change is required
- warn — days before expiry the user sees a warning
- inactive — days after expiry the account is locked before being fully disabled
- expire — absolute expiry date as days since epoch; empty means never
- reserved — ninth field; unused, always empty
A real entry looks like this (hash truncated for readability):
alice:$y$j9T$abc123...:19845:0:90:14:7::
Hashing Schemes
The hash field encodes the algorithm via a prefix identifier defined in the crypt(3) Modular Crypt Format:
| Prefix | Algorithm | Status |
|---|---|---|
$1$ | MD5crypt | Obsolete — do not use |
$2b$ | bcrypt | Strong; default on some BSDs, available via libxcrypt |
$5$ | SHA-256crypt | Acceptable; widely deployed |
$6$ | SHA-512crypt | Good; current default on most Linux distros |
$y$ | yescrypt | Best current choice; default on Debian 11+, Ubuntu 22.04+, Fedora 35+ |
yescrypt is memory-hard (based on scrypt), making GPU-accelerated cracking significantly more expensive than SHA-512crypt. If your distro supports it, prefer it. The configured default lives in /etc/login.defs:
grep -i crypt /etc/login.defs
On modern systems you should see ENCRYPT_METHOD YESCRYPT or SHA512. On older or minimal installs you may see nothing (falls back to SHA-512).
Special Hash Field Values
!or!!— account is locked; the hash is prefixed with a bang so PAM rejects all password logins*— no password login permitted (service accounts, new users before a password is set)- empty field — no password required; dangerous and almost always a misconfiguration
Password Aging Controls
Use chage to read and set aging fields without editing the file by hand. Editing raw shadow entries manually invites mistakes and can lock out users.
Viewing Current Settings
sudo chage -l alice
Output shows human-readable dates for last change, expiry, inactive period, and warning days.
Setting a 90-Day Maximum with 14-Day Warning
sudo chage -M 90 -W 14 alice
Forcing a Password Change on Next Login
sudo chage -d 0 alice
Setting lastchange to 0 (epoch) triggers an immediate forced change. This is the standard post-provisioning step when handing a new account to a user.
Setting a Hard Account Expiry
sudo chage -E 2025-12-31 alice
System-Wide Defaults
New accounts inherit defaults from /etc/login.defs. Key fields:
grep -E '^(PASS_MAX_DAYS|PASS_MIN_DAYS|PASS_WARN_AGE)' /etc/login.defs
These only affect accounts created after you change them. Existing accounts are unaffected; use chage to update those individually or in a loop.
pwconv and pwunconv
On any system that predates or has been migrated without shadow passwords, pwconv converts the system to shadow format by moving hashes from /etc/passwd into /etc/shadow and replacing them with an x placeholder. pwunconv reverses this — useful only for legacy compatibility testing; never do it in production.
sudo pwconv
All modern distros install with shadow passwords enabled, so you will rarely need pwconv except after restoring a very old backup or converting a minimal embedded image.
Verifying Integrity with pwck
pwck cross-validates /etc/passwd and /etc/shadow, checking that every user in one file has a matching entry in the other, home directories exist, and shells are valid.
sudo pwck
Run it after any manual edits. It will report mismatches and optionally remove bad entries interactively. To check without making changes:
sudo pwck -r
The -r flag puts pwck in read-only mode — it reports problems but does not modify anything.
Verification
After any configuration change, confirm your work end-to-end:
# Confirm shadow file permissions are correct
stat -c '%a %U %G %n' /etc/shadow
# Confirm a user's aging settings look right
sudo chage -l alice
# Confirm the hash algorithm in use
sudo grep '^alice:' /etc/shadow | cut -d: -f2 | cut -c1-5
The hash prefix output should be $y$j9 (yescrypt), $6$ (SHA-512), or $2b$ (bcrypt) depending on your distro defaults. Seeing $1$ means MD5crypt is still in use — update ENCRYPT_METHOD in /etc/login.defs and force password resets so new hashes are generated.
Troubleshooting
User Cannot Log In After chage
Check whether the account is locked (! prefix in hash) or expired. sudo chage -l username will show a past expiry date. Reset with sudo chage -E -1 username to remove the expiry, or unlock with sudo passwd -u username.
pwck Reports Missing Shadow Entry
A user exists in /etc/passwd but not /etc/shadow. Run sudo pwconv to generate the missing shadow entry, or add the user's line manually — but prefer pwconv to avoid formatting errors.
Wrong Hash Algorithm After System Upgrade
Changing ENCRYPT_METHOD in /etc/login.defs does not re-hash existing passwords. The hash is only updated when the user next changes their password. To migrate everyone immediately, force resets with a loop:
for user in $(awk -F: '$2 ~ /^\$[16]\$/' /etc/shadow | cut -d: -f1); do
sudo chage -d 0 "$user"
done
This forces a password change at next login only for accounts still using older SHA-512 or MD5 hashes, leaving yescrypt hashes untouched.
Frequently asked questions
- What does an exclamation mark at the start of the hash field mean?
- A leading ! (or !!) means the account is locked. PAM rejects password-based logins for that account. SSH key authentication may still work unless separately restricted. Unlock with sudo passwd -u username.
- Does changing ENCRYPT_METHOD in /etc/login.defs immediately re-hash existing passwords?
- No. The hash is only rewritten when a user next changes their password. To migrate proactively, force resets with chage -d 0 for the affected accounts.
- Why does my new Debian/Ubuntu system show $y$ hashes while my older RHEL server shows $6$?
- Debian 11 and Ubuntu 22.04 switched the default to yescrypt ($y$), which is memory-hard and more resistant to GPU cracking. Older RHEL/CentOS 7-8 defaults are SHA-512crypt ($6$). RHEL 9 and Fedora 35+ also moved to yescrypt.
- Can I safely edit /etc/shadow by hand?
- Technically yes, but it is risky. A typo in the hash or a wrong field count can lock everyone out. Always use vipw -s (which locks the file during editing) or the appropriate tools like chage and passwd.
- What is the difference between an expired password and a locked account?
- An expired password forces the user to choose a new one at next login but the account is still accessible for that purpose. A locked account (! prefix or passwd -l) prevents all password-based logins entirely, including the password-change prompt.
Related guides
Manage Secrets with Ansible Vault
Encrypt Ansible secrets with AES-256 using ansible-vault: encrypt files and inline vars, automate with password files, and isolate group-level secrets with vault IDs.
AppArmor Explained
Learn how AppArmor profiles work, how to switch between enforce and complain mode, create new profiles, and diagnose access denials on Ubuntu, Debian, and Arch.
Apply CIS Benchmarks with OpenSCAP
Use OpenSCAP and scap-security-guide to evaluate, report on, and remediate Linux systems against CIS Benchmarks — covering install, eval, and automation.
How to Audit a Linux System with auditd
Set up auditd on Linux to track file access, syscalls, and privilege use. Covers persistent rules, file watches, ausearch, and aureport across major distros.