Description
pam_faillock (in libpam-modules) counts failed authentications per user and denies access
once the count reaches deny within fail_interval, until unlock_time seconds have passed
or an administrator runs faillock --user <name> --reset. Its settings are in
/etc/security/faillock.conf. Debian ships the module but does not enable it: two
pam-auth-update profiles add it before (preauth) and after (authfail) pam_unix, and in
the account stack.
SSH public key logins do not go through the password check and are not counted.
Rationale
A lockout turns an online guessing attack from millions of attempts into a handful per account and per unlock period, and makes the attack visible in the logs.
Impact
A user who mistypes their password {{ deny }} times waits {{ unlock_time }} seconds, or asks an
administrator. An attacker can lock out a known account on purpose. With deny_root, root
can be locked too: keep a console access that does not rely on PAM password checks, such as
an SSH key.
Setting
| Location | /usr/share/pam-configs/cirsec_faillock*, /etc/security/faillock.conf |
| Default | Not enabled |
| Recommended | deny = {{ deny }}, unlock_time = {{ unlock_time }} |
Parameters
| Parameter | Core | Server | Server hardened | Desktop | Desktop hardened | Bounds |
|---|---|---|---|---|---|---|
denyinteger. Failed attempts before the account is locked. | 5 | 5 | 5 | 5 | 5 | 1 to 10 |
unlock_timeinteger, seconds. Seconds before a locked account unlocks by itself. 0 means only an administrator can unlock it. | 900 | 900 | 900 | 900 | 900 | 0 to 86400 |
deny_rootboolean. Also lock root after failed attempts (even_deny_root). | false | false | true | false | true |
The value used is your override (tailoring file), else the profile's, else the one of the profile it extends, else the default. Bounds limit what an override may set.
Audit
/etc/pam.d/common-authmust containpam_faillock.so preauthbeforepam_unix.soandpam_faillock.so authfailafter it;/etc/pam.d/common-accountmust containpam_faillock.so.- In
/etc/security/faillock.conf:denybetween 1 and {{ deny }},unlock_timeof 0 or at least {{ unlock_time }}; ifdeny_rootis required,even_deny_rootis set.
Automated: every check must pass. Scanners report each check by its id.
preauth
path: /etc/pam.d/common-auth
pattern: ^\s*auth\s+.*pam_faillock\.so\s+preauth
authfail
path: /etc/pam.d/common-auth
pattern: ^\s*auth\s+.*pam_faillock\.so\s+authfail
account
path: /etc/pam.d/common-account
pattern: ^\s*account\s+.*pam_faillock\.so
settings
Effective faillock.conf settings.
script: |
f=/etc/security/faillock.conf
get() { sed -n "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*\([0-9]*\).*/\1/p" "$f" | tail -n 1; }
deny=$(get deny) unlock=$(get unlock_time)
root=$(grep -c '^[[:space:]]*even_deny_root' "$f")
echo "deny=${deny:-3} unlock_time=${unlock:-600} even_deny_root=$root"
[ "${deny:-3}" -ge 1 ] && [ "${deny:-3}" -le "$CB_DENY" ] || exit 1
[ "${unlock:-600}" -eq 0 ] || [ "${unlock:-600}" -ge "$CB_UNLOCK_TIME" ] || exit 1
[ "$CB_DENY_ROOT" != true ] || [ "$root" -gt 0 ]
Remediation
- Create
/usr/share/pam-configs/cirsec_faillock_notify:Name: CIRSEC - count failed logins (pam_faillock preauth) Default: yes Priority: 1024 Auth-Type: Primary Auth: requisite pam_faillock.so preauth Account-Type: Primary Account: required pam_faillock.so - Create
/usr/share/pam-configs/cirsec_faillock:Name: CIRSEC - lock after failed logins (pam_faillock authfail) Default: yes Priority: 0 Auth-Type: Primary Auth: [default=die] pam_faillock.so authfail - Run
pam-auth-update --enable cirsec_faillock_notify cirsec_faillock. - Set
deny = {{ deny }}andunlock_time = {{ unlock_time }}in/etc/security/faillock.conf(andeven_deny_rootif required). - Before closing your session, check that a new login still works.
| Restart | none |
| Disruption | low |
| Lockout risk | yes: can cut off administrative access. Off by default in generated scripts and playbooks, opt in through a tailoring file. |
Rollback
Run pam-auth-update --remove cirsec_faillock cirsec_faillock_notify, delete the two profile
files, and unlock accounts with faillock --user <name> --reset.
configure
#!/bin/sh
# CB-DEB-AUTH-011 / configure - enable pam_faillock through pam-auth-update profiles and set faillock.conf.
# Parameters: CB_DENY, CB_UNLOCK_TIME, CB_DENY_ROOT. Idempotent.
# SPDX-License-Identifier: Apache-2.0
set -eu
: "${CB_DENY:?}" "${CB_UNLOCK_TIME:?}" "${CB_DENY_ROOT:?}"
p=/usr/share/pam-configs
cat >"$p/cirsec_faillock_notify" <<'EOF'
Name: CIRSEC - count failed logins (pam_faillock preauth)
Default: yes
Priority: 1024
Auth-Type: Primary
Auth:
requisite pam_faillock.so preauth
Account-Type: Primary
Account:
required pam_faillock.so
EOF
cat >"$p/cirsec_faillock" <<'EOF'
Name: CIRSEC - lock after failed logins (pam_faillock authfail)
Default: yes
Priority: 0
Auth-Type: Primary
Auth:
[default=die] pam_faillock.so authfail
EOF
for f in /etc/pam.d/common-auth /etc/pam.d/common-account; do cb_backup "$f"; done
DEBIAN_FRONTEND=noninteractive pam-auth-update --enable cirsec_faillock_notify cirsec_faillock
# Set one "key = value" (or a bare flag) in faillock.conf, replacing any active or commented line.
f=/etc/security/faillock.conf
cb_backup "$f"
set_kv() {
sed -i "/^[[:space:]]*#*[[:space:]]*$1[[:space:]]*\(=.*\)\{0,1\}$/d" "$f"
printf '%s\n' "$2" >>"$f"
}
set_kv deny "deny = $CB_DENY"
set_kv unlock_time "unlock_time = $CB_UNLOCK_TIME"
if [ "$CB_DENY_ROOT" = true ]; then set_kv even_deny_root even_deny_root; fi
Handwritten code reads parameters from CB_<NAME> environment variables. Each release turns these fixes into remediate.sh and an Ansible role per profile: dry run by default, a fix runs only when its check fails.
References
- pam_faillock(8), Debian manual pages Debian, vendor-doc, accessed 2026-10-18
- faillock.conf(5), Debian manual pages Debian, vendor-doc, accessed 2026-10-18
- pam-auth-update(8), Debian manual pages Debian, vendor-doc, accessed 2026-10-18
Mappings
| Framework | ID | Relation |
|---|---|---|
| NIST SP 800-53 Rev. 55.1.1 | AC-7 | equivalent |
How this control relates to the other framework's: equivalent, stricter, looser, partial or related.
Threats mitigated
| Taxonomy | ID |
|---|---|
| MITRE ATT&CK | T1110.001 |
People and lifecycle
| Authors |
|
| Verified | 2026-10-18 by Sn0wAlice on 12, Proxmox VM, Debian 12 genericcloud image, kernel 6.1.0-54-cloud-amd64. Lockout tested with pamtester - locked after 5 failures, faillock --reset unlocks; profiles core and server-hardened (even_deny_root). |
| Verified | 2026-10-18 by Sn0wAlice on 13, Proxmox VM, Debian 13 genericcloud image, kernel 6.12.113+deb13-cloud-amd64. Lockout tested with pamtester - locked after 5 failures, faillock --reset unlocks; profiles core and server-hardened (even_deny_root). |
| Introduced in | v0.1.0 |
| Created, updated | 2026-10-18, 2026-10-18 |
| Translations | fr |