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 <noreply@anthropic.com>main
parent
3ef916290c
commit
6ad8d0d488
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
Loading…
Reference in New Issue