{
  "id": "CB-UBU-AUTH-015",
  "standard": "ubuntu",
  "version": "main",
  "url": "https://baselines.cirsec.org/b/ubuntu/CB-UBU-AUTH-015",
  "json": "https://baselines.cirsec.org/b/ubuntu/CB-UBU-AUTH-015.json",
  "yaml": "https://baselines.cirsec.org/b/ubuntu/CB-UBU-AUTH-015.yml",
  "profiles_effective": [
    "core",
    "server",
    "server-hardened",
    "desktop",
    "desktop-hardened"
  ],
  "license": "Text CC BY 4.0, code Apache-2.0",
  "attribution": "CIRSEC Baselines, https://baselines.cirsec.org",
  "control": {
    "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\nperiod, inactivity period, date of last change), copied from the defaults when it was created.\nThis control applies the policy of CB-UBU-AUTH-014 to every existing account that has a\npassword, and checks that no last change date lies in the future.\n",
    "rationale": "Without this step, the aging policy only protects accounts created after it was set. A last\nchange date in the future would postpone expiry indefinitely.\n",
    "impact": "An account whose password is older than {{ max_days }} days is asked to change it at its next\npassword login, and is disabled {{ inactive_days }} days later. SSH key logins of such an\naccount are also refused until the password is changed: check service and automation accounts\nbefore applying.\n",
    "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\nwith `chage -l <user>`: maximum <= {{ max_days }}, minimum >= {{ min_days }}, warning >= 7,\ninactive between 0 and {{ inactive_days }}, and a last change date that is not in the future.\n",
      "checks": [
        {
          "id": "accounts",
          "engine": "shell",
          "description": "Aging fields of every account with a password.",
          "spec": {
            "script": "today=$(( $(date +%s) / 86400 ))\nbad=$(awk -F: -v max=\"$CB_MAX_DAYS\" -v min=\"$CB_MIN_DAYS\" -v inact=\"$CB_INACTIVE_DAYS\" -v today=\"$today\" '\n  $2 ~ /^\\$/ {\n    r = \"\"\n    if ($5 == \"\" || $5 > max) r = r \" max=\" $5\n    if ($4 == \"\" || $4 < min) r = r \" min=\" $4\n    if ($6 == \"\" || $6 < 7) r = r \" warn=\" $6\n    if ($7 == \"\" || $7 < 0 || $7 > inact) r = r \" inactive=\" $7\n    if ($3 != \"\" && $3 > today) r = r \" last_change_in_future\"\n    if (r != \"\") print $1 \":\" r\n  }' /etc/shadow)\n[ -z \"$bad\" ] && { echo \"all accounts follow the policy\"; exit 0; }\nprintf '%s\\n' \"$bad\"\nexit 1\n"
          }
        }
      ]
    },
    "remediation": {
      "manual": "For each account with a password: `chage --maxdays {{ max_days }} --mindays {{ min_days }}\n--warndays 7 --inactive {{ inactive_days }} <user>`. Fix last change dates in the future with\n`chage --lastday <YYYY-MM-DD> <user>`.\n",
      "effects": {
        "restart": "none",
        "disruption": "low",
        "lockout_risk": true
      },
      "rollback": "Restore `/etc/shadow` from the backup, or reset each account with `chage`.\n",
      "scripts": [
        {
          "id": "chage",
          "engine": "shell",
          "content": "cb_backup /etc/shadow\nawk -F: '$2 ~ /^\\$/ {print $1}' /etc/shadow | while read -r u; do\n  chage --maxdays \"$CB_MAX_DAYS\" --mindays \"$CB_MIN_DAYS\" --warndays 7 --inactive \"$CB_INACTIVE_DAYS\" \"$u\"\ndone\n"
        }
      ]
    },
    "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"
      }
    ],
    "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"
  }
}