Debian GNU/Linux · Accounts & authentication

Lock accounts after repeated failed logins

When it fails: Passwords can be guessed without limit. Without a lockout, an attacker can try passwords against local accounts as fast as the login services allow.

CB-DEB-AUTH-011mediumactiveautomatedautomated fix, lockout riskprofile core

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
DefaultNot enabled
Recommendeddeny = {{ deny }}, unlock_time = {{ unlock_time }}

Parameters

ParameterCoreServerServer hardenedDesktopDesktop hardenedBounds
denyinteger. Failed attempts before the account is locked.555551 to 10
unlock_timeinteger, seconds. Seconds before a locked account unlocks by itself. 0 means only an administrator can unlock it.9009009009009000 to 86400
deny_rootboolean. Also lock root after failed attempts (even_deny_root).falsefalsetruefalsetrue

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

  1. /etc/pam.d/common-auth must contain pam_faillock.so preauth before pam_unix.so and pam_faillock.so authfail after it; /etc/pam.d/common-account must contain pam_faillock.so.
  2. In /etc/security/faillock.conf: deny between 1 and {{ deny }}, unlock_time of 0 or at least {{ unlock_time }}; if deny_root is required, even_deny_root is 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

  1. 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
    
  2. 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
    
  3. Run pam-auth-update --enable cirsec_faillock_notify cirsec_faillock.
  4. Set deny = {{ deny }} and unlock_time = {{ unlock_time }} in /etc/security/faillock.conf (and even_deny_root if required).
  5. Before closing your session, check that a new login still works.
Restartnone
Disruptionlow
Lockout riskyes: 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

Mappings

FrameworkIDRelation
NIST SP 800-53 Rev. 55.1.1AC-7equivalent

How this control relates to the other framework's: equivalent, stricter, looser, partial or related.

Threats mitigated

People and lifecycle

Authors
Verified2026-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).
Verified2026-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 inv0.1.0
Created, updated2026-10-18, 2026-10-18
Translationsfr