diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh index fe83426..8944288 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh @@ -5,41 +5,40 @@ # 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): -# policy-daemon-enable- Ensure is enabled and running (systemctl enable --now). -# Leaving the group reverts: service is disabled and stopped. -# policy-daemon-disable- Ensure is disabled and stopped (systemctl disable --now). -# Leaving the group reverts: service is re-enabled and started. -# may omit the .service suffix; all systemd unit types work. -# 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 -# policy-scan-notify (see User-group section below — treated as a user group) -# 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. -# local_sudo_ Grant full sudo on this specific device by adding -# them to the local sudoers drop-in. Leaving the group removes -# the drop-in on the next run. +# dev_daemon-enable- Ensure is enabled and running (systemctl enable --now). +# Leaving the group reverts: service is disabled and stopped. +# dev_daemon-disable- Ensure is disabled and stopped (systemctl disable --now). +# Leaving the group reverts: service is re-enabled and started. +# may omit the .service suffix; all systemd unit types work. +# If a unit appears in both enable and disable groups it is skipped. +# dev_timeshift-backup Enforce a daily Timeshift snapshot (requires timeshift installed) +# dev_security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans +# dev_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. +# dev_local-sudo- Grant full sudo on this specific device by adding +# them to the local sudoers drop-in. Leaving the group removes +# the drop-in on the next run. # # User-group naming conventions (per-user policies — follow the user across devices): -# policy-block-binary- Prevent members of this FreeIPA user group from running -# on any enrolled host. Use __ in place of . to support Flatpak -# app IDs (e.g. policy-block-binary-org__gimp__Gimp blocks the -# Flatpak org.gimp.Gimp). Enforced via a PATH-priority wrapper -# that checks group membership at runtime via SSSD/id(1). -# Removing the user group from FreeIPA reverts the wrapper. -# policy-scan-notify Members receive scan alert notifications on every enrolled device -# they log into. The fetch-alerts timer is installed fleet-wide -# when the group exists; the notification daemon starts on login -# only for group members (checked via id(1) / SSSD). -# Deleting the IPA user group removes the timer and profile.d -# snippet on the next enforcer run. +# usr_block-binary- Prevent members of this FreeIPA user group from running +# on any enrolled host. Use __ in place of . to support Flatpak +# app IDs (e.g. usr_block-binary-org__gimp__Gimp blocks the +# Flatpak org.gimp.Gimp). Enforced via a PATH-priority wrapper +# that checks group membership at runtime via SSSD/id(1). +# Removing the user group from FreeIPA reverts the wrapper. +# usr_scan-notify Members receive scan alert notifications on every enrolled device +# they log into. The fetch-alerts timer is installed fleet-wide +# when the group exists; the notification daemon starts on login +# only for group members (checked via id(1) / SSSD). +# Deleting the IPA user group removes the timer and profile.d +# snippet on the next enforcer run. # # Notes: -# - Install scan tools first: add the host to ansipa-module-anti-malware. -# - Configure Timeshift (type + target device) before enabling policy-timeshift-backup. +# - Install scan tools first: add the host to dev_mod_anti-malware. +# - Configure Timeshift (type + target device) before enabling dev_timeshift-backup. set -euo pipefail @@ -79,40 +78,40 @@ if [[ -n "$RAW_GROUPS" ]]; then for g in "${GRP_ARRAY[@]}"; do g="${g// /}" case "$g" in - policy-daemon-enable-*) ACTIVE_DAEMON_ENABLE+=("${g#policy-daemon-enable-}") ;; - policy-daemon-disable-*) ACTIVE_DAEMON_DISABLE+=("${g#policy-daemon-disable-}") ;; - policy-timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;; - policy-security-scan) WANT_SECURITY_SCAN=true ;; - no_local_users) WANT_NO_LOCAL_USERS=true ;; - local_sudo_*) ACTIVE_LOCAL_SUDO_USERS+=("${g#local_sudo_}") ;; + dev_daemon-enable-*) ACTIVE_DAEMON_ENABLE+=("${g#dev_daemon-enable-}") ;; + dev_daemon-disable-*) ACTIVE_DAEMON_DISABLE+=("${g#dev_daemon-disable-}") ;; + dev_timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;; + dev_security-scan) WANT_SECURITY_SCAN=true ;; + dev_no-local-users) WANT_NO_LOCAL_USERS=true ;; + dev_local-sudo-*) ACTIVE_LOCAL_SUDO_USERS+=("${g#dev_local-sudo-}") ;; esac done done <<< "$RAW_GROUPS" fi # ── Fetch user-group-based binary block policies from FreeIPA ───────────────── -# policy-block-binary- groups are FreeIPA *user* groups — membership follows +# usr_block-binary- groups are FreeIPA *user* groups — membership follows # the user to every enrolled host rather than being tied to a device. ACTIVE_BLOCK_BINARIES=() ACTIVE_BLOCK_IPA_GROUPS=() _BLOCK_LIST=$(ipa group-find --pkey-only 2>/dev/null \ | 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 [[ -z "$_grp" ]] && continue - _raw="${_grp#policy-block-binary-}" + _raw="${_grp#usr_block-binary-}" ACTIVE_BLOCK_BINARIES+=("${_raw//__/.}") ACTIVE_BLOCK_IPA_GROUPS+=("$_grp") done <<< "$_BLOCK_LIST" unset _BLOCK_LIST _grp _raw # ── 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 # 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 fi @@ -134,7 +133,7 @@ _in_block_list() { # ── Binary blocking (user-based) ────────────────────────────────────────────── # 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; # non-members are transparently passed through to the real binary. # __ 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 [[ ! -f "$TIMESHIFT_CRON" ]]; 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 log "Enabling daily Timeshift backups" 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. 0 3 * * * root /usr/bin/timeshift --create --comments "ansipa-daily" --tags D 2>&1 | logger -t timeshift-backup CRON @@ -208,7 +207,7 @@ CRON else if [[ -f "$TIMESHIFT_CRON" ]]; then 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 @@ -263,8 +262,8 @@ SCAN if [[ ! -f "$SCAN_CRON" ]]; then log "Enabling daily security scans (ClamAV / rkhunter / chkrootkit)" cat > "$SCAN_CRON" <<'CRON' -# ansipa-policy-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. +# ansipa-dev_security-scan: managed by ansipa-enforce-policies — do not edit manually. +# Install scan tools by adding the host to the dev_mod_anti-malware group. 0 2 * * * root /usr/local/bin/ansipa-security-scan.sh CRON chmod 644 "$SCAN_CRON" @@ -273,12 +272,12 @@ else if [[ -f "$SCAN_CRON" ]]; then rm -f "$SCAN_CRON" 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 # ── 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 # 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). @@ -335,11 +334,11 @@ UNIT log "Installing /etc/profile.d/ansipa-notify.sh" cat > "$NOTIFY_PROFILED" <<'PROFILED' # 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. _NOTIFY_DAEMON=/usr/local/bin/ansipa-scan-notify.sh 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 "$_NOTIFY_DAEMON" & disown @@ -353,7 +352,7 @@ else systemctl disable --now ansipa-fetch-alerts.timer 2>/dev/null || true rm -f "$FETCH_SVC" "$FETCH_TIMER" 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 if [[ -f "$NOTIFY_PROFILED" ]]; then rm -f "$NOTIFY_PROFILED" @@ -362,9 +361,9 @@ else fi # ── Daemon enable / disable ─────────────────────────────────────────────────── -# policy-daemon-enable-: ensure the unit is enabled and running. +# dev_daemon-enable-: ensure the unit is enabled and running. # Leaving the group reverts: unit is disabled and stopped. -# policy-daemon-disable-: ensure the unit is disabled and stopped. +# dev_daemon-disable-: ensure the unit is disabled and stopped. # Leaving the group reverts: unit is re-enabled and started. # may omit the .service suffix; systemd accepts both forms. # Conflicts (unit in both lists): logged as a warning, unit is left untouched. @@ -448,7 +447,7 @@ else fi # ── Per-device local sudo grants ────────────────────────────────────────────── -# local_sudo_: write a sudoers drop-in granting full sudo on +# dev_local-sudo-: write a sudoers drop-in granting full sudo on # this specific device. The drop-in is removed when the host leaves the group. 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" 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 [[ -z "$_OLD_USER" ]] && continue _still_active=false @@ -476,7 +475,7 @@ while IFS= read -r _OLD_USER; do _DROPIN="$LOCAL_SUDO_DIR/ansipa-local-sudo-${_OLD_USER}" if [[ -f "$_DROPIN" ]]; then 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 done < "$LOCAL_SUDO_STATE" @@ -490,7 +489,7 @@ fi unset _USER _DROPIN _OLD_USER _still_active _U # ── 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 # authenticate and gain elevated privileges. # Leaving the group reverts: every account locked by this policy is unlocked. diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh index a793b4a..66b23c5 100644 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # 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: # - Downloads it to ~/administration// for every active login session. diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-install-flatpaks.service b/setup/modules/FreeipaAnsible/ansible/ansipa-install-flatpaks.service index cb43574..dbaa09e 100644 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-install-flatpaks.service +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-install-flatpaks.service @@ -1,5 +1,5 @@ [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 Wants=network-online.target diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-install-flatpaks.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-install-flatpaks.sh index 30373f0..84d4d14 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-install-flatpaks.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-install-flatpaks.sh @@ -2,18 +2,18 @@ # ansipa-install-flatpaks.sh — install Flatpak apps based on FreeIPA group membership. # # IPA group naming convention (dots encoded as double underscores): -# fp_install_org__mozilla__firefox → installs org.mozilla.firefox -# fp_install_com__spotify__Client → installs com.spotify.Client -# fp_install_io__missioncenter__MissionCenter → installs io.missioncenter.MissionCenter +# dev_fp_org__mozilla__firefox → installs org.mozilla.firefox +# dev_fp_com__spotify__Client → installs com.spotify.Client +# 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. # # Scope: system-wide (--system), runs as root via systemd service. set -e -PREFIX="fp_install_" +PREFIX="dev_fp_" # ── Preflight ───────────────────────────────────────────────────────────────── 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 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). # $NF captures the name regardless of leading label text. IPA_GROUPS=$(ipa group-find --pkey-only 2>/dev/null \ @@ -49,7 +49,7 @@ if [[ -z "$IPA_GROUPS" ]]; then fi # ── 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 DESIRED_FLATPAKS=() while IFS= read -r G; do diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.service b/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.service index 75ee154..b0abdfd 100644 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.service +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.service @@ -1,5 +1,5 @@ [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 Wants=network-online.target diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.sh index 47753ba..66fc8da 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-install-modules.sh @@ -3,11 +3,15 @@ # FreeIPA host group membership. # # Host groups follow the naming convention: -# ansipa-module- e.g. ansipa-module-docker, ansipa-module-ollama +# dev_mod_ e.g. dev_mod_docker, dev_mod_ollama # # When this host is a member of such a group, the corresponding module -# script in /usr/local/lib/ansipa-modules/.sh is executed (once, -# stamped in /var/lib/ansipa-modules/). +# script in /usr/local/lib/ansipa-modules/.sh is executed once. +# 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 # ANSIPA_USER= non-root user for AUR helper (yay) @@ -22,15 +26,112 @@ CONFIG=/etc/ansipa-modules.conf ANSIPA_USER="${ANSIPA_USER:-}" MODULES_DIR="${MODULES_DIR:-/usr/local/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() { 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; } +# ── 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: 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 ─────────────────────────────────────────────────────── 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) fi if [[ -z "$ANSIPA_USER" ]]; then @@ -39,7 +140,6 @@ if [[ -z "$ANSIPA_USER" ]]; then fi 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 ──────── YAY_BIN=$(command -v yay 2>/dev/null || true) @@ -54,17 +154,14 @@ EOF chmod +x "$WRAP_DIR/yay" fi -# ── Discover which ansipa-module-* host groups this host belongs to ─────────── -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 with host keytab so IPA commands work from the service context ────── 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 \ | grep -i "Member of host-groups:" | sed 's/.*: //' || true) @@ -73,11 +170,11 @@ if [[ -z "$RAW_GROUPS" ]]; then exit 0 fi -# Parse comma-separated list, keep only ansipa-module-* entries +# Parse comma-separated list, keep only dev_mod_* entries WANTED_MODULES=() while IFS=',' read -ra GRP_ARRAY; do for g in "${GRP_ARRAY[@]}"; do - g="${g// /}" # strip spaces + g="${g// /}" if [[ "$g" == ${PREFIX}* ]]; then WANTED_MODULES+=("${g#$PREFIX}") fi @@ -85,7 +182,7 @@ while IFS=',' read -ra GRP_ARRAY; do done <<< "$RAW_GROUPS" 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 fi @@ -103,14 +200,19 @@ for MODULE in "${WANTED_MODULES[@]}"; do if [[ ! -f "$SCRIPT" ]]; then warn "Module script not found: $SCRIPT — skipping '$MODULE'." + _manifest_update "$MODULE" "pending" continue fi log "Applying module: $MODULE" + _manifest_update "$MODULE" "pending" + if env PATH="$WRAP_DIR:$PATH" bash "$SCRIPT" >>"$STATE_DIR/${MODULE}.log" 2>&1; then touch "$STAMP" + _manifest_update "$MODULE" "installed" log "Module '$MODULE' applied successfully." else + _manifest_update "$MODULE" "failed" warn "Module '$MODULE' failed — see $STATE_DIR/${MODULE}.log" fi done diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-install-packages.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-install-packages.sh index 4f99e36..edda048 100644 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-install-packages.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-install-packages.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -PREFIX="ansipa-install-" +PREFIX="dev_pkg_" # Detect distro if [ -f /etc/os-release ]; then diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-install.service b/setup/modules/FreeipaAnsible/ansible/ansipa-install.service index 42fd343..1ba674c 100644 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-install.service +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-install.service @@ -1,5 +1,5 @@ [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 [Service] diff --git a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-install.yml b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-install.yml index 7736df4..495435d 100644 --- a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-install.yml +++ b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-install.yml @@ -17,7 +17,7 @@ mode: '0644' content: | [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 [Service] @@ -51,7 +51,7 @@ mode: '0644' content: | [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 Wants=network-online.target diff --git a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-modules.yml b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-modules.yml index aef0fe5..47fa307 100644 --- a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-modules.yml +++ b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-modules.yml @@ -2,7 +2,8 @@ # deploy-ansipa-modules.yml — deploy the module auto-installer to enrolled 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) # # Usage: @@ -10,8 +11,10 @@ # ansible-playbook -i inventory deploy-ansipa-modules.yml -e ansipa_user=amir # # FreeIPA host group convention: -# Create host groups named ansipa-module- (e.g. ansipa-module-docker) -# and add hosts to them. The timer will apply the matching module automatically. +# Create host groups named dev_mod_ (e.g. dev_mod_docker, dev_mod_ollama) +# and add hosts to them. The timer checks group membership and applies any missing +# modules automatically. State is tracked in STATE_DIR/.done stamps and +# summarised in STATE_DIR/manifest.json. - name: Deploy FreeIPA module auto-installer hosts: all @@ -24,6 +27,11 @@ tasks: + - name: Ensure python3 is installed + package: + name: python3 + state: present + - name: Create module directories file: path: "{{ item }}" @@ -66,7 +74,7 @@ mode: '0644' content: | [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 Wants=network-online.target diff --git a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml index 70f7c97..95bfa16 100644 --- a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml +++ b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml @@ -2,21 +2,24 @@ # 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. -# 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- Ensure is enabled and running; reverted when host leaves group +# dev_daemon-disable- Ensure 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- Grant local sudo on this device; reverted when host leaves # -# policy-block-binary- Block execution of via a PATH-priority wrapper + AppArmor -# policy-daemon-enable- Ensure is enabled and running; reverted when host leaves group -# policy-daemon-disable- Ensure is disabled and stopped; reverted when host leaves group -# 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 +# User policies (FreeIPA user groups — follow the user across all enrolled devices): +# usr_block-binary- Block execution of via a PATH-priority wrapper +# usr_scan-notify Fetch alerts from server, notify user every 10 min until acknowledged # # Prerequisites: # - Host enrolled in FreeIPA (sssd + ipa CLI available) -# - For security-scan / scan-notify: samba-client installed (handled below) -# - For security-scan / 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 timeshift-backup: also add host to ansipa-module-timeshift group +# - For dev_security-scan / usr_scan-notify: samba-client installed (handled below) +# - For dev_security-scan / usr_scan-notify: smb_scan_password set (use ansible-vault in production) +# - For security-scan tools: also add host to dev_mod_anti-malware group +# - For timeshift-backup: also add host to dev_mod_timeshift group # # Usage: # ansible-playbook -i inventory deploy-ansipa-policies.yml \