feat(ansipa): unify FreeIPA group naming with dev_* / usr_* prefixes

All ansipa host/user group names now follow a consistent prefix scheme:

  dev_mod_<name>          — dotfiles module install (was ansipa-module-)
  dev_fp_<app-id>         — Flatpak install (was fp_install_)
  dev_pkg_<package>       — native package install (was ansipa-install-)
  dev_daemon-enable-<u>   — service enable policy (was policy-daemon-enable-)
  dev_daemon-disable-<u>  — service disable policy (was policy-daemon-disable-)
  dev_timeshift-backup    — backup policy (was policy-timeshift-backup)
  dev_security-scan       — scan policy (was policy-security-scan)
  dev_no-local-users      — auth lockdown (was no_local_users)
  dev_local-sudo-<user>   — per-device sudo grant (was local_sudo_)
  usr_block-binary-<name> — per-user binary block (was policy-block-binary-)
  usr_scan-notify         — per-user alert notification (was policy-scan-notify)

Also adds a JSON state manifest (manifest.json) to ansipa-install-modules
and tightens the FreeIPA enrollment guard to check /etc/ipa/default.conf.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LcnnA1whUwQkDv1omsgh9Y
main
Amir Alexander Abdelbaki 2026-06-26 11:48:24 +02:00
parent 2be85739b5
commit 9289f01965
11 changed files with 218 additions and 106 deletions

View File

