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."