Linux for Defenders 10: How Weak a Password Will This Box Accept? PAM, pwquality and faillock
0 / 8 steps
The goal
By the end you will know exactly how weak a password your Linux machine accepts, and you will have changed it. You will read the PAM stack that judges every password, score real examples with pwscore, set a length-first policy that even root has to follow, find accounts with no password at all, add a five-strike lockout with pam_faillock and prove it works in all four cases, and force a password change after an incident.
Your own VM only, with a snapshot and a spare root shell. Steps 6 and 7 edit the files every login depends on. A typo can lock out every account, so take a VM snapshot first and keep a second terminal open as root (sudo -i) until step 7 passes. Only the throwaway users alex, sam and temp are used. No password cracking here; this lab is about the rules, not the hashes.
Why this lab. Weak and reused passwords are still how most break-ins start, and "we have a password policy" usually means a document, not a setting. On Linux the setting is a few lines of PAM. Knowing how to read and test them is a Linux+ objective, a Security+ control, and the answer to the auditor's first question.
Steven asked if typing "password" twice gets past a 14-character minimum. It does: sixteen characters, score 4 out of 100. Length is not everything, Steven. — Nyx
Before you start. You need the Ubuntu VM and the alex and sam test users from Lab 01 (create them with sudo useradd -m -s /bin/bash alex and the same for sam if you skipped it), and give sam a password with sudo passwd sam. The Red Hat way is at the end. Every output below is real, from the lab machine.
Steps
01Read the rules this machine uses for passwords
Every password change and every login goes through PAM, a stack of small modules read top to bottom. On Ubuntu the shared files are in /etc/pam.d/. Read the non-comment lines of two of them:
$ grep -vE '^\s*(#|$)' /etc/pam.d/common-password $ grep -vE '^\s*(#|$)' /etc/pam.d/common-auth
$ grep -vE '^\s*(#|$)' /etc/pam.d/common-password password requisite pam_pwquality.so retry=3 password [success=1 default=ignore] pam_unix.so obscure use_authtok try_first_pass yescrypt password requisite pam_deny.so password required pam_permit.so $ grep -vE '^\s*(#|$)' /etc/pam.d/common-auth auth [success=1 default=ignore] pam_unix.so nullok auth requisite pam_deny.so auth required pam_permit.so auth optional pam_cap.so
Two lines matter most. pam_pwquality.so is the gatekeeper that judges a new password before it is saved. pam_unix.so ... yescrypt stores it as a yescrypt hash (the $y$ you will see at the start of each entry in /etc/shadow). In the login stack, note the word nullok: it lets an account with an empty password log in. Keep that in mind for step 5.
Why this matters. Policy documents say "passwords must be strong". This file is what the machine actually enforces. If the two disagree, the file wins.
02Score some passwords against the default rules
pwscore (from the libpwquality-tools package) runs a password through exactly the same checks pam_pwquality uses, without changing anything. Try four:
$ sudo apt install libpwquality-tools $ echo 'password1' | pwscore $ echo 'Summer2026!' | pwscore $ echo 'Tr0ub4dor&3' | pwscore $ echo 'correct horse battery staple' | pwscore
password1 -> Password quality check failed:
The password fails the dictionary check - it is based on a dictionary word
Summer2026! -> 71
Tr0ub4dor&3 -> 84
correct horse battery staple -> 100
(Lined up side by side for reading.) Only the dictionary word is refused. Summer2026! (season, year, symbol: the first thing a password-guessing tool tries) passes with a score of 71, because the default minimum length is 8 and it has the "right" mix of characters.
Why this matters. Out of the box, the rules stop the very worst passwords and nothing more. Any password policy you think the machine has, test it with pwscore before you trust it.
03Set a policy that rewards length
Current NIST guidance favours long passwords and blocklists over symbol rules, so make length the rule. Put the settings in a drop-in file, which survives package upgrades:
$ sudo mkdir -p /etc/security/pwquality.conf.d $ sudo tee /etc/security/pwquality.conf.d/50-lab.conf <<'EOF' # Lab 10 policy: length over complexity minlen = 14 dictcheck = 1 usercheck = 1 maxrepeat = 3 enforce_for_root EOF
minlen = 14 is the length; dictcheck and usercheck refuse dictionary words and the user's own name; maxrepeat = 3 blocks aaaa; enforce_for_root makes the rules apply even when root sets someone's password. Score the same four again:
password1 -> Password quality check failed: The password is shorter than 14 characters Summer2026! -> Password quality check failed: The password is shorter than 14 characters Tr0ub4dor&3 -> Password quality check failed: The password is shorter than 14 characters correct horse battery staple -> 100
The four-word passphrase is the only one that gets through, and it is also the only one a person can remember. That is the point of the policy.
Why this matters. Each extra character multiplies the work for anyone guessing. A 14-character minimum quietly kills the whole Season+Year+! family, which is still one of the most common patterns in leaked password lists.
04Watch PAM enforce it on a real password change
pwscore is a preview. Now let a real user try. Your test user sam from Lab 01 changes their own password:
$ su - sam -c passwd
Changing password for sam. Current password: New password: (typed Summer2026!) BAD PASSWORD: The password is shorter than 14 characters New password: (typed copper kettle winter lamp) Retype new password: passwd: password updated successfully
And because of enforce_for_root, root is held to it too. Setting alex a password that contains their own name fails, even through chpasswd:
$ echo 'alex:Alex-Lab-Pass-2026' | sudo chpasswd
BAD PASSWORD: The password contains the user name in some form
chpasswd: (line 1, user alex) password not changed
Why this matters. Without enforce_for_root, an admin resetting a password at 5 pm on a Friday sets Welcome1 and nothing stops them. Help-desk resets are where weak passwords really come from.
05Find accounts with no password at all
An empty password field means no password is asked. With nullok in the login stack, that account can log in with an empty password. Make one on purpose, then find it the way an auditor would:
$ sudo useradd -m -s /bin/bash temp $ sudo passwd -d temp $ sudo awk -F: '($2==""){print $1" has NO password"}' /etc/shadow temp has NO password $ sudo passwd -S temp temp NP 2026-10-21 0 99999 7 -1
NP means no password. Lock it, and check again (L means locked):
$ sudo passwd -l temp $ sudo passwd -S temp temp L 2026-10-21 0 99999 7 -1 $ sudo awk -F: '($2==""){print $1}' /etc/shadow | wc -l 0
In the next step you also take nullok out of the login stack, so a blank password can never work again, even if one appears.
Why this matters. Empty passwords turn up on old images, test accounts and machines built by scripts. The awk line takes a second, and it belongs in every audit.
06Lock out password guessing with pam_faillock
Ubuntu does not lock accounts after failed logins by default. pam_faillock adds that. Before you edit anything here, open a second terminal and run sudo -i in it. Keep it open until step 7 passes: a mistake in these files can stop every login, including yours. Back up, then write the new login stack:
$ sudo cp /etc/pam.d/common-auth /root/common-auth.bak $ sudo cp /etc/pam.d/common-account /root/common-account.bak $ sudo tee /etc/pam.d/common-auth <<'EOF' auth required pam_faillock.so preauth auth [success=2 default=ignore] pam_unix.so auth [default=die] pam_faillock.so authfail auth requisite pam_deny.so auth required pam_faillock.so authsucc auth required pam_permit.so auth optional pam_cap.so EOF
Read it as a little program. preauth refuses a locked account before the password is even checked. If pam_unix accepts the password, success=2 jumps over the next two lines to authsucc, which clears the failure count. If it rejects it, authfail records the failure. nullok is gone. Now add faillock to the account stack and set the limits:
$ sudo sed -i '0,/^account/s//account\trequired\t\t\tpam_faillock.so\naccount/' /etc/pam.d/common-account $ printf 'deny = 5\nunlock_time = 900\n' | sudo tee -a /etc/security/faillock.conf $ grep -vE '^\s*(#|$)' /etc/security/faillock.conf deny = 5 unlock_time = 900
Get the jump count right. When this lab was built, the first draft used success=1 (copied from the original file). That skips only the authfail line and lands on pam_deny, so every correct password was refused. It looked like a lockout working perfectly until the reset in step 7 still failed. The count must skip both lines: success=2. Always test a right password, not only wrong ones.
Why this matters. Five tries and a 15-minute wait make online guessing so slow it stops being worth doing, while a real user who fat-fingers twice is never bothered.
07Prove it: four cases, all four must pass
From alex's session (or any non-root user), try to switch to sam. Test the right password, five wrong ones, the right one while locked, and the right one after a reset:
$ su - sam (right password) sam@labvm:~$ exit $ su - sam (wrong, five times) su: Permission denied su: Permission denied su: Permission denied su: Permission denied su: Permission denied $ su - sam (right password, while locked) su: Authentication failure
Locked: even the right password is refused. Root can see why, and lift it:
$ sudo faillock --user sam sam: When Type Source Valid 2026-10-21 15:37:12 TTY /dev/pts/0 V ... (five lines, one per failure) $ sudo journalctl | grep faillock | tail -1 Oct 21 15:36:31 labvm su[3108]: pam_faillock(su-l:auth): Consecutive login failures for user sam account temporarily locked $ sudo faillock --user sam --reset $ su - sam (right password) sam@labvm:~$
One more check, so normal users are not punished for typos: two wrong attempts, then the right one, leaves zero failures recorded (authsucc cleared them), and sudo still works for alex. Only now close that spare root terminal.
Why this matters. A lockout you have not tested is either not working or locking out everyone. The journal line is also exactly what a SOC alert should key on: many Consecutive login failures across many accounts in a few minutes is password spraying.
08Force a change after an incident, not every 90 days
Current NIST guidance says do not force regular password changes; force one when there is a reason, like a phishing click or a leaked password. chage does it per user:
$ sudo chage -d 0 sam $ sudo chage -l sam | head -2 Last password change : password must be changed Password expires : password must be changed $ su - sam Password: You are required to change your password immediately (administrator enforced). Changing password for sam.
The change goes through the same pam_pwquality rules from step 3, so the new password has to be good too. For the record, /etc/login.defs on this machine still says PASS_MAX_DAYS 99999 (no expiry) and ENCRYPT_METHOD SHA512, but new hashes are yescrypt, because the pam_unix line in step 1 decides that, not login.defs.
Why this matters. Forced 90-day changes push people to Summer2026! then Autumn2026!. Forcing one change right after an incident is the version that actually stops an attacker using a stolen password.
What you learned
- /etc/pam.d/common-password and common-auth — the rules the machine really enforces; watch for nullok
- pwscore — test a password against the live policy without changing anything
- /etc/security/pwquality.conf.d/ — minlen = 14, dictcheck, usercheck, enforce_for_root: length first
- awk -F: '($2=="")' /etc/shadow and passwd -S — find and lock accounts with no password
- pam_faillock — preauth / authfail / authsucc, success=2, deny = 5; test the right password, not only wrong ones
- faillock --user name and --reset — see and lift a lockout; the journal line is your spraying alert
- chage -d 0 name — force a change after an incident instead of every 90 days
On Red Hat (RHEL, Rocky, Fedora) and Debian. Tested on Rocky Linux 9.8: you do not hand-edit PAM on Red Hat. authselect writes the stack for you: sudo authselect select minimal with-faillock (or sssd with-faillock on a machine joined to a directory) adds the pam_faillock.so preauth, authfail and account lines to /etc/pam.d/system-auth and password-auth, already in the right order, and authselect check confirms the files have not been changed by hand. pam_pwquality.so is in the stack by default; the policy file and pwquality.conf.d/ drop-ins, pwscore, faillock and chage all work exactly as above, and /etc/security/faillock.conf holds deny and unlock_time. Debian 12 matches Ubuntu, with libpam-pwquality installed.
Put it on your resume. Hardened Linux authentication with PAM: set a length-based password policy enforced for root, removed empty-password access, implemented account lockout with pam_faillock and verified it end to end, and used chage for incident-driven password resets. Change it to match what you actually did.
Tried it? Tell us
Nova built this lab from Steven’s ideas, and every command above was run on a real machine before it went up. If a step behaved differently for you, or you know a better way, tell us. Ideas for the next lab are welcome too — no promises on timing.
NextLinux for Defenders 11 — Run containers without handing out root→
Go deeper
- What makes a strong password — why the passphrase in step 3 wins, in plain language
- Linux users, passwords and permissions — /etc/passwd, /etc/shadow and the hash fields
- Linux for Defenders 05 — SSH — keys only for remote logins, so passwords matter only at the console
Original walkthrough. Every command and output above was run on Ubuntu 24.04 (libpam-pwquality 1.4.5, PAM from Ubuntu's 24.04 packages) and the Red Hat differences on Rocky Linux 9.8 (authselect, minimal profile). If something does not match your machine, tell me and I’ll fix it.