@ -5,41 +5,40 @@
# leaving the group removes it on the next run (every 30 min via systemd timer). # leaving the group removes it on the next run (every 30 min via systemd timer).
# #
# Host-group naming conventions (device policies — applied to the whole machine): # Host-group naming conventions (device policies — applied to the whole machine):
# policy-daemon-enable-<unit> Ensure <unit> is enabled and running (systemctl enable --now). # dev_daemon-enable-<unit> Ensure <unit> is enabled and running (systemctl enable --now).
# Leaving the group reverts: service is disabled and stopped. # Leaving the group reverts: service is disabled and stopped.
# policy-daemon-disable-<unit> Ensure <unit> is disabled and stopped (systemctl disable --now). # dev_daemon-disable-<unit> Ensure <unit> is disabled and stopped (systemctl disable --now).
# Leaving the group reverts: service is re-enabled and started. # Leaving the group reverts: service is re-enabled and started.
# <unit> may omit the .service suffix; all systemd unit types work. # <unit> may omit the .service suffix; all systemd unit types work.
# If a unit appears in both enable and disable groups it is skipped. # If a unit appears in both enable and disable groups it is skipped.
# policy-timeshift-backup Enforce a daily Timeshift snapshot (requires timeshift installed) # dev_timeshift-backup Enforce a daily Timeshift snapshot (requires timeshift installed)
# policy-security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans # dev_security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans
# policy-scan-notify (see User-group section below — treated as a user group) # dev_no-local-users Lock passwords for root and all local users (UID >= 1000) so
# no_local_users Lock passwords for root and all local users (UID >= 1000) so # that only FreeIPA domain accounts with centrally-managed sudo
# that only FreeIPA domain accounts with centrally-managed sudo # rules can authenticate and gain elevated privileges.
# rules can authenticate and gain elevated privileges. # Leaving the group reverts: every account locked by this policy
# Leaving the group reverts: every account locked by this policy # is unlocked on the next run.
# is unlocked on the next run. # dev_local-sudo-<username> Grant <username> full sudo on this specific device by adding
# local_sudo_<username> Grant <username> full sudo on this specific device by adding # them to the local sudoers drop-in. Leaving the group removes
# them to the local sudoers drop-in. Leaving the group removes # the drop-in on the next run.
# the drop-in on the next run.
# #
# User-group naming conventions (per-user policies — follow the user across devices): # User-group naming conventions (per-user policies — follow the user across devices):
# policy-block-binary-<name> Prevent members of this FreeIPA user group from running <name> # usr_block-binary-<name> Prevent members of this FreeIPA user group from running <name>
# on any enrolled host. Use __ in place of . to support Flatpak # on any enrolled host. Use __ in place of . to support Flatpak
# app IDs (e.g. policy-block-binary-org__gimp__Gimp blocks the # app IDs (e.g. usr_block-binary-org__gimp__Gimp blocks the
# Flatpak org.gimp.Gimp). Enforced via a PATH-priority wrapper # Flatpak org.gimp.Gimp). Enforced via a PATH-priority wrapper
# that checks group membership at runtime via SSSD/id(1). # that checks group membership at runtime via SSSD/id(1).
# Removing the user group from FreeIPA reverts the wrapper. # Removing the user group from FreeIPA reverts the wrapper.
# policy-scan-notify Members receive scan alert notifications on every enrolled device # usr_scan-notify Members receive scan alert notifications on every enrolled device
# they log into. The fetch-alerts timer is installed fleet-wide # they log into. The fetch-alerts timer is installed fleet-wide
# when the group exists; the notification daemon starts on login # when the group exists; the notification daemon starts on login
# only for group members (checked via id(1) / SSSD). # only for group members (checked via id(1) / SSSD).
# Deleting the IPA user group removes the timer and profile.d # Deleting the IPA user group removes the timer and profile.d
# snippet on the next enforcer run. # snippet on the next enforcer run.
# #
# Notes: # Notes:
# - Install scan tools first: add the host to ansipa-module-anti-malware. # - Install scan tools first: add the host to dev_mod_anti-malware.
# - Configure Timeshift (type + target device) before enabling policy-timeshift-backup. # - Configure Timeshift (type + target device) before enabling dev_timeshift-backup.
set -euo pipefail set -euo pipefail
@ -79,40 +78,40 @@ if [[ -n "$RAW_GROUPS" ]]; then
for g in "${GRP_ARRAY[@]}"; do for g in "${GRP_ARRAY[@]}"; do
g="${g// /}" g="${g// /}"
case "$g" in case "$g" in
policy-daemon-enable-*) ACTIVE_DAEMON_ENABLE+=("${g#policy-daemon-enable-}") ;; dev_daemon-enable-*) ACTIVE_DAEMON_ENABLE+=("${g#dev_daemon-enable-}") ;;
policy-daemon-disable-*) ACTIVE_DAEMON_DISABLE+=("${g#policy-daemon-disable-}") ;; dev_daemon-disable-*) ACTIVE_DAEMON_DISABLE+=("${g#dev_daemon-disable-}") ;;
policy-timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;; dev_timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;;
policy-security-scan) WANT_SECURITY_SCAN=true ;; dev_security-scan) WANT_SECURITY_SCAN=true ;;
no_local_users) WANT_NO_LOCAL_USERS=true ;; dev_no-local-users) WANT_NO_LOCAL_USERS=true ;;
local_sudo_*) ACTIVE_LOCAL_SUDO_USERS+=("${g#local_sudo_}") ;; dev_local-sudo-*) ACTIVE_LOCAL_SUDO_USERS+=("${g#dev_local-sudo-}") ;;
esac esac
done done
done <<< "$RAW_GROUPS" done <<< "$RAW_GROUPS"
fi fi
# ── Fetch user-group-based binary block policies from FreeIPA ───────────────── # ── Fetch user-group-based binary block policies from FreeIPA ─────────────────
# policy-block-binary-<name> groups are FreeIPA *user* groups — membership follows # usr_block-binary-<name> groups are FreeIPA *user* groups — membership follows
# the user to every enrolled host rather than being tied to a device. # the user to every enrolled host rather than being tied to a device.
ACTIVE_BLOCK_BINARIES=() ACTIVE_BLOCK_BINARIES=()
ACTIVE_BLOCK_IPA_GROUPS=() ACTIVE_BLOCK_IPA_GROUPS=()
_BLOCK_LIST=$(ipa group-find --pkey-only 2>/dev/null \ _BLOCK_LIST=$(ipa group-find --pkey-only 2>/dev/null \
| awk '/Group name:/ {print $NF}' \ | awk '/Group name:/ {print $NF}' \
| grep "^policy-block-binary-" | sort -u || true) | grep "^usr_block-binary-" | sort -u || true)
while IFS= read -r _grp; do while IFS= read -r _grp; do
[[ -z "$_grp" ]] && continue [[ -z "$_grp" ]] && continue
_raw="${_grp#policy-block-binary-}" _raw="${_grp#usr_block-binary-}"
ACTIVE_BLOCK_BINARIES+=("${_raw//__/.}") ACTIVE_BLOCK_BINARIES+=("${_raw//__/.}")
ACTIVE_BLOCK_IPA_GROUPS+=("$_grp") ACTIVE_BLOCK_IPA_GROUPS+=("$_grp")
done <<< "$_BLOCK_LIST" done <<< "$_BLOCK_LIST"
unset _BLOCK_LIST _grp _raw unset _BLOCK_LIST _grp _raw
# ── Fetch user-group-based scan-notify policy from FreeIPA ──────────────────── # ── Fetch user-group-based scan-notify policy from FreeIPA ────────────────────
# policy-scan-notify is a FreeIPA *user* group — membership follows the user to # usr_scan-notify is a FreeIPA *user* group — membership follows the user to
# every enrolled host. Install the fetch-alerts timer on any device where the # every enrolled host. Install the fetch-alerts timer on any device where the
# group exists; the profile.d snippet gates daemon start on runtime membership. # group exists; the profile.d snippet gates daemon start on runtime membership.
if ipa group-show policy-scan-notify >/dev/null 2>&1; then if ipa group-show usr_scan-notify >/dev/null 2>&1; then
WANT_SCAN_NOTIFY=true WANT_SCAN_NOTIFY=true
fi fi
@ -134,7 +133,7 @@ _in_block_list() {
# ── Binary blocking (user-based) ────────────────────────────────────────────── # ── Binary blocking (user-based) ──────────────────────────────────────────────
# A PATH-priority wrapper is installed in /usr/local/bin/ for every binary named # A PATH-priority wrapper is installed in /usr/local/bin/ for every binary named
# by a policy-block-binary-* FreeIPA *user* group. The wrapper checks the # by a usr_block-binary-* FreeIPA *user* group. The wrapper checks the
# caller's group membership at runtime (via id + SSSD) and only blocks members; # caller's group membership at runtime (via id + SSSD) and only blocks members;
# non-members are transparently passed through to the real binary. # non-members are transparently passed through to the real binary.
# __ in the group suffix decodes to . so Flatpak app IDs are fully supported. # __ in the group suffix decodes to . so Flatpak app IDs are fully supported.
@ -195,11 +194,11 @@ TIMESHIFT_CRON="$CRON_DIR/ansipa-timeshift-backup"
if [[ "$WANT_TIMESHIFT_BACKUP" == true ]]; then if [[ "$WANT_TIMESHIFT_BACKUP" == true ]]; then
if [[ ! -f "$TIMESHIFT_CRON" ]]; then if [[ ! -f "$TIMESHIFT_CRON" ]]; then
if ! command -v timeshift &>/dev/null; then if ! command -v timeshift &>/dev/null; then
warn "timeshift not found — add host to ansipa-module-timeshift first. Cron will be installed anyway." warn "timeshift not found — add host to dev_mod_timeshift first. Cron will be installed anyway."
fi fi
log "Enabling daily Timeshift backups" log "Enabling daily Timeshift backups"
cat > "$TIMESHIFT_CRON" <<'CRON' cat > "$TIMESHIFT_CRON" <<'CRON'
# ansipa-policy-timeshift-backup: managed by ansipa-enforce-policies — do not edit manually. # ansipa-dev_timeshift-backup: managed by ansipa-enforce-policies — do not edit manually.
# Timeshift must be configured on this host (type + target device) before snapshots work. # Timeshift must be configured on this host (type + target device) before snapshots work.
0 3 * * * root /usr/bin/timeshift --create --comments "ansipa-daily" --tags D 2>&1 | logger -t timeshift-backup 0 3 * * * root /usr/bin/timeshift --create --comments "ansipa-daily" --tags D 2>&1 | logger -t timeshift-backup
CRON CRON
@ -208,7 +207,7 @@ CRON
else else
if [[ -f "$TIMESHIFT_CRON" ]]; then if [[ -f "$TIMESHIFT_CRON" ]]; then
rm -f "$TIMESHIFT_CRON" rm -f "$TIMESHIFT_CRON"
log "Removed Timeshift backup cron (host left policy-timeshift-backup group)" log "Removed Timeshift backup cron (host left dev_timeshift-backup group)"
fi fi
fi fi
@ -263,8 +262,8 @@ SCAN
if [[ ! -f "$SCAN_CRON" ]]; then if [[ ! -f "$SCAN_CRON" ]]; then
log "Enabling daily security scans (ClamAV / rkhunter / chkrootkit)" log "Enabling daily security scans (ClamAV / rkhunter / chkrootkit)"
cat > "$SCAN_CRON" <<'CRON' cat > "$SCAN_CRON" <<'CRON'
# ansipa-policy-security-scan: managed by ansipa-enforce-policies — do not edit manually. # ansipa-dev_security-scan: managed by ansipa-enforce-policies — do not edit manually.
# Install scan tools by adding the host to the ansipa-module-anti-malware group. # Install scan tools by adding the host to the dev_mod_anti-malware group.
0 2 * * * root /usr/local/bin/ansipa-security-scan.sh 0 2 * * * root /usr/local/bin/ansipa-security-scan.sh
CRON CRON
chmod 644 "$SCAN_CRON" chmod 644 "$SCAN_CRON"
@ -273,12 +272,12 @@ else
if [[ -f "$SCAN_CRON" ]]; then if [[ -f "$SCAN_CRON" ]]; then
rm -f "$SCAN_CRON" rm -f "$SCAN_CRON"
rm -f "$SCAN_SCRIPT" rm -f "$SCAN_SCRIPT"
log "Removed security scan policy (host left policy-security-scan group)" log "Removed security scan policy (host left dev_security-scan group)"
fi fi
fi fi
# ── Scan notification daemon ────────────────────────────────────────────────── # ── Scan notification daemon ──────────────────────────────────────────────────
# policy-scan-notify is a FreeIPA *user* group (not a host group). The fetch- # usr_scan-notify is a FreeIPA *user* group (not a host group). The fetch-
# alerts timer runs fleet-wide on any host where the group exists; the profile.d # alerts timer runs fleet-wide on any host where the group exists; the profile.d
# snippet starts the notification daemon on login only for group members # snippet starts the notification daemon on login only for group members
# (checked via id(1) / SSSD so no IPA query is needed at login time). # (checked via id(1) / SSSD so no IPA query is needed at login time).
@ -335,11 +334,11 @@ UNIT
log "Installing /etc/profile.d/ansipa-notify.sh" log "Installing /etc/profile.d/ansipa-notify.sh"
cat > "$NOTIFY_PROFILED" <<'PROFILED' cat > "$NOTIFY_PROFILED" <<'PROFILED'
# ansipa-notify: launch the scan alert notification daemon on login for # ansipa-notify: launch the scan alert notification daemon on login for
# members of the policy-scan-notify FreeIPA user group. # members of the usr_scan-notify FreeIPA user group.
# Managed by ansipa-enforce-policies — do not edit manually. # Managed by ansipa-enforce-policies — do not edit manually.
_NOTIFY_DAEMON=/usr/local/bin/ansipa-scan-notify.sh _NOTIFY_DAEMON=/usr/local/bin/ansipa-scan-notify.sh
if [[ -x "$_NOTIFY_DAEMON" ]] && \ if [[ -x "$_NOTIFY_DAEMON" ]] && \
id -nG 2>/dev/null | grep -qw "policy-scan-notify" && \ id -nG 2>/dev/null | grep -qw "usr_scan-notify" && \
! pgrep -u "$(id -u)" -f "ansipa-scan-notify" >/dev/null 2>&1; then ! pgrep -u "$(id -u)" -f "ansipa-scan-notify" >/dev/null 2>&1; then
"$_NOTIFY_DAEMON" & "$_NOTIFY_DAEMON" &
disown disown
@ -353,7 +352,7 @@ else
systemctl disable --now ansipa-fetch-alerts.timer 2>/dev/null || true systemctl disable --now ansipa-fetch-alerts.timer 2>/dev/null || true
rm -f "$FETCH_SVC" "$FETCH_TIMER" rm -f "$FETCH_SVC" "$FETCH_TIMER"
systemctl daemon-reload systemctl daemon-reload
log "Removed ansipa-fetch-alerts timer (policy-scan-notify user group no longer exists)" log "Removed ansipa-fetch-alerts timer (usr_scan-notify user group no longer exists)"
fi fi
if [[ -f "$NOTIFY_PROFILED" ]]; then if [[ -f "$NOTIFY_PROFILED" ]]; then
rm -f "$NOTIFY_PROFILED" rm -f "$NOTIFY_PROFILED"
@ -362,9 +361,9 @@ else
fi fi
# ── Daemon enable / disable ─────────────────────────────────────────────────── # ── Daemon enable / disable ───────────────────────────────────────────────────
# policy-daemon-enable-<unit>: ensure the unit is enabled and running. # dev_daemon-enable-<unit>: ensure the unit is enabled and running.
# Leaving the group reverts: unit is disabled and stopped. # Leaving the group reverts: unit is disabled and stopped.
# policy-daemon-disable-<unit>: ensure the unit is disabled and stopped. # dev_daemon-disable-<unit>: ensure the unit is disabled and stopped.
# Leaving the group reverts: unit is re-enabled and started. # Leaving the group reverts: unit is re-enabled and started.
# <unit> may omit the .service suffix; systemd accepts both forms. # <unit> may omit the .service suffix; systemd accepts both forms.
# Conflicts (unit in both lists): logged as a warning, unit is left untouched. # Conflicts (unit in both lists): logged as a warning, unit is left untouched.
@ -448,7 +447,7 @@ else
fi fi
# ── Per-device local sudo grants ────────────────────────────────────────────── # ── Per-device local sudo grants ──────────────────────────────────────────────
# local_sudo_<username>: write a sudoers drop-in granting <username> full sudo on # dev_local-sudo-<username>: write a sudoers drop-in granting <username> full sudo on
# this specific device. The drop-in is removed when the host leaves the group. # this specific device. The drop-in is removed when the host leaves the group.
LOCAL_SUDO_DIR="/etc/sudoers.d" LOCAL_SUDO_DIR="/etc/sudoers.d"
@ -465,7 +464,7 @@ for _USER in "${ACTIVE_LOCAL_SUDO_USERS[@]}"; do
grep -qxF "$_USER" "$LOCAL_SUDO_STATE" 2>/dev/null || echo "$_USER" >> "$LOCAL_SUDO_STATE" grep -qxF "$_USER" "$LOCAL_SUDO_STATE" 2>/dev/null || echo "$_USER" >> "$LOCAL_SUDO_STATE"
done done
# Revoke sudo for users no longer in any active local_sudo_* group. # Revoke sudo for users no longer in any active dev_local-sudo-* group.
while IFS= read -r _OLD_USER; do while IFS= read -r _OLD_USER; do
[[ -z "$_OLD_USER" ]] && continue [[ -z "$_OLD_USER" ]] && continue
_still_active=false _still_active=false
@ -476,7 +475,7 @@ while IFS= read -r _OLD_USER; do
_DROPIN="$LOCAL_SUDO_DIR/ansipa-local-sudo-${_OLD_USER}" _DROPIN="$LOCAL_SUDO_DIR/ansipa-local-sudo-${_OLD_USER}"
if [[ -f "$_DROPIN" ]]; then if [[ -f "$_DROPIN" ]]; then
rm -f "$_DROPIN" rm -f "$_DROPIN"
log "Revoked local sudo for $_OLD_USER (host left local_sudo_$_OLD_USER group)" log "Revoked local sudo for $_OLD_USER (host left dev_local-sudo-$_OLD_USER group)"
fi fi
fi fi
done < "$LOCAL_SUDO_STATE" done < "$LOCAL_SUDO_STATE"
@ -490,7 +489,7 @@ fi
unset _USER _DROPIN _OLD_USER _still_active _U unset _USER _DROPIN _OLD_USER _still_active _U
# ── No-local-users policy ────────────────────────────────────────────────────── # ── No-local-users policy ──────────────────────────────────────────────────────
# no_local_users: lock the passwords of root and all local users (UID >= 1000) # dev_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 # so that only FreeIPA domain accounts with centrally-managed sudo rules can
# authenticate and gain elevated privileges. # authenticate and gain elevated privileges.
# Leaving the group reverts: every account locked by this policy is unlocked. # Leaving the group reverts: every account locked by this policy is unlocked.

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ansipa-fetch-alerts.sh — fetch security alerts from the server SMB share. # ansipa-fetch-alerts.sh — fetch security alerts from the server SMB share.
# Runs as root every 10 minutes via ansipa-fetch-alerts.timer (policy-scan-notify). # Runs as root every 10 minutes via ansipa-fetch-alerts.timer (usr_scan-notify).
# #
# For each alert on the server that hasn't been acknowledged yet: # For each alert on the server that hasn't been acknowledged yet:
# - Downloads it to ~/administration/<hostname>/ for every active login session. # - Downloads it to ~/administration/<hostname>/ for every active login session.

View File

@ -1,5 +1,5 @@
[Unit] [Unit]
Description=Install Flatpaks based on FreeIPA fp_install_* groups Description=Install Flatpaks based on FreeIPA dev_fp_* groups
After=network-online.target sssd.service After=network-online.target sssd.service
Wants=network-online.target Wants=network-online.target

View File

@ -2,18 +2,18 @@
# ansipa-install-flatpaks.sh — install Flatpak apps based on FreeIPA group membership. # ansipa-install-flatpaks.sh — install Flatpak apps based on FreeIPA group membership.
# #
# IPA group naming convention (dots encoded as double underscores): # IPA group naming convention (dots encoded as double underscores):
# fp_install_org__mozilla__firefox → installs org.mozilla.firefox # dev_fp_org__mozilla__firefox → installs org.mozilla.firefox
# fp_install_com__spotify__Client → installs com.spotify.Client # dev_fp_com__spotify__Client → installs com.spotify.Client
# fp_install_io__missioncenter__MissionCenter → installs io.missioncenter.MissionCenter # dev_fp_io__missioncenter__MissionCenter → installs io.missioncenter.MissionCenter
# #
# Decoding: strip "fp_install_" prefix, then replace every __ with a dot. # Decoding: strip "dev_fp_" prefix, then replace every __ with a dot.
# Single underscores in Flatpak IDs are preserved as-is. # Single underscores in Flatpak IDs are preserved as-is.
# #
# Scope: system-wide (--system), runs as root via systemd service. # Scope: system-wide (--system), runs as root via systemd service.
set -e set -e
PREFIX="fp_install_" PREFIX="dev_fp_"
# ── Preflight ───────────────────────────────────────────────────────────────── # ── Preflight ─────────────────────────────────────────────────────────────────
if ! command -v flatpak &>/dev/null; then if ! command -v flatpak &>/dev/null; then
@ -35,7 +35,7 @@ if ! flatpak remote-list --system | awk '{print $1}' | grep -qx "flathub"; then
https://dl.flathub.org/repo/flathub.flatpakrepo https://dl.flathub.org/repo/flathub.flatpakrepo
fi fi
# ── Discover IPA groups matching fp_install_* ───────────────────────────────── # ── Discover IPA groups matching dev_fp_* ────────────────────────────────────
# ipa group-find --pkey-only outputs one group name per line (possibly indented). # ipa group-find --pkey-only outputs one group name per line (possibly indented).
# $NF captures the name regardless of leading label text. # $NF captures the name regardless of leading label text.
IPA_GROUPS=$(ipa group-find --pkey-only 2>/dev/null \ IPA_GROUPS=$(ipa group-find --pkey-only 2>/dev/null \
@ -49,7 +49,7 @@ if [[ -z "$IPA_GROUPS" ]]; then
fi fi
# ── Decode group names → Flatpak application IDs ───────────────────────────── # ── Decode group names → Flatpak application IDs ─────────────────────────────
# 1. Strip the fp_install_ prefix # 1. Strip the dev_fp_ prefix
# 2. Replace every __ with a literal dot # 2. Replace every __ with a literal dot
DESIRED_FLATPAKS=() DESIRED_FLATPAKS=()
while IFS= read -r G; do while IFS= read -r G; do

View File

@ -1,5 +1,5 @@
[Unit] [Unit]
Description=Apply setup modules based on FreeIPA ansipa-module-* host groups Description=Apply setup modules based on FreeIPA dev_mod_* host groups
After=network-online.target sssd.service After=network-online.target sssd.service
Wants=network-online.target Wants=network-online.target

View File

@ -3,11 +3,15 @@
# FreeIPA host group membership. # FreeIPA host group membership.
# #
# Host groups follow the naming convention: # Host groups follow the naming convention:
# ansipa-module-<name> e.g. ansipa-module-docker, ansipa-module-ollama # dev_mod_<name> e.g. dev_mod_docker, dev_mod_ollama
# #
# When this host is a member of such a group, the corresponding module # When this host is a member of such a group, the corresponding module
# script in /usr/local/lib/ansipa-modules/<name>.sh is executed (once, # script in /usr/local/lib/ansipa-modules/<name>.sh is executed once.
# stamped in /var/lib/ansipa-modules/). # Completion is tracked via a stamp file in STATE_DIR and reflected in
# a JSON manifest at STATE_DIR/manifest.json.
#
# Guard: the script exits immediately if the FreeIPA client is not enrolled
# (/etc/ipa/default.conf must exist and the ipa command must be available).
# #
# Configuration: /etc/ansipa-modules.conf # Configuration: /etc/ansipa-modules.conf
# ANSIPA_USER=<username> non-root user for AUR helper (yay) # ANSIPA_USER=<username> non-root user for AUR helper (yay)
@ -22,15 +26,112 @@ CONFIG=/etc/ansipa-modules.conf
ANSIPA_USER="${ANSIPA_USER:-}" ANSIPA_USER="${ANSIPA_USER:-}"
MODULES_DIR="${MODULES_DIR:-/usr/local/lib/ansipa-modules}" MODULES_DIR="${MODULES_DIR:-/usr/local/lib/ansipa-modules}"
STATE_DIR="${STATE_DIR:-/var/lib/ansipa-modules}" STATE_DIR="${STATE_DIR:-/var/lib/ansipa-modules}"
PREFIX="ansipa-module-" MANIFEST="$STATE_DIR/manifest.json"
PREFIX="dev_mod_"
LOG_TAG="ansipa-modules" LOG_TAG="ansipa-modules"
log() { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; } log() { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; }
warn() { echo "[$LOG_TAG][WARN] $*" >&2; logger -t "$LOG_TAG" "WARN: $*" 2>/dev/null || true; } warn() { echo "[$LOG_TAG][WARN] $*" >&2; logger -t "$LOG_TAG" "WARN: $*" 2>/dev/null || true; }
# ── Guard: only proceed if the ansipa/FreeIPA client is enrolled ──────────────
# /etc/ipa/default.conf is written by ipa-client-install on successful enrollment.
# Without it, there is no domain to query and no host groups to check.
if [[ ! -f /etc/ipa/default.conf ]]; then
log "FreeIPA client not enrolled (/etc/ipa/default.conf absent) — skipping."
exit 0
fi
if ! command -v ipa &>/dev/null; then
warn "ipa command not found — FreeIPA packages not installed. Exiting."
exit 0
fi
# ── State directories ─────────────────────────────────────────────────────────
mkdir -p "$STATE_DIR"
HOST_FQDN=$(hostname -f 2>/dev/null || hostname)
# ── Manifest helpers (python3) ────────────────────────────────────────────────
# The manifest is a JSON file that records the status and timestamps for every
# module this host has encountered. It is updated atomically (tmp + rename).
# Stamp files (.done) remain the authoritative "is this installed" check;
# the manifest is derived from them and used for reporting/auditing.
_manifest_update() {
# Args: <module> <status> status ∈ {installed, failed, pending}
local module="$1" status="$2"
local ts; ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
python3 - "$MANIFEST" "$module" "$status" "$HOST_FQDN" "$ts" <<'PYEOF'
import json, sys, os
manifest_path, module, status, hostname, ts = sys.argv[1:6]
try:
with open(manifest_path) as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
data = {}
data["hostname"] = hostname
data.setdefault("modules", {})
data["last_run"] = ts
entry = data["modules"].setdefault(module, {})
entry["status"] = status
if status == "installed":
# Preserve original installed_at if already recorded
entry.setdefault("installed_at", ts)
else:
entry["last_attempt"] = ts
tmp = manifest_path + ".tmp"
with open(tmp, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
os.rename(tmp, manifest_path)
PYEOF
}
_manifest_touch_run() {
# Update last_run and hostname without touching any module entries
local ts; ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
python3 - "$MANIFEST" "$HOST_FQDN" "$ts" <<'PYEOF'
import json, sys, os
manifest_path, hostname, ts = sys.argv[1:4]
try:
with open(manifest_path) as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
data = {}
data["hostname"] = hostname
data.setdefault("modules", {})
data["last_run"] = ts
tmp = manifest_path + ".tmp"
with open(tmp, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
os.rename(tmp, manifest_path)
PYEOF
}
# Reconcile: scan existing .done stamps into the manifest.
# Handles modules installed before the manifest feature was introduced.
_manifest_reconcile() {
local stamp_file mod
for stamp_file in "$STATE_DIR"/*.done; do
[[ -f "$stamp_file" ]] || continue
mod="${stamp_file%.done}"
mod="${mod##*/}"
_manifest_update "$mod" "installed"
done
}
# ── Resolve ANSIPA_USER ─────────────────────────────────────────────────────── # ── Resolve ANSIPA_USER ───────────────────────────────────────────────────────
if [[ -z "$ANSIPA_USER" ]]; then if [[ -z "$ANSIPA_USER" ]]; then
# Use the first non-root, non-system user with a login shell
ANSIPA_USER=$(awk -F: '($3>=1000 && $7!~/nologin|false/) {print $1; exit}' /etc/passwd) ANSIPA_USER=$(awk -F: '($3>=1000 && $7!~/nologin|false/) {print $1; exit}' /etc/passwd)
fi fi
if [[ -z "$ANSIPA_USER" ]]; then if [[ -z "$ANSIPA_USER" ]]; then
@ -39,7 +140,6 @@ if [[ -z "$ANSIPA_USER" ]]; then
fi fi
log "Running as root, AUR helper delegated to user: $ANSIPA_USER" log "Running as root, AUR helper delegated to user: $ANSIPA_USER"
mkdir -p "$STATE_DIR"
# ── Create a yay wrapper so module scripts can call 'yay' as non-root ──────── # ── Create a yay wrapper so module scripts can call 'yay' as non-root ────────
YAY_BIN=$(command -v yay 2>/dev/null || true) YAY_BIN=$(command -v yay 2>/dev/null || true)
@ -54,17 +154,14 @@ EOF
chmod +x "$WRAP_DIR/yay" chmod +x "$WRAP_DIR/yay"
fi fi
# ── Discover which ansipa-module-* host groups this host belongs to ─────────── # ── Kinit with host keytab so IPA commands work from the service context ──────
HOST_FQDN=$(hostname -f 2>/dev/null || hostname)
if ! command -v ipa &>/dev/null; then
warn "ipa command not found — host not enrolled in FreeIPA. Exiting."
exit 0
fi
# kinit with host keytab so IPA commands work from the service context
kinit -k "host/$HOST_FQDN" &>/dev/null || true kinit -k "host/$HOST_FQDN" &>/dev/null || true
# Record this run in the manifest and reconcile any pre-existing stamps
_manifest_touch_run
_manifest_reconcile
# ── Discover which dev_mod_* host groups this host belongs to ─────────────────
RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \ RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \
| grep -i "Member of host-groups:" | sed 's/.*: //' || true) | grep -i "Member of host-groups:" | sed 's/.*: //' || true)
@ -73,11 +170,11 @@ if [[ -z "$RAW_GROUPS" ]]; then
exit 0 exit 0
fi fi
# Parse comma-separated list, keep only ansipa-module-* entries # Parse comma-separated list, keep only dev_mod_* entries
WANTED_MODULES=() WANTED_MODULES=()
while IFS=',' read -ra GRP_ARRAY; do while IFS=',' read -ra GRP_ARRAY; do
for g in "${GRP_ARRAY[@]}"; do for g in "${GRP_ARRAY[@]}"; do
g="${g// /}" # strip spaces g="${g// /}"
if [[ "$g" == ${PREFIX}* ]]; then if [[ "$g" == ${PREFIX}* ]]; then
WANTED_MODULES+=("${g#$PREFIX}") WANTED_MODULES+=("${g#$PREFIX}")
fi fi
@ -85,7 +182,7 @@ while IFS=',' read -ra GRP_ARRAY; do
done <<< "$RAW_GROUPS" done <<< "$RAW_GROUPS"
if [[ ${#WANTED_MODULES[@]} -eq 0 ]]; then if [[ ${#WANTED_MODULES[@]} -eq 0 ]]; then
log "No ansipa-module-* host groups found for '$HOST_FQDN'." log "No ${PREFIX}* host groups found for '$HOST_FQDN'."
exit 0 exit 0
fi fi
@ -103,14 +200,19 @@ for MODULE in "${WANTED_MODULES[@]}"; do
if [[ ! -f "$SCRIPT" ]]; then if [[ ! -f "$SCRIPT" ]]; then
warn "Module script not found: $SCRIPT — skipping '$MODULE'." warn "Module script not found: $SCRIPT — skipping '$MODULE'."
_manifest_update "$MODULE" "pending"
continue continue
fi fi
log "Applying module: $MODULE" log "Applying module: $MODULE"
_manifest_update "$MODULE" "pending"
if env PATH="$WRAP_DIR:$PATH" bash "$SCRIPT" >>"$STATE_DIR/${MODULE}.log" 2>&1; then if env PATH="$WRAP_DIR:$PATH" bash "$SCRIPT" >>"$STATE_DIR/${MODULE}.log" 2>&1; then
touch "$STAMP" touch "$STAMP"
_manifest_update "$MODULE" "installed"
log "Module '$MODULE' applied successfully." log "Module '$MODULE' applied successfully."
else else
_manifest_update "$MODULE" "failed"
warn "Module '$MODULE' failed — see $STATE_DIR/${MODULE}.log" warn "Module '$MODULE' failed — see $STATE_DIR/${MODULE}.log"
fi fi
done done

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e set -e
PREFIX="ansipa-install-" PREFIX="dev_pkg_"
# Detect distro # Detect distro
if [ -f /etc/os-release ]; then if [ -f /etc/os-release ]; then

View File

@ -1,5 +1,5 @@
[Unit] [Unit]
Description=Install packages based on FreeIPA ansipa-install-* groups Description=Install packages based on FreeIPA dev_pkg_* groups
After=network-online.target sssd.service After=network-online.target sssd.service
[Service] [Service]

View File

@ -17,7 +17,7 @@
mode: '0644' mode: '0644'
content: | content: |
[Unit] [Unit]
Description=Install packages based on FreeIPA ansipa-install-* groups Description=Install packages based on FreeIPA dev_pkg_* groups
After=network-online.target sssd.service After=network-online.target sssd.service
[Service] [Service]
@ -51,7 +51,7 @@
mode: '0644' mode: '0644'
content: | content: |
[Unit] [Unit]
Description=Install Flatpaks based on FreeIPA fp_install_* groups Description=Install Flatpaks based on FreeIPA dev_fp_* groups
After=network-online.target sssd.service After=network-online.target sssd.service
Wants=network-online.target Wants=network-online.target

View File

@ -2,7 +2,8 @@
# deploy-ansipa-modules.yml — deploy the module auto-installer to enrolled hosts. # deploy-ansipa-modules.yml — deploy the module auto-installer to enrolled hosts.
# #
# Prerequisites on target hosts: # Prerequisites on target hosts:
# - FreeIPA client enrolled (sssd running, ipa command available) # - FreeIPA client enrolled (/etc/ipa/default.conf exists, ipa command available)
# - python3 installed (used for JSON manifest writes)
# - A non-root user with yay access (set ANSIPA_USER in /etc/ansipa-modules.conf) # - A non-root user with yay access (set ANSIPA_USER in /etc/ansipa-modules.conf)
# #
# Usage: # Usage:
@ -10,8 +11,10 @@
# ansible-playbook -i inventory deploy-ansipa-modules.yml -e ansipa_user=amir # ansible-playbook -i inventory deploy-ansipa-modules.yml -e ansipa_user=amir
# #
# FreeIPA host group convention: # FreeIPA host group convention:
# Create host groups named ansipa-module-<name> (e.g. ansipa-module-docker) # Create host groups named dev_mod_<name> (e.g. dev_mod_docker, dev_mod_ollama)
# and add hosts to them. The timer will apply the matching module automatically. # and add hosts to them. The timer checks group membership and applies any missing
# modules automatically. State is tracked in STATE_DIR/<module>.done stamps and
# summarised in STATE_DIR/manifest.json.
- name: Deploy FreeIPA module auto-installer - name: Deploy FreeIPA module auto-installer
hosts: all hosts: all
@ -24,6 +27,11 @@
tasks: tasks:
- name: Ensure python3 is installed
package:
name: python3
state: present
- name: Create module directories - name: Create module directories
file: file:
path: "{{ item }}" path: "{{ item }}"
@ -66,7 +74,7 @@
mode: '0644' mode: '0644'
content: | content: |
[Unit] [Unit]
Description=Apply setup modules based on FreeIPA ansipa-module-* host groups Description=Apply setup modules based on FreeIPA dev_mod_* host groups
After=network-online.target sssd.service After=network-online.target sssd.service
Wants=network-online.target Wants=network-online.target

View File

@ -2,21 +2,24 @@
# deploy-ansipa-policies.yml — deploy the policy enforcement daemon to enrolled clients. # deploy-ansipa-policies.yml — deploy the policy enforcement daemon to enrolled clients.
# #
# Installs ansipa-enforce-policies.sh and a systemd timer that runs it every 30 minutes. # Installs ansipa-enforce-policies.sh and a systemd timer that runs it every 30 minutes.
# Policies are declared by adding hosts to the following FreeIPA host groups: # Device policies (FreeIPA host groups — applied to the whole machine):
# dev_daemon-enable-<unit> Ensure <unit> is enabled and running; reverted when host leaves group
# dev_daemon-disable-<unit> Ensure <unit> is disabled and stopped; reverted when host leaves group
# dev_timeshift-backup Enforce daily Timeshift snapshots (03:00)
# dev_security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans + SMB upload (02:00)
# dev_no-local-users Lock local account passwords; only FreeIPA accounts can auth
# dev_local-sudo-<username> Grant <username> local sudo on this device; reverted when host leaves
# #
# policy-block-binary-<name> Block execution of <name> via a PATH-priority wrapper + AppArmor # User policies (FreeIPA user groups — follow the user across all enrolled devices):
# policy-daemon-enable-<unit> Ensure <unit> is enabled and running; reverted when host leaves group # usr_block-binary-<name> Block execution of <name> via a PATH-priority wrapper
# policy-daemon-disable-<unit> Ensure <unit> is disabled and stopped; reverted when host leaves group # usr_scan-notify Fetch alerts from server, notify user every 10 min until acknowledged
# policy-timeshift-backup Enforce daily Timeshift snapshots (03:00)
# policy-security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans + SMB upload (02:00)
# policy-scan-notify Fetch alerts from server, notify user every 10 min until acknowledged
# #
# Prerequisites: # Prerequisites:
# - Host enrolled in FreeIPA (sssd + ipa CLI available) # - Host enrolled in FreeIPA (sssd + ipa CLI available)
# - For security-scan / scan-notify: samba-client installed (handled below) # - For dev_security-scan / usr_scan-notify: samba-client installed (handled below)
# - For security-scan / scan-notify: smb_scan_password set (use ansible-vault in production) # - For dev_security-scan / usr_scan-notify: smb_scan_password set (use ansible-vault in production)
# - For security-scan tools: also add host to ansipa-module-anti-malware group # - For security-scan tools: also add host to dev_mod_anti-malware group
# - For timeshift-backup: also add host to ansipa-module-timeshift group # - For timeshift-backup: also add host to dev_mod_timeshift group
# #
# Usage: # Usage:
# ansible-playbook -i inventory deploy-ansipa-policies.yml \ # ansible-playbook -i inventory deploy-ansipa-policies.yml \