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_01LcnnA1whUwQkDv1omsgh9Ymain
parent
2be85739b5
commit
9289f01965
|
|
@ -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-<unit> Ensure <unit> is enabled and running (systemctl enable --now).
|
||||
# Leaving the group reverts: service is disabled and stopped.
|
||||
# policy-daemon-disable-<unit> Ensure <unit> is disabled and stopped (systemctl disable --now).
|
||||
# Leaving the group reverts: service is re-enabled and started.
|
||||
# <unit> 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_<username> Grant <username> 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-<unit> Ensure <unit> is enabled and running (systemctl enable --now).
|
||||
# Leaving the group reverts: service is disabled and stopped.
|
||||
# dev_daemon-disable-<unit> Ensure <unit> is disabled and stopped (systemctl disable --now).
|
||||
# Leaving the group reverts: service is re-enabled and started.
|
||||
# <unit> 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-<username> Grant <username> 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-<name> Prevent members of this FreeIPA user group from running <name>
|
||||
# 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-<name> Prevent members of this FreeIPA user group from running <name>
|
||||
# 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-<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.
|
||||
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-<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.
|
||||
# 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.
|
||||
# <unit> 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_<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.
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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/<hostname>/ for every active login session.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -3,11 +3,15 @@
|
|||
# FreeIPA host group membership.
|
||||
#
|
||||
# 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
|
||||
# script in /usr/local/lib/ansipa-modules/<name>.sh is executed (once,
|
||||
# stamped in /var/lib/ansipa-modules/).
|
||||
# script in /usr/local/lib/ansipa-modules/<name>.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=<username> 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: <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 ───────────────────────────────────────────────────────
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
PREFIX="ansipa-install-"
|
||||
PREFIX="dev_pkg_"
|
||||
|
||||
# Detect distro
|
||||
if [ -f /etc/os-release ]; then
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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-<name> (e.g. ansipa-module-docker)
|
||||
# and add hosts to them. The timer will apply the matching module automatically.
|
||||
# Create host groups named dev_mod_<name> (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/<module>.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
|
||||
|
||||
|
|
|
|||
|
|
@ -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-<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
|
||||
# policy-daemon-enable-<unit> Ensure <unit> is enabled and running; reverted when host leaves group
|
||||
# policy-daemon-disable-<unit> Ensure <unit> 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-<name> Block execution of <name> 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 \
|
||||
|
|
|
|||
Loading…
Reference in New Issue