Debian GNU/Linux · Accounts & authentication

Set password aging and hashing defaults for new accounts

When it fails: New accounts get passwords that never expire and are never disabled when unused. Accounts created with the Debian defaults keep the same password forever and stay usable after their owner left.

CB-DEB-AUTH-014lowactiveautomatedautomated fixprofile core

As released in v0.1.1 beta · pre-release. Current state.

Description

/etc/login.defs and useradd -D define what new local accounts get:

  • PASS_MAX_DAYS: maximum age of a password (Debian: 99999, never expires);
  • PASS_MIN_DAYS: minimum days between two changes (Debian: 0);
  • PASS_WARN_AGE: days of warning before expiry (Debian: 7);
  • ENCRYPT_METHOD: hash used by chpasswd and newusers (Debian 12: SHA512, Debian 13: YESCRYPT);
  • INACTIVE (useradd -D): days after password expiry before the account is disabled (Debian: never).

NIST SP 800-63B advises against forced periodic changes when passwords are long and checked against breach lists. Set max_days to 99999 in your tailoring file if you follow that model and rely on the quality and lockout controls instead.

Rationale

A password that is never renewed and an account that is never disabled let a leaked password, or a departed user, keep access indefinitely. A minimum age stops users from cycling through the history to get their old password back.

Impact

New accounts must change their password every {{ max_days }} days and are disabled {{ inactive_days }} days after it expires. Existing accounts are handled by CB-DEB-AUTH-015.

Setting

Location/etc/login.defs, /etc/default/useradd
DefaultPASS_MAX_DAYS 99999, PASS_MIN_DAYS 0, PASS_WARN_AGE 7, INACTIVE -1
RecommendedPASS_MAX_DAYS <= {{ max_days }}, PASS_MIN_DAYS >= {{ min_days }}, PASS_WARN_AGE >= 7, YESCRYPT or SHA512, INACTIVE <= {{ inactive_days }}

Parameters

ParameterCoreServerServer hardenedDesktopDesktop hardenedBounds
max_daysinteger, days. Maximum password age in days.3653653653653651 to 99999
min_daysinteger, days. Minimum days between password changes.001010 to 7
inactive_daysinteger, days. Days after password expiry before the account is disabled.45454545450 to 365

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. In /etc/login.defs: PASS_MAX_DAYS <= {{ max_days }}, PASS_MIN_DAYS >= {{ min_days }}, PASS_WARN_AGE >= 7, ENCRYPT_METHOD is YESCRYPT or SHA512.
  2. useradd -D | grep INACTIVE shows a value between 0 and {{ inactive_days }}.

Automated: every check must pass. Scanners report each check by its id.

login-defs

Aging and hashing settings in /etc/login.defs.

script: |
  get() { sed -n "s/^[[:space:]]*$1[[:space:]]\{1,\}\([^[:space:]]*\).*/\1/p" /etc/login.defs | tail -n 1; }
  max=$(get PASS_MAX_DAYS) min=$(get PASS_MIN_DAYS) warn=$(get PASS_WARN_AGE) enc=$(get ENCRYPT_METHOD)
  echo "PASS_MAX_DAYS=$max PASS_MIN_DAYS=$min PASS_WARN_AGE=$warn ENCRYPT_METHOD=$enc"
  [ "${max:-99999}" -le "$CB_MAX_DAYS" ] && [ "${min:-0}" -ge "$CB_MIN_DAYS" ] && [ "${warn:-0}" -ge 7 ] \
    && { [ "$enc" = YESCRYPT ] || [ "$enc" = SHA512 ]; }

inactive

Default INACTIVE of useradd.

script: |
  i=$(useradd -D | sed -n 's/^INACTIVE=//p')
  echo "INACTIVE=$i"
  [ "${i:--1}" -ge 0 ] && [ "${i:--1}" -le "$CB_INACTIVE_DAYS" ]

Remediation

  1. Set in /etc/login.defs: PASS_MAX_DAYS {{ max_days }}, PASS_MIN_DAYS {{ min_days }}, PASS_WARN_AGE 7 and keep ENCRYPT_METHOD at YESCRYPT (or SHA512).
  2. Run useradd -D -f {{ inactive_days }}.
Restartnone
Disruptionnone

Rollback

Restore /etc/login.defs and /etc/default/useradd from the backup.

defaults

f=/etc/login.defs
cb_backup "$f"
cb_backup /etc/default/useradd
set_def() {
  if grep -Eq "^[[:space:]]*$1[[:space:]]" "$f"; then sed -i "s/^[[:space:]]*$1[[:space:]].*/$1\t$2/" "$f"; else printf '%s\t%s\n' "$1" "$2" >>"$f"; fi
}
set_def PASS_MAX_DAYS "$CB_MAX_DAYS"
set_def PASS_MIN_DAYS "$CB_MIN_DAYS"
w=$(sed -n 's/^[[:space:]]*PASS_WARN_AGE[[:space:]]\{1,\}\([0-9]*\).*/\1/p' "$f" | tail -n 1)
[ "${w:-0}" -ge 7 ] || set_def PASS_WARN_AGE 7
e=$(sed -n 's/^[[:space:]]*ENCRYPT_METHOD[[:space:]]\{1,\}\([A-Z0-9]*\).*/\1/p' "$f" | tail -n 1)
case $e in YESCRYPT | SHA512) ;; *) set_def ENCRYPT_METHOD YESCRYPT ;; esac
useradd -D -f "$CB_INACTIVE_DAYS"

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.1IA-5(1)partial
NIST SP 800-53 Rev. 55.1.1AC-2(3)partial

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

Threats mitigated

  • related CB-DEB-AUTH-015 Apply the password aging policy to existing accounts

People and lifecycle

Authors
Verified2026-10-18 by Sn0wAlice on 12, Proxmox VM, Debian 12 genericcloud image, kernel 6.1.0-54-cloud-amd64. Generated check, fix and check again for profiles core and server-hardened; state kept after reboot.
Verified2026-10-18 by Sn0wAlice on 13, Proxmox VM, Debian 13 genericcloud image, kernel 6.12.113+deb13-cloud-amd64. Generated check, fix and check again for profiles core and server-hardened; state kept after reboot.
Introduced inv0.1.0
Created, updated2026-10-18, 2026-10-18
Translationsfr