How to Use Shadow Passwords in Linux: Complete Password Security and Management Guide

how to use shadow password in linux

how to use shadow password in linux

Every Linux system I’ve worked on has used the shadow password system since before I ever touched Linux professionally — it’s been the default for so long that it’s easy to forget it was ever optional, or that there was an era where password hashes lived in a file every single user on the system could read. Understanding exactly how shadow passwords work, and the tools built around managing them, matters far more than most administrators realize until the day they need to actually troubleshoot a locked account or audit password policy compliance.

The Problem Shadow Passwords Solve

Historically, Unix stored password hashes directly in /etc/passwd — a file that, by design, needs to be world-readable, since dozens of system utilities need to look up usernames, UIDs, and shell information for every user on the system. That meant password hashes, even though they were hashed rather than stored in plaintext, were sitting in a file anyone on the system could read and run offline cracking attempts against.

The shadow password system splits this: /etc/passwd keeps the world-readable account metadata, while the actual password hashes move into /etc/shadow, a file readable only by root (and, on some configurations, a restricted shadow group used by specific privileged utilities).

The /etc/passwd File Format

cat /etc/passwd

Example line:

alice:x:1001:1001:Alice Smith:/home/alice:/bin/bash

Fields, colon-separated:

  1. Username
  2. Password field — always x on a shadow-enabled system, signaling “look in /etc/shadow instead”
  3. UID
  4. GID
  5. GECOS field (full name/comment)
  6. Home directory
  7. Login shell

If you ever see an actual hash (or nothing at all) in the second field instead of x, that’s a strong signal shadow passwords aren’t properly enabled for that account, or a tool bypassed the shadow mechanism when creating it.

The /etc/shadow File Format

sudo cat /etc/shadow

Example line:

alice:$6$randomsalt$longhashstring:19945:0:90:7:14::

Fields, colon-separated:

  1. Username
  2. Encrypted password hash (or special values — see below)
  3. Days since Jan 1, 1970 that the password was last changed
  4. Minimum days before the password can be changed again
  5. Maximum days before the password must be changed
  6. Warning period (days before expiration to start warning the user)
  7. Inactivity period (days after expiration before the account is disabled entirely)
  8. Account expiration date (days since epoch)
  9. Reserved field (unused)

Understanding the Hash Field

The hash field format is $id$salt$hash, where $id$ identifies the hashing algorithm:

IdentifierAlgorithm
$1$MD5 (obsolete, insecure — should not appear on any modern system)
$2a$/$2b$/$2y$Blowfish (bcrypt)
$5$SHA-256
$6$SHA-512 (the modern default on virtually every current distro)
$y$yescrypt (newer default on some recent distros, e.g. current Fedora/Debian releases)

Special (non-hash) values in this field:

Check your system’s configured default algorithm:

cat /etc/login.defs | grep -i crypt
authselect current 2>/dev/null   # on systems using authselect, may show the active hashing profile

Permissions on These Files (and Why They Matter)

ls -l /etc/passwd /etc/shadow

Expected output:

-rw-r--r-- 1 root root  ... /etc/passwd
-rw-r----- 1 root shadow ... /etc/shadow

/etc/passwd stays world-readable by necessity. /etc/shadow should be readable only by root (and the shadow group, used by specific utilities like passwd itself, which run setuid/setgid to perform the privileged read). If you ever find /etc/shadow with broader permissions than this, that’s a serious finding worth fixing immediately:

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

Managing Shadow Passwords with Standard Tools

passwd — Change a Password

passwd                  # change your own password
sudo passwd alice       # change another user's password (root only)

chage — Manage Password Aging Directly

sudo chage -l alice

Example output:

Last password change                    : Jan 15, 2026
Password expires                        : Apr 15, 2026
Password inactive                       : Apr 22, 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

Set a maximum password age of 90 days:

sudo chage -M 90 alice

Force a password change on next login:

sudo chage -d 0 alice

Set an account expiration date:

sudo chage -E 2026-12-31 alice

Interactive mode, prompting for each field:

sudo chage alice

usermod — Lock/Unlock Accounts

sudo usermod -L alice     # lock (prepends "!" to the hash in /etc/shadow)
sudo usermod -U alice     # unlock (removes the "!")

Verify the effect directly:

sudo grep alice /etc/shadow

A locked account shows a hash beginning with ! — the original hash is still preserved after the !, which is exactly why this is reversible (as opposed to actually deleting the password).

passwd -l / -u (Equivalent Shortcuts)

sudo passwd -l alice     # lock
sudo passwd -u alice     # unlock
sudo passwd -S alice     # show status

passwd -S output format:

