PAM policy: password quality and lockout
pwquality and faillock, tested safely.
PAM (Pluggable Authentication Modules) is the library that console logins, su, sudo and SSH password logins ask whether to let someone in. In this lesson you read a PAM stack, add password-quality rules that follow current NIST guidance, lock an account after repeated failures, and apply both the way each distribution expects: pam-auth-update on Ubuntu, authselect on RHEL. Because one wrong line can lock every user out, including you, you also test each change on a separate service before any real login depends on it.
How a PAM stack decides
Each program that authenticates has a file in /etc/pam.d/, and on Ubuntu most of them include the same shared files. This is an untouched install:
sshd and sudo both include common-auth and common-account, so a change there applies to every login path at once, the console included.
Each line has a type, a control and a module. The type says which stage it belongs to: auth proves identity, account decides whether this account may log in now (expiry, lockout), password sets a new secret, and session sets up and tears down the login.
The control says what the module's answer means. requisite fails the whole stack at once. required fails it but keeps running the rest, so the caller cannot tell which module refused. optional rarely matters.
The bracket form maps results to actions. [success=1 default=ignore] on pam_unix means "if the password is right, skip one line", which jumps over pam_deny to pam_permit; any other result falls through to pam_deny. nullok lets an account with an empty password field in; the accounts lesson showed there are none, and RHEL's without-nullok feature removes the option.
On Ubuntu you do not edit these lines by hand. pam-auth-update builds the common-* files from profiles in /usr/share/pam-configs, one per module package, and a hand edit makes it treat the files as locally modified and stop updating them (pam-auth-update(8)).
Four profiles ship by default, and neither the password-quality module nor a lockout profile is among them.
Test before you trust
The safe way to try a PAM stack is a separate service file that includes the shared stacks, exercised with pamtester (sudo apt install pamtester, from Ubuntu's universe repository) against a throwaway user. Real logins never read the test file, and pamtester prints the result of each PAM call directly. The test file isolates only the test, though: the moment you change a shared file, directly or with pam-auth-update, every login uses it. In the lab those changes landed in private copies of the PAM files in a separate mount namespace; on your machine they are live. The user:
And the service file:
# SecOpsLog lab: a separate PAM service for testing the shared stacks# with pamtester. sshd, sudo and login never read this file.@include common-auth@include common-account@include common-password
pamtester reads the password from standard input, so the right one authenticates and a wrong one returns "Authentication failure" with exit status 1. That is the baseline to compare every change against.
Password quality with pwquality
The threat is a password an attacker guesses from a list: short, common, or built from the user name. Ubuntu does not install the checking module by default (above). sudo apt install libpam-pwquality adds its profile, and pam-auth-update inserts the line in front of pam_unix (yescrypt on that line is the hash algorithm for new passwords); the lab machine already has it.
The profile's Priority decides the order: higher numbers come first, and unix has 256. The rules themselves live in /etc/security/pwquality.conf or, better, a drop-in in /etc/security/pwquality.conf.d/. NIST SP 800-63B-4 asks for at least 15 characters for a password used on its own, a check against commonly used and compromised passwords, and no rules about character classes. The dictionary and user-name checks are on by default, so the drop-in needs two settings. As everywhere in this course, comments go on their own lines.
# SecOpsLog: length plus the dictionary and user-name checks (on by# default), and no character-class rules (NIST SP 800-63B-4).minlen = 15# Apply the checks when root sets someone else's password too.enforce_for_root
chauthtok is the password-change call. The three bad candidates fail the length check, cracklib's dictionary check (which calls password12345678 too simplistic) and the user-name check; after retry=3 attempts the change is refused. Without enforce_for_root, root would only see the warnings and the change would go through. A long passphrase passes. The dictionary is cracklib's word list, not a list of breached passwords; pwquality.conf(5) documents badwords for extra words and dictpath for a dictionary you build yourself.
Whose advice is this? The CIS RHEL 10 Level 1 profile in scap-security-guide 0.1.82 sets minlen 14 and minclass 4 (all four character types), a composition rule that NIST SP 800-63B-4 says verifiers "SHALL NOT impose". SecOpsLog follows NIST and records the deviation where a CIS audit applies. The impact is small: rules apply only when a password is set, existing passwords are not re-checked, and SSH key users are not affected. To roll back, delete the drop-in; sudo pam-auth-update --disable pwquality takes the module out of the stack.
Lockout with faillock
The threat is online guessing against whatever still accepts passwords: the console, su, sudo for users who need a password, SSH where password logins remain. NIST SP 800-63B-4 requires limiting consecutive failures to no more than 100; the CIS RHEL 10 Level 1 profile uses 5 failures and a 900-second lock. Settings go in /etc/security/faillock.conf. Its defaults (3 failures, 600 seconds) apply when, as on a fresh install, every line is commented out.
# SecOpsLog: lock after 5 failures within 15 minutes; unlock after 15.deny = 5fail_interval = 900unlock_time = 900
Ubuntu ships no faillock profile, so write two. pam_faillock(8) needs a preauth call before pam_unix and an authfail call after it, plus an account call that clears the count after a successful login. The priorities place them: 1024 runs before unix, 0 after it.
Name: Lock an account after repeated failures (pam_faillock, record)Default: noPriority: 0Auth-Type: PrimaryAuth:[default=die] pam_faillock.so authfail
Name: Lock an account after repeated failures (pam_faillock, check and reset)Default: noPriority: 1024Auth-Type: PrimaryAuth:requisite pam_faillock.so preauthAccount-Type: PrimaryAccount:required pam_faillock.so
pam-auth-update computed the jump: pam_unix now skips two lines on success, over the authfail call and pam_deny (try_first_pass makes it reuse a password an earlier module already read). Read the new stack as three paths:
Now fail five times, read the record, and try the right password:
Five failures are recorded with the service that saw them (an SSH password failure would show the remote address instead). The sixth attempt, with the right password, is refused before the password is read. The journal line under pamtester is what you alert on; on a real server the identifier is the program that asked, such as sshd-session or login.
Operational impact is where lockout policies go wrong. Anyone who knows an account name can lock it, so a shared account locked by a stranger is an outage. Root is exempt unless you add even_deny_root, and on Ubuntu root has no password anyway. Tallies live in /run/faillock, so a reboot clears them. SSH key logins never pass through the auth stage, so faillock neither counts nor blocks them. Your own mistyped sudo passwords do count, so an administrator who fumbles five times is locked out of sudo for 15 minutes. The lab's profile is written to show the lock; it also leaks. pam_faillock(8) notes that preauth without silent in faillock.conf, or under the requisite control, tells a guesser whether an account exists, because the lock message never appears for unknown names. RHEL's profile avoids both with auth required pam_faillock.so preauth silent (below); use that line in faillock_notify on a real server, at the cost of users not being told why they are refused. Clear a real user's lock with faillock --user NAME --reset only after checking where the failures came from. To roll back, disable the profiles:
The stack is back to Ubuntu's four lines. Delete the two profile files as well if you do not intend to enable them again.
sudo -i before you enable or disable a profile, and keep it open. After the change, open a new session and run sudo -k; sudo true to prove that sudo still works, and know sudo faillock --user NAME --reset, to run from the root shell. The console is no fallback: login includes the same common-auth, and on Ubuntu, where root's password is locked, the emergency shell does not open (sulogin(8) needs --force for that). What remains is a boot-level repair or a rescue image. For hosts whose PAM is managed centrally, keep a documented break-glass path, such as a console-only root password held in your secrets vault.On RHEL 10: authselect
RHEL builds system-auth and password-auth from an authselect profile, and the files in /etc/pam.d are symbolic links into /etc/authselect, so a hand edit is lost the next time authselect writes them. pam_pwquality is already in the stack, with library defaults (a minimum length of 8), so the same drop-in as above applies. Lockout is a feature of the profile:
with-faillock adds the same three calls, but as required and with silent on preauth, so neither the control nor a message reveals whether an account exists; it reads /etc/security/faillock.conf, which on this machine is all comments (defaults: 3 failures, 600 seconds). disable-feature is the rollback, and without-nullok and with-pwhistory are the other features a hardening baseline usually asks for. pamtester is not in the Rocky Linux BaseOS, AppStream or Extras repositories, so on RHEL test with a throwaway user at the console or over a password-enabled SSH test instance.
Try this
On an Ubuntu test machine, with a second session open as root (sudo -i), create a throwaway user with a password, the hard-pam-demo service file and the two faillock profiles, and run sudo pam-auth-update --enable faillock faillock_notify. Fail five times with pamtester and confirm that the right password then gets "The account is locked due to 5 failed logins." Check sudo faillock --user, reset it, and confirm the right password works again. Add the pwquality drop-in and confirm that a 14-character password is refused with "shorter than 15 characters". Finish with sudo pam-auth-update --disable faillock faillock_notify and check that common-auth has four lines again.
Takeaway
Change PAM only through pam-auth-update profiles or authselect features, test the result with pamtester on a separate service before real logins use it, and keep a root session (sudo -i) open until you have. Prefer length and a blocklist over character rules, and a temporary lock over a permanent one.