Account hardening
Listen to this lesson
This episode is a study companion for CompTIA Linux+ XK0-006 and is not produced by or endorsed by CompTIA.
Why this matters
Most breaches start with a credential rather than an exploit. Somebody reuses a password that appeared in a breach elsewhere, or a service account keeps a default password nobody changed, or an old employee's login still works.
The controls here are unglamorous and they are what actually stops that. Worth knowing too that the conventional advice has changed: forced 90-day rotation is now discouraged by NIST, because it reliably produces Summer2026! followed by Autumn2026!. Length and breach-checking beat complexity and rotation.
The lesson
Password policy: length, complexity, history
Policy is enforced by PAM, in pam_pwquality.so:
# /etc/security/pwquality.conf
minlen = 14
dcredit = -1 # at least 1 digit
ucredit = -1 # at least 1 uppercase
lcredit = -1 # at least 1 lowercase
ocredit = -1 # at least 1 special character
minclass = 3 # at least 3 of the 4 classes
maxrepeat = 3 # no more than 3 identical consecutive characters
dictcheck = 1 # reject dictionary words
usercheck = 1 # reject passwords containing the username
retry = 3
Negative credit values mean require at least this many; positive values mean count them toward the length requirement, which is the source of much confusion. -1 is what you want.
Length matters far more than complexity. A 20-character passphrase beats an 8-character password with four character classes by an enormous margin, and it is easier to remember and type. Set a high minlen and be relaxed about minclass.
Reuse and history — stop people cycling back to an old password:
# in /etc/pam.d/system-auth (RHEL) or /etc/pam.d/common-password (Debian)
password sufficient pam_unix.so sha512 shadow use_authtok remember=24
remember=24 keeps the last 24 hashes in /etc/security/opasswd and refuses them. Without it, "you must change your password" is satisfied by changing it and changing it straight back.
Expiration is set per account with chage, and system-wide defaults live in /etc/login.defs:
chage -M 365 alice # maximum age
chage -m 1 alice # minimum age — stops instant cycling
chage -W 14 alice # warning period
chage -l alice
The -m minimum is the companion to remember=: without a minimum age a user can change their password 25 times in a minute to flush the history and return to the original.
Current guidance is a long maximum age or none at all, combined with immediate forced change on any suspicion of compromise, which is what chage -d 0 does.
Lockout after failed attempts
pam_tally2 was the traditional module; pam_faillock replaced it on current systems and is what you will actually configure. Both count failures and lock the account.
# pam_faillock (current)
faillock --user alice # show failures
faillock --user alice --reset # clear them
# pam_tally2 (older systems)
pam_tally2 --user alice
pam_tally2 --user alice --reset
# /etc/security/faillock.conf
deny = 5
unlock_time = 900 # auto-unlock after 15 minutes
fail_interval = 900
even_deny_root = 0
unlock_time rather than a permanent lock is usually right. A permanent lock turns a forgotten password into a help-desk ticket, and — more importantly — it hands anyone an easy denial-of-service: fail five logins against every account and nobody can work. Automatic unlock after fifteen minutes blunts brute-forcing without that.
Leave even_deny_root = 0 unless you have another guaranteed route in. Locking root out of a machine with no console is a bad afternoon.
Multifactor authentication
A password is something you know. MFA adds something you have — a phone, a hardware key — so a stolen password alone is not enough.
dnf install google-authenticator
google-authenticator # per user: generates a secret and QR code
# /etc/pam.d/sshd
auth required pam_google_authenticator.so
# /etc/ssh/sshd_config
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
That AuthenticationMethods line requires both a key and a token — genuine two-factor. Hardware keys (YubiKey, and anything speaking FIDO2) are stronger again, and OpenSSH supports them natively with ed25519-sk key types, which require a physical touch per authentication.
Set up and test MFA in a second session. A misconfigured PAM stack for sshd locks out every remote login at once, and this is the single easiest way to do that to yourself.
Checking against breach lists
Complexity rules do not stop Password1!, which meets every requirement and appears in every breach corpus. Checking candidate passwords against known breached ones catches what rules cannot.
Have I Been Pwned exposes this with k-anonymity, so you never send the password or its full hash:
# SHA-1 the password, send the first 5 hex characters only
printf 'password123' | sha1sum | tr 'a-f' 'A-F' | cut -c1-5
curl https://api.pwnedpasswords.com/range/CBFDA # returns ~800 suffixes
# match the remaining 35 characters locally
The server learns five characters of a hash and nothing else. That property is what makes the check acceptable to run against real credentials — and it is worth understanding rather than memorising, because "does this leak the password?" is the obvious objection and the answer is no.
For offline enforcement, pam_pwquality can consult a local wordlist built from breach corpora, which is the approach in high-security environments where no external call is permitted.
Not running as root
The most effective account control is simply not being root.
- Log in as yourself; escalate with
sudofor specific commands. -
Run services as dedicated unprivileged accounts. A compromised web server running as
nginxreaches whatnginxmay reach; running as root it reaches everything. - Use systemd's own confinement in unit files — these are per-service least privilege and cost nothing:
[Service]
User=myapp
Group=myapp
NoNewPrivileges=true # cannot gain privileges via setuid
PrivateTmp=true # its own /tmp, invisible to others
ProtectSystem=strict # the filesystem is read-only to it
ProtectHome=true # /home, /root, /run/user are inaccessible
ReadWritePaths=/var/lib/myapp
NoNewPrivileges=true is the highest-value single line there. It means the process and every child can never acquire privileges, so a setuid binary inside that service's tree becomes useless as an escalation route.
Restricted shells
Some accounts should exist without a usable login.
/sbin/nologin refuses login politely and logs the attempt. This is the correct shell for every service account — nginx, postgres, backup. /bin/false does the same more abruptly.
usermod -s /sbin/nologin svcaccount
grep -vE '/(nologin|false)$' /etc/passwd | cut -d: -f1,7 # who CAN log in?
That last command is a good audit one-liner: it lists every account with a real shell, and anything on it that is not a person is worth questioning.
/bin/rbash — restricted bash — permits login but confines the user. They cannot cd, cannot set PATH or SHELL, cannot use absolute path names to run commands, and cannot redirect output. Combined with a directory of symlinks to the handful of permitted commands, it gives a genuinely limited account.
usermod -s /bin/rbash limiteduser
mkdir /home/limiteduser/bin
ln -s /usr/bin/less /home/limiteduser/bin/
# set PATH=/home/limiteduser/bin in a root-owned, read-only ~/.bash_profile
rbash is a speed bump, not a jail. Any permitted command with a shell escape defeats it — vi, less, awk, find — which is the same weakness as a careless sudo grant. If the user needs a real boundary, use a chroot, a container, or ForceCommand in sshd.
On the exam
- Length beats complexity; current guidance discourages forced rotation and prefers breach-checking.
-
pam_pwqualityenforces complexity;remember=inpam_unixenforces history, and needs a minimum age (chage -m) or it can be flushed. -
pam_faillockis the current lockout module;pam_tally2is the older one.unlock_timeavoids a trivial denial-of-service. - MFA via
pam_google_authenticator, withAuthenticationMethods publickey,keyboard-interactiverequiring both factors. - Breach checking with k-anonymity sends only the first five hash characters.
- Service accounts get
/sbin/nologinand no password. -
rbashrestricts but is escaped by any command with a shell escape. -
NoNewPrivileges=truein a systemd unit blocks setuid escalation for that service and its children.
Practise what you just read
1. What does current password guidance from NIST and similar bodies recommend?
Select one
Show answer
C. Forced rotation produces predictable variations -- Summer2025! becoming Autumn2025! -- and complexity rules push people toward substitutions attackers already model. Length is what raises the cost of cracking, and checking against breach corpora eliminates passwords already known. Rotate on evidence of compromise, not on the calendar.
2. Which PAM module implements account lockout after repeated failed logins on a current system?
Select one
Show answer
D. pam_faillock replaced the older pam_tally2, which is deprecated and absent from recent releases. Set unlock_time so a lockout expires on its own -- without it, anyone who knows a username can lock a colleague out indefinitely by failing a few logins, which turns a defence into a denial-of-service tool.
3. What should a service account's login shell and password be set to?
Select one
Show answer
A. A service account exists for a daemon to run as, not for anyone to log in with, so it needs neither an interactive shell nor a password. nologin refuses the session and /bin/false does the same silently. Any password on such an account is an unnecessary credential to leak, and any shell is an unnecessary route in.
5 more questions on this objective are part of the full course.
Hands-on labs
Part of the free CompTIA Linux+ XK0-006 course — 48 lessons and 82 hands-on labs.