feat(ansipa): add daemon enable/disable policy via host-group regex

Host groups named policy-daemon-enable-<unit> and
policy-daemon-disable-<unit> 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 <unit>; state written to
           /var/lib/ansipa-policies/daemon-enabled
  disable: systemctl disable --now <unit>; 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 <noreply@anthropic.com>
main
Amir Alexander Abdelbaki 2026-05-20 15:25:15 +02:00
parent 63cd59fb91
commit aced2c754e
2 changed files with 109 additions and 10 deletions

View File

@ -5,12 +5,18 @@
# 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: # Host-group naming conventions:
# policy-block-binary-<name> Block execution of <name> via two layers: # policy-block-binary-<name> Block execution of <name> via two layers:
# 1. PATH-priority wrapper in /usr/local/bin/ (catches $PATH calls) # 1. PATH-priority wrapper in /usr/local/bin/ (catches $PATH calls)
# 2. AppArmor deny profile in /etc/apparmor.d/ (catches absolute paths) # 2. AppArmor deny profile in /etc/apparmor.d/ (catches absolute paths)
# AppArmor layer is skipped silently if apparmor_parser is not present. # AppArmor layer is skipped silently if apparmor_parser is not present.
# policy-timeshift-backup Enforce a daily Timeshift snapshot (requires timeshift installed) # policy-daemon-enable-<unit> Ensure <unit> is enabled and running (systemctl enable --now).
# policy-security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans # 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
# #
# Notes: # Notes:
# - Install scan tools first: add the host to ansipa-module-anti-malware. # - 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 ──────────────────────────────────────────────── # ── Parse active policy groups ────────────────────────────────────────────────
ACTIVE_BLOCK_BINARIES=() ACTIVE_BLOCK_BINARIES=()
ACTIVE_DAEMON_ENABLE=()
ACTIVE_DAEMON_DISABLE=()
WANT_TIMESHIFT_BACKUP=false WANT_TIMESHIFT_BACKUP=false
WANT_SECURITY_SCAN=false WANT_SECURITY_SCAN=false
WANT_SCAN_NOTIFY=false WANT_SCAN_NOTIFY=false
@ -53,6 +61,8 @@ if [[ -n "$RAW_GROUPS" ]]; then
g="${g// /}" g="${g// /}"
case "$g" in case "$g" in
policy-block-binary-*) ACTIVE_BLOCK_BINARIES+=("${g#policy-block-binary-}") ;; 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-timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;;
policy-security-scan) WANT_SECURITY_SCAN=true ;; policy-security-scan) WANT_SECURITY_SCAN=true ;;
policy-scan-notify) WANT_SCAN_NOTIFY=true ;; policy-scan-notify) WANT_SCAN_NOTIFY=true ;;
@ -62,6 +72,7 @@ if [[ -n "$RAW_GROUPS" ]]; then
fi fi
log "Active policies — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \ 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" \ "| timeshift-backup: $WANT_TIMESHIFT_BACKUP" \
"| security-scan: $WANT_SECURITY_SCAN | scan-notify: $WANT_SCAN_NOTIFY" "| security-scan: $WANT_SECURITY_SCAN | scan-notify: $WANT_SCAN_NOTIFY"
@ -347,4 +358,90 @@ else
fi fi
fi fi
# ── Daemon enable / disable ───────────────────────────────────────────────────
# policy-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.
# 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.
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." log "Policy enforcement complete."

View File

@ -4,10 +4,12 @@
# 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: # Policies are declared by adding hosts to the following FreeIPA host groups:
# #
# policy-block-binary-<name> Block execution of <name> via a PATH-priority wrapper + AppArmor # policy-block-binary-<name> Block execution of <name> via a PATH-priority wrapper + AppArmor
# policy-timeshift-backup Enforce daily Timeshift snapshots (03:00) # policy-daemon-enable-<unit> Ensure <unit> is enabled and running; reverted when host leaves group
# policy-security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans + SMB upload (02:00) # policy-daemon-disable-<unit> Ensure <unit> is disabled and stopped; reverted when host leaves group
# policy-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)