From aced2c754ed8b5cd5ec96335960600397ad0b56a Mon Sep 17 00:00:00 2001 From: The_miro Date: Wed, 20 May 2026 15:25:15 +0200 Subject: [PATCH] feat(ansipa): add daemon enable/disable policy via host-group regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Host groups named policy-daemon-enable- and policy-daemon-disable- are now matched by a wildcard case arm in the group parser — no per-service configuration required. Enforcement (every 30 min via existing timer): enable: systemctl enable --now ; state written to /var/lib/ansipa-policies/daemon-enabled disable: systemctl disable --now ; state written to /var/lib/ansipa-policies/daemon-disabled revert: when a host leaves a group the opposite action is applied on the next run (enable→disable, disable→enable) conflict: unit in both lists is skipped with a warning The .service suffix is optional — _svc_unit() appends it when the name contains no dot, so all systemd unit types work as-is. Co-Authored-By: Claude Sonnet 4.6 --- .../ansible/ansipa-enforce-policies.sh | 109 +++++++++++++++++- .../ansible/deploy-ansipa-policies.yml | 10 +- 2 files changed, 109 insertions(+), 10 deletions(-) diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh index 936d6d4..501e80d 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh @@ -5,12 +5,18 @@ # leaving the group removes it on the next run (every 30 min via systemd timer). # # Host-group naming conventions: -# policy-block-binary- Block execution of via two layers: -# 1. PATH-priority wrapper in /usr/local/bin/ (catches $PATH calls) -# 2. AppArmor deny profile in /etc/apparmor.d/ (catches absolute paths) -# AppArmor layer is skipped silently if apparmor_parser is not present. -# policy-timeshift-backup Enforce a daily Timeshift snapshot (requires timeshift installed) -# policy-security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans +# policy-block-binary- Block execution of via two layers: +# 1. PATH-priority wrapper in /usr/local/bin/ (catches $PATH calls) +# 2. AppArmor deny profile in /etc/apparmor.d/ (catches absolute paths) +# AppArmor layer is skipped silently if apparmor_parser is not present. +# 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 # # Notes: # - Install scan tools first: add the host to ansipa-module-anti-malware. @@ -43,6 +49,8 @@ RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \ # ── Parse active policy groups ──────────────────────────────────────────────── ACTIVE_BLOCK_BINARIES=() +ACTIVE_DAEMON_ENABLE=() +ACTIVE_DAEMON_DISABLE=() WANT_TIMESHIFT_BACKUP=false WANT_SECURITY_SCAN=false WANT_SCAN_NOTIFY=false @@ -53,6 +61,8 @@ if [[ -n "$RAW_GROUPS" ]]; then g="${g// /}" case "$g" in policy-block-binary-*) ACTIVE_BLOCK_BINARIES+=("${g#policy-block-binary-}") ;; + 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 ;; policy-scan-notify) WANT_SCAN_NOTIFY=true ;; @@ -62,6 +72,7 @@ if [[ -n "$RAW_GROUPS" ]]; then fi log "Active policies — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \ + "| daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none} | daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \ "| timeshift-backup: $WANT_TIMESHIFT_BACKUP" \ "| security-scan: $WANT_SECURITY_SCAN | scan-notify: $WANT_SCAN_NOTIFY" @@ -347,4 +358,90 @@ else fi fi +# ── Daemon enable / disable ─────────────────────────────────────────────────── +# policy-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. +# 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. + +DAEMON_ENABLE_STATE="$STATE_DIR/daemon-enabled" +DAEMON_DISABLE_STATE="$STATE_DIR/daemon-disabled" +[[ -f "$DAEMON_ENABLE_STATE" ]] || touch "$DAEMON_ENABLE_STATE" +[[ -f "$DAEMON_DISABLE_STATE" ]] || touch "$DAEMON_DISABLE_STATE" + +# Append .service only when the name has no unit-type suffix already. +_svc_unit() { [[ "$1" == *.* ]] && echo "$1" || echo "${1}.service"; } + +_in_enable_list() { local n="$1"; for s in "${ACTIVE_DAEMON_ENABLE[@]}"; do [[ "$s" == "$n" ]] && return 0; done; return 1; } +_in_disable_list() { local n="$1"; for s in "${ACTIVE_DAEMON_DISABLE[@]}"; do [[ "$s" == "$n" ]] && return 0; done; return 1; } + +# Apply enable policies +for _SVC in "${ACTIVE_DAEMON_ENABLE[@]}"; do + if _in_disable_list "$_SVC"; then + warn "Conflict: '$_SVC' is in both daemon-enable and daemon-disable groups — skipped" + continue + fi + _UNIT=$(_svc_unit "$_SVC") + _EN=$(systemctl is-enabled "$_UNIT" 2>/dev/null || echo "not-found") + _AC=$(systemctl is-active "$_UNIT" 2>/dev/null || echo "inactive") + if [[ "$_EN" != "enabled" || "$_AC" != "active" ]]; then + log "Enabling service: $_UNIT (enabled=$_EN active=$_AC)" + systemctl enable --now "$_UNIT" 2>/dev/null \ + && log "Service enabled: $_UNIT" \ + || warn "Failed to enable $_UNIT — unit may not exist on this host" + fi +done + +# Apply disable policies +for _SVC in "${ACTIVE_DAEMON_DISABLE[@]}"; do + if _in_enable_list "$_SVC"; then + continue # conflict already warned above + fi + _UNIT=$(_svc_unit "$_SVC") + _EN=$(systemctl is-enabled "$_UNIT" 2>/dev/null || echo "not-found") + _AC=$(systemctl is-active "$_UNIT" 2>/dev/null || echo "inactive") + if [[ "$_EN" == "enabled" || "$_AC" == "active" ]]; then + log "Disabling service: $_UNIT (enabled=$_EN active=$_AC)" + systemctl disable --now "$_UNIT" 2>/dev/null \ + && log "Service disabled: $_UNIT" \ + || warn "Failed to disable $_UNIT — unit may not exist on this host" + fi +done + +# Revert: host left a daemon-enable group → disable and stop the service +while IFS= read -r _OLD; do + [[ -z "$_OLD" ]] && continue + if ! _in_enable_list "$_OLD"; then + _UNIT=$(_svc_unit "$_OLD") + log "Reverting enable policy: disabling $_UNIT (host left daemon-enable group)" + systemctl disable --now "$_UNIT" 2>/dev/null \ + || warn "Failed to disable (revert) $_UNIT" + fi +done < "$DAEMON_ENABLE_STATE" + +# Revert: host left a daemon-disable group → re-enable and start the service +while IFS= read -r _OLD; do + [[ -z "$_OLD" ]] && continue + if ! _in_disable_list "$_OLD"; then + _UNIT=$(_svc_unit "$_OLD") + log "Reverting disable policy: enabling $_UNIT (host left daemon-disable group)" + systemctl enable --now "$_UNIT" 2>/dev/null \ + || warn "Failed to enable (revert) $_UNIT" + fi +done < "$DAEMON_DISABLE_STATE" + +# Persist current state +if [[ ${#ACTIVE_DAEMON_ENABLE[@]} -gt 0 ]]; then + printf '%s\n' "${ACTIVE_DAEMON_ENABLE[@]}" | sort -u > "$DAEMON_ENABLE_STATE" +else + > "$DAEMON_ENABLE_STATE" +fi +if [[ ${#ACTIVE_DAEMON_DISABLE[@]} -gt 0 ]]; then + printf '%s\n' "${ACTIVE_DAEMON_DISABLE[@]}" | sort -u > "$DAEMON_DISABLE_STATE" +else + > "$DAEMON_DISABLE_STATE" +fi + log "Policy enforcement complete." diff --git a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml index bd0ab8e..70f7c97 100644 --- a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml +++ b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml @@ -4,10 +4,12 @@ # 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: # -# policy-block-binary- Block execution of via a PATH-priority wrapper + AppArmor -# 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 +# 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 # # Prerequisites: # - Host enrolled in FreeIPA (sssd + ipa CLI available)