Accounts & PAM policy
Password rules, lockout, and login limits.
A big office building has a lot of doors. The front entrance, the parking garage, the server room, the loading dock. You do not want each door inventing its own way to check badges, because then a mistake at one door is a hole nobody else knows about. So the building runs one security desk with one rulebook, and every door phones that same desk before it opens. On Linux, the shared desk is PAM (Pluggable Authentication Modules), the machinery the operating system calls whenever a program needs to prove who you are.
When you log in at the console, run sudo (the command that runs one action as another user, usually root), open an SSH (Secure Shell, the encrypted remote-login protocol) session, or type your password to get back past a locked screen, none of those programs decides on its own whether to let you in. Each hands the question to PAM. That is the whole point. You set your password rules, your lockout policy, and your login limits once, in the place every login path already consults, instead of hoping every service got it right by itself.
How PAM Decides Who Gets In
Open /etc/pam.d/ and you see one file per service (sshd, sudo, login) plus a few shared fragments. Each line is one check, and the checks fall into four kinds of question. auth asks whether you are who you claim to be. account asks whether you are allowed in right now, today, from here. password governs how a new secret gets set. session runs the setup and teardown around a login, like mounting your home directory and applying limits. Ubuntu and Debian keep the shared logic in common-auth, common-account, common-password, and common-session, and a tool called pam-auth-update stitches them together so a package upgrade does not stomp your edits.
Each line also carries a control word (required, requisite, sufficient) that says what happens when that check passes or fails. A requisite failure stops the whole stack immediately. A required failure is recorded, the stack keeps running, then fails at the end so an attacker cannot tell which step rejected them. That ordering is why the same module gets listed more than once, as you will see with lockout.
Password Rules Worth Having
A four-digit bike lock is weak because it is short, not because the digits are exotic. Length is what makes guessing expensive. The file /etc/security/pwquality.conf is where you set the floor for every password on the box.
minlen = 14 # minimum length; length beats clever complexitydifok = 5 # a new password must differ from the old in 5+ positionsdcredit = -1 # require at least one digitucredit = -1 # require at least one uppercase lettermaxrepeat = 3 # no more than 3 identical characters in a rowenforce_for_root = 1 # root is not exempt
The credit options read backwards until you know the trick. A negative number is a requirement, a positive number is a bonus that counts toward the length. dcredit = -1 means the password must contain at least one digit (d is for digit). ucredit = -1 requires an uppercase letter. difok = 5 means a new password has to differ from the old one in at least five spots, so nobody rotates Summer2025 into Summer2026 and calls it a change. enforce_for_root = 1 makes the root account obey the same floor, which matters because root is the account attackers actually want.
None of that runs until the module is in the stack. On Debian and Ubuntu the rule lives in common-password, called through pam_pwquality.so.
password requisite pam_pwquality.so retry=3password [success=1 default=ignore] pam_unix.so obscure use_authtok try_first_pass yescrypt
That second line is the one that stores the result, hashing the password with yescrypt (a modern, deliberately slow password-hashing function that makes offline cracking painful). Test the whole thing the honest way, by trying to set a bad password as an ordinary user.
Slowing Down a Brute-Force
A cash machine (ATM) eats your card after three wrong PINs. It does not care how fast you type. Three strikes and the card is dead for a while. The pam_faillock module does the same for accounts. After a set number of failures it refuses that account for a cooldown window, so an attacker spraying passwords gets a handful of tries an hour instead of thousands a second. The settings go in one file.
deny = 5 # lock the account after 5 failed attemptsunlock_time = 900 # keep it locked for 15 minutes (900 seconds)fail_interval = 900 # only count failures within a 15-minute windoweven_deny_root # a bare flag: root is locked too
That last flag has a sharp edge. even_deny_root locks root as well, so a password-spray aimed at root can lock root out too, which is a denial of service (an outage caused by using up a resource, here the login itself). On a keys-only server that is usually the right call, but keep a way back in that you control, like physical console access or a break-glass account (an emergency admin login you set aside for exactly this), so a stranger cannot lock you out of your own machine by doing nothing but guessing wrong.
Like pwquality, faillock does nothing until it is wired into the auth stack, and it appears there more than once on purpose: a preauth check before the password is tested, an authfail line that records a loss, and an authsucc line on the winning path. It also needs a single line in the account group to actually turn a counted failure into a refusal.
auth requisite pam_faillock.so preauthauth [success=1 default=ignore] pam_unix.so nullokauth [default=die] pam_faillock.so authfailauth sufficient pam_faillock.so authsucc# and in /etc/pam.d/common-account:account required pam_faillock.so
This is where operations meets security. When someone runs a password-spray against your SSH port, the misses pile up in the authentication log (/var/log/auth.log) and faillock starts counting. The pam_unix line names the source address, and the pam_faillock line tells you the account crossed the limit. Once the lock trips, later tries show only the faillock line, because the requisite preauth check turns them away before the password is ever tested.
To see where an account stands, ask faillock directly. Each row is a recorded failure with the source address, which is often the fastest way to spot the attacking host.
Once you have confirmed a lockout was a stuck teammate and not the attacker still knocking, clear the counter with sudo faillock --user deploy --reset. Resetting while the source address is still hammering you only hands them another five free guesses, so check the source first.
Capping the Blast Radius
A fuse box keeps one shorted appliance from burning down the house. Resource limits do that for accounts. A fork bomb (a script that spawns processes until the machine falls over) is contained if the account it runs under can only ever hold a few hundred processes. You set the ceilings in /etc/security/limits.conf.
# domain type item value@developers hard nproc 512 # max processes per developer@developers hard nofile 4096 # max open files per developer* hard nproc 1024 # a ceiling for everyone else
The item nproc caps the number of processes a user can run (nproc is number of processes). nofile caps open file handles. hard is the ceiling nobody can raise, soft is the everyday default a user could bump up toward the hard limit. The module pam_limits.so in the session group applies these caps at login. deploy is one of the developers, so read its hard ceiling back with ulimit (the shell's built-in for showing and setting resource limits), using -H for the hard value, to confirm it landed.
Stale Credentials and Idle Accounts
A spare key you cut years ago still opens the door if nobody rekeyed the lock. Old credentials are the same. The account nobody has logged into since a contractor rolled off is exactly what an attacker hopes is still valid months later. Two files handle aging: login.defs sets the defaults stamped onto new accounts, and chage adjusts an account that already exists.
Those numbers live in /etc/shadow (one line per user, each date stored as a plain count of days since January 1, 1970). chage is the friendly front end that does the arithmetic for you. The account above carries only the distro default, 365 days with no inactivity lock. To tighten it, give chage a shorter maximum age and an inactivity window, and to retire an account for good, lock its password so no password login works at all. chage changes the account quietly and prints nothing on success, so the single line below is passwd confirming the lock.
PASS_MAX_DAYS 365 # default max password age for new accountsPASS_MIN_DAYS 0 # no forced wait before a user can change againPASS_WARN_AGE 7 # warn 7 days before expiryENCRYPT_METHOD YESCRYPT # hash new passwords with yescryptUMASK 022 # default permission mask for new files
Here is where good intentions go sideways. Forcing a password change every 30 days feels safe and does the opposite. NIST (the United States National Institute of Standards and Technology) found that frequent forced rotation pushes people toward weak, predictable patterns, Spring1, Spring2, Spring3, often on a sticky note under the keyboard. The stronger setup is a long unique password, checked against lists of known-breached passwords, plus MFA (multi-factor authentication, a second proof like a phone app or a hardware key) and lockout. Set a long maximum age to catch genuinely abandoned accounts, and let length and breach-checking carry the weight instead of the calendar.
Before you trust any of this, prove it. Create a throwaway account, fail its password the configured number of times, and watch faillock --user count the misses and then deny the login. Set its aging with chage and read it straight back with chage -l. Check the process ceiling with ulimit -Hu as that user. The config file is your intent, but the running behavior against a test account is the truth, and only the second one actually keeps anyone out.
Try this
Work through “Stale Credentials and Idle Accounts” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: a bad PAM line can lock out every login. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.