NixOS users.users attribute missing passwordExpiry for credential lifecycle management
0 reputation · 21 Jan 2022, 03:32 UTC
0 reputation · 21 Jan 2022, 03:32 UTC
NixOS users.users. provides declarative configuration for home directories, shells, and group membership, but does not expose a passwordExpiry attribute. This creates a mismatch between declared system state and the actual /etc/shadow expiry field, requiring administrators to manage credential lifecycle through external PAM modules or ad hoc scripts. The absence of a built‑in option means that password expiry policies are not version‑controlled alongside the Nix expression, and upgrade paths may silently preserve or reset shadow settings depending on the release series. Additionally, removing a user from the NixOS configuration does not automatically purge the corresponding system user, potentially leaving orphaned accounts with unchanged password policies.
Current NixOS releases vary in how they handle shadow file persistence during configuration rebuilds, and no official option guarantees consistent exp field behavior across versions. Administrators relying on PAM for expiry reporting face the risk of policy conflicts that could lock out users if password aging rules contradict the system's declarative intent. These uncertainties complicate auditing and automated compliance checks for credential rotation schedules.
What mechanisms can NixOS provide to declaratively set or reset the /etc/shadow exp field for system users? How should password expiry policies be version‑controlled and propagated through the Nix store without manual intervention? Is there a recommended approach for auditing and reconciling existing shadow entries against the declared user configuration?
34775 reputation · 21 Jan 2022, 06:50 UTC
There is no upstream NixOS option that maps to the /etc/shadow aging fields (last change, max days, expiry date). The users.users.<name> attribute set covers passwords (hashedPassword, initialPassword), shells, groups, and SSH keys, but not the fields managed by chage. The practical mechanism today is to run chage idempotently from your configuration — via system.activationScripts or a systemd oneshot — so the policy lives in the Nix expression even though the module doesn't own the field.
Confirmed behavior: NixOS activation only writes the shadow fields the user module knows about. Expiry values set manually with chage are therefore invisible to your configuration, and whether they survive a rebuild untouched has varied across nixpkgs releases and depends on users.mutableUsers. Do not assume either "rebuilds preserve expiry" or "rebuilds reset it" — verify on your pinned release (see below). Also confirmed: removing a user from the configuration does not delete the account; the shadow entry, including its aging fields, remains until you run userdel.
A minimal activation script keeps expiry declarative:
system.activationScripts.userExpiry =
lib.stringAfter [ "users" ] ''
for u in alice bob; do
if id "$u" >/dev/null 2>&1; then
${pkgs.shadow}/bin/chage -M 90 -W 7 "$u"
fi
done
'';
Two details matter. First, the id guard handles first activation, when the user may not exist yet depending on script ordering. Second, chage -M is naturally idempotent for the max-days field, but avoid chage -d (last-change date) in a script that runs on every rebuild — it would reset the aging clock each switch. If you need absolute expiry dates, chage -E 2027-01-01 is also idempotent. For larger fleets, generate the loop from an attribute set in your config so users and their policies are declared in one place and reviewed in the same diff.
If you already set users.mutableUsers = false with hashedPassword, note that this makes password content declarative but still does not reset aging fields — drift in expiry persists unless you script it as above.
Build the audit from the same source of truth. A periodic systemd timer (or a CI job against a built system) can compare actual state against expected values:
getent shadow alice # field 8 is the expiry date (days since epoch)
chage -l alice # human-readable view
A simple check script can parse getent shadow output and fail if field 8 (or field 5, max days) differs from the value in your Nix expression. Run it as a systemd.timers entry with OnCalendar=daily and wire failures into your existing alerting. For orphaned accounts, reconcile by diffing getent passwd against the users declared in your configuration and removing stragglers with userdel — there is no automatic purge.
nixos-option users.users, or read the users module in your pinned nixpkgs.getent shadow <user> before and after nixos-rebuild switch to observe whether expiry fields change on your release./etc/shadow to confirm idempotency.One caveat worth flagging: if PAM modules (e.g., pam_unix with aging enforcement) also act on these fields, make sure the scripted policy and PAM behavior agree, or you risk contradictory lockout behavior. The exact interaction depends on your PAM stack, so test a forced-expiry account in a VM generation before rolling out.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.