alice L 01/15/2026 0 90 7 -1

Fields: username, status (L=locked, P=usable password, NP=no password), last change date, min/max/warning days, inactivity period.

System-Wide Password Policy Defaults

/etc/login.defs sets defaults applied when new accounts are created (via useradd):

PASS_MAX_DAYS   90
PASS_MIN_DAYS   7
PASS_WARN_AGE   14

These become the initial /etc/shadow aging values for any newly created account — existing accounts aren’t retroactively affected unless you update them explicitly with chage.

Auditing Password Security Across All Accounts

Find accounts with no password set at all (a serious finding):

sudo awk -F: '($2 == "" ) { print $1 }' /etc/shadow

Find accounts using a locked/disabled password field:

sudo awk -F: '($2 ~ /^!/ || $2 == "*") { print $1 }' /etc/shadow

Find accounts with password aging effectively disabled (max days set to -1 or a very high number):

sudo chage -l alice | grep "Maximum"

Find accounts whose passwords have never been changed since account creation (last-changed field of 0):

sudo awk -F: '($3 == 0) { print $1 }' /etc/shadow

Converting Between passwd and shadow Formats

If you ever inherit a system genuinely still using unshadowed passwords (extremely rare today, but worth knowing), pwconv and pwunconv handle the conversion:

sudo pwconv      # migrate plaintext-in-passwd hashes into /etc/shadow, replacing them with 'x'
sudo pwunconv    # reverse the process — merge shadow data back into /etc/passwd (essentially never appropriate on a real system)

There’s a matching pair for group passwords too:

sudo grpconv
sudo grpunconv

Verifying System Integrity

sudo pwck        # checks /etc/passwd and /etc/shadow for consistency issues
sudo grpck        # same check for /etc/group and /etc/gshadow

These flag issues like duplicate UIDs, users referenced in /etc/passwd missing from /etc/shadow, or malformed entries — genuinely useful as a periodic integrity check, especially after any bulk user-management scripting.

How the Hashing Actually Works Internally

It’s worth understanding what actually happens when passwd sets a new hash, since the structure of the hash field itself ($id$salt$hash) directly explains a couple of properties people often assume incorrectly.

When you set a password, the system generates a random salt (a short random string, unique per password), then runs the chosen algorithm (SHA-512, yescrypt, etc.) against the combination of your password and that salt, typically with a configurable number of rounds/iterations to deliberately slow down the computation. The salt is stored alongside the resulting hash in plain view within /etc/shadow — it doesn’t need to be secret, since its purpose isn’t to hide anything but to guarantee that even two users with the identical password end up with completely different stored hashes.

alice:$6$xK2mP9vQ$hashvaluehere...:19945:0:90:7:14::
bob:$6$aB7nR4wZ$differenthashvalue...:19945:0:90:7:14::

Even if alice and bob happen to share the exact same password, their salts differ, so their stored hashes look completely unrelated — this is precisely what defeats precomputed “rainbow table” attacks, which rely on attacking many accounts at once using a single precomputed table; a unique salt per account forces an attacker to redo the expensive computation separately for every single account rather than reusing prior work.

The round count (built into modern algorithms like yescrypt, or configurable via $6$rounds=N$ for SHA-512) directly trades off computation cost against attack resistance — deliberately slow hashing means a brute-force attacker checking password guesses against a stolen hash can only test a relatively small number of guesses per second, compared to the billions per second achievable against a fast, unsalted hash.

# Check what rounds parameter (if explicitly configured) your system uses
grep -i rounds /etc/login.defs

yescrypt: The Newer Default on Many Current Distros

Several current-generation distros (recent Debian, Fedora, and derivatives) have shifted their default hashing algorithm from SHA-512 to yescrypt, identified by the $y$ prefix. yescrypt is deliberately designed to be both CPU-time and memory-hard, meaning it resists a specific class of attack that plain CPU-time-hard algorithms like SHA-512 remain vulnerable to: attackers using specialized hardware (GPUs, ASICs) that can parallelize enormous numbers of simultaneous hash attempts cheaply, since memory-hardness makes that parallelization dramatically more expensive to scale.

grep ENCRYPT_METHOD /etc/login.defs

If you’re managing a system that predates this shift, or one that was upgraded rather than freshly installed, existing password hashes typically remain in whatever format they were originally created with — the algorithm identifier is per-password, not a single global system state, so a single /etc/shadow file can genuinely contain a mix of $6$ and $y$ entries side by side, reflecting whenever each individual password was last actually changed relative to the system’s default algorithm at that time.

