# yaml-language-server: $schema=../../../../schema/control.schema.json id: CB-UBU-AUTH-015 title: Apply the password aging policy to existing accounts finding: title: Existing accounts keep passwords that never expire description: Changing login.defs only affects new accounts; existing ones keep their old aging settings. status: active profiles: [core] risk: severity: low assessment: automated tags: [password, accounts] description: | Each account has its own aging fields in `/etc/shadow` (minimum and maximum age, warning period, inactivity period, date of last change), copied from the defaults when it was created. This control applies the policy of CB-UBU-AUTH-014 to every existing account that has a password, and checks that no last change date lies in the future. rationale: | Without this step, the aging policy only protects accounts created after it was set. A last change date in the future would postpone expiry indefinitely. impact: | An account whose password is older than {{ max_days }} days is asked to change it at its next password login, and is disabled {{ inactive_days }} days later. SSH key logins of such an account are also refused until the password is changed: check service and automation accounts before applying. parameters: - name: max_days type: integer description: Maximum password age in days. unit: days default: 365 minimum: 1 maximum: 99999 - name: min_days type: integer description: Minimum days between password changes. unit: days default: 0 profile_defaults: server-hardened: 1 desktop-hardened: 1 minimum: 0 maximum: 7 - name: inactive_days type: integer description: Days after password expiry before the account is disabled. unit: days default: 45 minimum: 0 maximum: 365 audit: manual: | For every account with a password hash in `/etc/shadow` (field 2 starting with `$`), check with `chage -l `: maximum <= {{ max_days }}, minimum >= {{ min_days }}, warning >= 7, inactive between 0 and {{ inactive_days }}, and a last change date that is not in the future. checks: - id: accounts engine: shell description: Aging fields of every account with a password. spec: script: | today=$(( $(date +%s) / 86400 )) bad=$(awk -F: -v max="$CB_MAX_DAYS" -v min="$CB_MIN_DAYS" -v inact="$CB_INACTIVE_DAYS" -v today="$today" ' $2 ~ /^\$/ { r = "" if ($5 == "" || $5 > max) r = r " max=" $5 if ($4 == "" || $4 < min) r = r " min=" $4 if ($6 == "" || $6 < 7) r = r " warn=" $6 if ($7 == "" || $7 < 0 || $7 > inact) r = r " inactive=" $7 if ($3 != "" && $3 > today) r = r " last_change_in_future" if (r != "") print $1 ":" r }' /etc/shadow) [ -z "$bad" ] && { echo "all accounts follow the policy"; exit 0; } printf '%s\n' "$bad" exit 1 remediation: manual: | For each account with a password: `chage --maxdays {{ max_days }} --mindays {{ min_days }} --warndays 7 --inactive {{ inactive_days }} `. Fix last change dates in the future with `chage --lastday `. effects: restart: none disruption: low lockout_risk: true rollback: | Restore `/etc/shadow` from the backup, or reset each account with `chage`. scripts: - id: chage engine: shell content: | cb_backup /etc/shadow awk -F: '$2 ~ /^\$/ {print $1}' /etc/shadow | while read -r u; do chage --maxdays "$CB_MAX_DAYS" --mindays "$CB_MIN_DAYS" --warndays 7 --inactive "$CB_INACTIVE_DAYS" "$u" done references: - title: chage(1), Ubuntu manual pages url: https://manpages.ubuntu.com/manpages/noble/man1/chage.1.html publisher: Canonical kind: vendor-doc accessed: 2026-10-19 - title: shadow(5), Ubuntu manual pages url: https://manpages.ubuntu.com/manpages/noble/man5/shadow.5.html publisher: Canonical kind: vendor-doc accessed: 2026-10-19 mappings: - framework: nist-800-53r5 framework_version: "5.1.1" id: AC-2(3) relation: partial - framework: cis-ubuntu-2004 framework_version: "3.0.0" id: "5.4.1.1" relation: partial - framework: cis-ubuntu-2004 framework_version: "3.0.0" id: "5.4.1.5" relation: partial - framework: cis-ubuntu-2004 framework_version: "3.0.0" id: "5.4.1.6" relation: equivalent - framework: cis-ubuntu-2204 framework_version: "3.0.0" id: "5.4.1.1" relation: partial - framework: cis-ubuntu-2204 framework_version: "3.0.0" id: "5.4.1.5" relation: partial - framework: cis-ubuntu-2204 framework_version: "3.0.0" id: "5.4.1.6" relation: equivalent - framework: cis-ubuntu-2404 framework_version: "2.0.0" id: "5.4.1.1" relation: partial - framework: cis-ubuntu-2404 framework_version: "2.0.0" id: "5.4.1.5" relation: partial - framework: cis-ubuntu-2404 framework_version: "2.0.0" id: "5.4.1.6" relation: equivalent - framework: cis-ubuntu-2604 framework_version: "1.0.0" id: "5.4.1.1" relation: partial - framework: cis-ubuntu-2604 framework_version: "1.0.0" id: "5.4.1.5" relation: partial - framework: cis-ubuntu-2604 framework_version: "1.0.0" id: "5.4.1.6" relation: equivalent threats: - framework: mitre-attack id: T1078.003 related_controls: - id: CB-UBU-AUTH-014 type: requires authors: - name: Alice github: Sn0wAlice verified: - date: 2026-10-19 by: Sn0wAlice versions: ["20.04"] environment: Proxmox VM, Ubuntu 20.04 cloud image, kernel 5.4.0-216-generic notes: "Fails with a test account that has a password; chage applies the policy (min 1 day in server-hardened); kept after reboot." - date: 2026-10-19 by: Sn0wAlice versions: ["22.04"] environment: Proxmox VM, Ubuntu 22.04 cloud image, kernel 5.15.0-198-generic notes: "Fails with a test account that has a password; chage applies the policy (min 1 day in server-hardened); kept after reboot." - date: 2026-10-19 by: Sn0wAlice versions: ["24.04"] environment: Proxmox VM, Ubuntu 24.04 cloud image, kernel 6.8.0-147-generic notes: "Fails with a test account that has a password; chage applies the policy (min 1 day in server-hardened); kept after reboot." - date: 2026-10-19 by: Sn0wAlice versions: ["26.04"] environment: Proxmox VM, Ubuntu 26.04 cloud image, kernel 7.0.0-38-generic notes: "Fails with a test account that has a password; chage applies the policy (min 1 day in server-hardened); kept after reboot." introduced_in: 0.1.0 created: 2026-10-19 updated: 2026-10-19