Ubuntu · 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 Ubuntu defaults keep the same password forever and stay usable after their owner left.

CB-UBU-AUTH-014lowactiveautomatedautomated fixprofile core

As released in v0.1.2 (beta). Current state.

Description

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

  • PASS_MAX_DAYS: maximum age of a password (Ubuntu: 99999, never expires);
  • PASS_MIN_DAYS: minimum days between two changes (Ubuntu: 0);
  • PASS_WARN_AGE: days of warning before expiry (Ubuntu: 7);
  • ENCRYPT_METHOD: hash used by chpasswd and newusers (Ubuntu 20.04 to 24.04: SHA512, 26.04: YESCRYPT; the shadow tools of 20.04 do not support YESCRYPT);
  • INACTIVE (useradd -D): days after password expiry before the account is disabled (Ubuntu: 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-UBU-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; only SHA512 on Ubuntu 20.04).
  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)
h=YESCRYPT
[ "$(. /etc/os-release && printf '%s' "$VERSION_ID")" != 20.04 ] || h=SHA512
case $e in YESCRYPT | SHA512) ;; *) set_def ENCRYPT_METHOD "$h" ;; 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-UBU-AUTH-015 Apply the password aging policy to existing accounts

People and lifecycle

Authors
Verified2026-10-19 by Sn0wAlice on 20.04, Proxmox VM, Ubuntu 20.04 cloud image, kernel 5.4.0-216-generic. Fails on a fresh install as expected; generated fix, check again and second fix idempotent; kept after reboot; profiles core and server-hardened.
Verified2026-10-19 by Sn0wAlice on 22.04, Proxmox VM, Ubuntu 22.04 cloud image, kernel 5.15.0-198-generic. Fails on a fresh install as expected; generated fix, check again and second fix idempotent; kept after reboot; profiles core and server-hardened.
Verified2026-10-19 by Sn0wAlice on 24.04, Proxmox VM, Ubuntu 24.04 cloud image, kernel 6.8.0-147-generic. Fails on a fresh install as expected; generated fix, check again and second fix idempotent; kept after reboot; profiles core and server-hardened.
Verified2026-10-19 by Sn0wAlice on 26.04, Proxmox VM, Ubuntu 26.04 cloud image, kernel 7.0.0-38-generic. Fails on a fresh install as expected; generated fix, check again and second fix idempotent; kept after reboot; profiles core and server-hardened.
Introduced inv0.1.0
Created, updated2026-10-19, 2026-10-19
Translationsfr