# See the actual algorithm mix currently in use across all accounts
sudo awk -F: '{print $2}' /etc/shadow | grep -oE '^\$[0-9y]+\$' | sort | uniq -c

The Full chage Interactive Walkthrough

Beyond the flag-based invocations shown earlier, chage‘s interactive mode is worth knowing for occasional one-off adjustments where remembering the specific flag isn’t worth the effort:

sudo chage alice
Changing the aging information for alice
Enter the new value, or press ENTER for the default

        Minimum Password Age [0]:
        Maximum Password Age [90]: 60
        Last Password Change (YYYY-MM-DD) [2026-01-15]:
        Password Expiration Warning [7]:
        Password Inactive [14]:
        Account Expiration Date (YYYY-MM-DD) [never]:

Pressing Enter on any field keeps its current value, while typing a new value updates just that field — genuinely convenient for a quick adjustment (like extending the max age here from 90 to 60… though notably that’s a tightening, not an extension, worth double-checking the direction of any change you make this way) without needing to look up the corresponding single-letter flag.

Correlating Shadow Aging With PAM’s Own Lockout Mechanism

A distinction worth being explicit about, since these are genuinely two separate systems that can each independently prevent a login, with different remediation paths: shadow’s own account/password expiration (chage-managed) is evaluated by the pam_unix.so account phase, while brute-force lockout (pam_faillock) is a completely separate mechanism tracking recent failed attempts, unrelated to password age at all.

# Check both independently when a user reports being unable to log in
sudo chage -l someuser         # shadow-based aging/expiration status
sudo faillock --user someuser  # PAM-based failed-attempt lockout status

A user can be simultaneously “not locked” by one mechanism and “locked” by the other — for instance, a perfectly valid, non-expired password that’s nonetheless temporarily blocked because pam_faillock recorded too many recent failed attempts (possibly from an unrelated automated scan hitting SSH), which requires faillock --reset specifically, not any shadow/chage adjustment, to resolve.

Auditing Shadow File Integrity After Bulk User Operations

Beyond the basic pwck/grpck checks mentioned earlier, it’s worth running a slightly more thorough set of checks after any bulk account provisioning or migration work, since subtle inconsistencies here can produce hard-to-diagnose login failures much later:

# Confirm every user in /etc/passwd has a corresponding /etc/shadow entry and vice versa
comm -3 <(cut -d: -f1 /etc/passwd | sort) <(cut -d: -f1 /etc/shadow | sort)

# Check for duplicate UIDs, a common artifact of scripted account creation gone wrong
awk -F: '{print $3}' /etc/passwd | sort | uniq -d

# Check for accounts with a shell that shouldn't allow interactive login but still have a valid password set
sudo awk -F: '$7 ~ /nologin|false/ {print $1}' /etc/passwd | while read u; do
    sudo grep "^$u:" /etc/shadow | grep -v '!' | grep -v '\*'
done

That last check is worth explaining: service/system accounts are typically given /usr/sbin/nologin or /bin/false as their shell specifically to prevent interactive login, but this is a separate control from the password hash itself — an account with nologin as its shell that nonetheless has a genuine, usable password hash (rather than the expected !/* locked marker) represents a real, if narrow, gap: certain non-shell authentication paths (some PAM-based services, certain application-level auth checks) don’t necessarily consult the shell field at all, meaning the “nologin shell” protection alone isn’t a substitute for an actually-locked password field on accounts that should never authenticate interactively.

Troubleshooting

User can’t log in, “account expired” message — check and, if appropriate, extend expiration:

sudo chage -l username
sudo chage -E -1 username   # remove expiration entirely

User locked out unexpectedly — check for a leading ! in the shadow hash and whether pam_faillock (see the PAM article) has independently locked the account, since these are two separate lockout mechanisms:

sudo grep username /etc/shadow
sudo faillock --user username

Password changes not respecting policy — confirm the relevant PAM module (pam_pwquality/pam_cracklib) is actually configured; chage//etc/login.defs control aging, not complexity — those are enforced separately through PAM.

“Authentication token manipulation error” when changing a password — often a permissions or disk-space issue on /etc/shadow; check both:

ls -l /etc/shadow
df -h /etc

Security Best Practices

Summary

The shadow password system separates world-readable account metadata (/etc/passwd) from root-only password hashes and aging data (/etc/shadow), closing off the offline-cracking exposure that plaintext-hash-in-passwd systems had. Day-to-day management runs through passwd, chage, and usermod, with system-wide defaults set in /etc/login.defs — and the periodic audit habit of checking for empty passwords, disabled expiration, and unchanged default passwords is one of the simpler, higher-value account security checks available on any Linux system.

References

Exit mobile version