From 6ad8d0d488f569f53c54162ebf208631f4921636 Mon Sep 17 00:00:00 2001 From: The_miro Date: Wed, 20 May 2026 16:18:48 +0200 Subject: [PATCH] feat(ansipa): add no_local_users device policy to lock all local account passwords Adds a new host group policy `no_local_users` that locks the passwords of root and all local users (UID >= 1000) via `passwd -l`, ensuring only FreeIPA domain accounts with centrally-managed sudo rules can authenticate and gain elevated privileges. Leaving the group reverts by unlocking every account tracked in the state file. Updates docs with group reference entry and Local User Lockdown section. Co-Authored-By: Claude Sonnet 4.6 --- docs/md/freeipa-ansible.md | 14 ++++ .../ansible/ansipa-enforce-policies.sh | 65 ++++++++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/docs/md/freeipa-ansible.md b/docs/md/freeipa-ansible.md index 83a6516..815581e 100644 --- a/docs/md/freeipa-ansible.md +++ b/docs/md/freeipa-ansible.md @@ -277,6 +277,7 @@ Keys on the SMB share are accessible only to `KeyAdmin` group members (see [SMB | `policy-timeshift-backup` | `ansipa-enforce-policies.sh` | Daily Timeshift snapshot at 03:00 | | `policy-security-scan` | `ansipa-enforce-policies.sh` | Daily ClamAV + rkhunter + chkrootkit scan + SMB upload | | `policy-scan-notify` | `ansipa-enforce-policies.sh` | Fetch server alerts, notify user every 10 min until acknowledged | +| `no_local_users` | `ansipa-enforce-policies.sh` | Lock passwords for root and all local users (UID ≥ 1000); reverted on leave | --- @@ -329,6 +330,19 @@ Client (policy-scan-notify, every 10 min via systemd timer) - `samba-client` is installed automatically by `deploy-ansipa-policies.yml`. - SMB credentials are written to `/etc/ansipa-smb.creds` (root-only, `0600`). +### Local User Lockdown + +Adding a host to the `no_local_users` group locks the password of every local account — root (UID 0) and all regular users (UID ≥ 1000) — using `passwd -l`. Accounts whose passwords are already locked (`!` or `*` prefix in shadow) are left untouched and are not tracked. + +State is persisted in `/var/lib/ansipa-policies/no-local-users` (one username per line). Only accounts that were actively unlocked at apply time are written to this file, so the revert step only unlocks what was changed. + +| Action | Effect | +|--------|--------| +| Join `no_local_users` | `passwd -l` on root + all UID ≥ 1000 local accounts | +| Leave `no_local_users` | `passwd -u` on every account listed in the state file | + +**Interaction with FreeIPA sudo:** Domain accounts in the `sudoers` or `sudo-nopasswd` FreeIPA groups retain full sudo access via SSSD — local password lockdown does not affect them. Ensure at least one domain admin has sudo before adding a host to this group. + --- ## LUKS Key Flow diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh index 501e80d..ba5e38b 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh @@ -17,6 +17,11 @@ # If a unit appears in both enable and disable groups it is skipped. # policy-timeshift-backup Enforce a daily Timeshift snapshot (requires timeshift installed) # policy-security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans +# no_local_users Lock passwords for root and all local users (UID >= 1000) so +# that only FreeIPA domain accounts with centrally-managed sudo +# rules can authenticate and gain elevated privileges. +# Leaving the group reverts: every account locked by this policy +# is unlocked on the next run. # # Notes: # - Install scan tools first: add the host to ansipa-module-anti-malware. @@ -54,6 +59,7 @@ ACTIVE_DAEMON_DISABLE=() WANT_TIMESHIFT_BACKUP=false WANT_SECURITY_SCAN=false WANT_SCAN_NOTIFY=false +WANT_NO_LOCAL_USERS=false if [[ -n "$RAW_GROUPS" ]]; then while IFS=',' read -ra GRP_ARRAY; do @@ -66,6 +72,7 @@ if [[ -n "$RAW_GROUPS" ]]; then policy-timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;; policy-security-scan) WANT_SECURITY_SCAN=true ;; policy-scan-notify) WANT_SCAN_NOTIFY=true ;; + no_local_users) WANT_NO_LOCAL_USERS=true ;; esac done done <<< "$RAW_GROUPS" @@ -74,7 +81,8 @@ fi log "Active policies — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \ "| daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none} | daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \ "| timeshift-backup: $WANT_TIMESHIFT_BACKUP" \ - "| security-scan: $WANT_SECURITY_SCAN | scan-notify: $WANT_SCAN_NOTIFY" + "| security-scan: $WANT_SECURITY_SCAN | scan-notify: $WANT_SCAN_NOTIFY" \ + "| no-local-users: $WANT_NO_LOCAL_USERS" # ── Helpers ─────────────────────────────────────────────────────────────────── in_active_list() { @@ -444,4 +452,59 @@ else > "$DAEMON_DISABLE_STATE" fi +# ── No-local-users policy ────────────────────────────────────────────────────── +# no_local_users: lock the passwords of root and all local users (UID >= 1000) +# so that only FreeIPA domain accounts with centrally-managed sudo rules can +# authenticate and gain elevated privileges. +# Leaving the group reverts: every account locked by this policy is unlocked. + +NO_LOCAL_USERS_STATE="$STATE_DIR/no-local-users" + +_apply_no_local_users() { + log "Applying no_local_users policy — locking local account passwords" + [[ -f "$NO_LOCAL_USERS_STATE" ]] || touch "$NO_LOCAL_USERS_STATE" + + while IFS=: read -r uname _ uid _; do + [[ "$uid" =~ ^[0-9]+$ ]] || continue + { [[ "$uid" == "0" ]] || [[ "$uid" -ge 1000 ]]; } || continue + + # Skip accounts already tracked (locked on a previous run) + grep -qxF "$uname" "$NO_LOCAL_USERS_STATE" 2>/dev/null && continue + + # Lock only accounts that currently have a real (unlocked) password hash + local hash + hash=$(getent shadow "$uname" 2>/dev/null | cut -d: -f2 || true) + [[ -z "$hash" || "$hash" == '!'* || "$hash" == '*'* ]] && continue + + if passwd -l "$uname" &>/dev/null; then + echo "$uname" >> "$NO_LOCAL_USERS_STATE" + log "Locked local account: $uname" + else + warn "Failed to lock local account: $uname" + fi + done < /etc/passwd +} + +_revert_no_local_users() { + [[ -f "$NO_LOCAL_USERS_STATE" ]] || return 0 + log "Reverting no_local_users policy — unlocking previously locked accounts" + while IFS= read -r uname; do + [[ -z "$uname" ]] && continue + if passwd -u "$uname" &>/dev/null; then + log "Unlocked local account: $uname" + else + warn "Failed to unlock local account: $uname (may have been removed)" + fi + done < "$NO_LOCAL_USERS_STATE" + > "$NO_LOCAL_USERS_STATE" +} + +if [[ "$WANT_NO_LOCAL_USERS" == true ]]; then + _apply_no_local_users +else + if [[ -f "$NO_LOCAL_USERS_STATE" ]] && [[ -s "$NO_LOCAL_USERS_STATE" ]]; then + _revert_no_local_users + fi +fi + log "Policy enforcement complete."