diff --git a/docs/md/freeipa-ansible.md b/docs/md/freeipa-ansible.md index 2d7e72b..c678a6e 100644 --- a/docs/md/freeipa-ansible.md +++ b/docs/md/freeipa-ansible.md @@ -14,8 +14,8 @@ All relevant files live under `setup/modules/FreeipaAnsible/`. │ │ │ • FreeIPA — user/host directory, Kerberos KDC, DNS (optional) │ │ • Keycloak — OIDC provider federating FreeIPA via LDAP │ -│ • Samba — two SMB shares for scan results and LUKS backup keys │ -│ • cronie — hourly scan-result analyser (ansipa-check-scans) │ +│ • CheckMK — fleet monitoring (agent + local checks) │ +│ • Samba — SMB shares for the policy store and LUKS backup keys │ └───────────┬─────────────────────────┬────────────────────────────────┘ │ SSSD / Kerberos │ SMB (445) ▼ ▼ @@ -24,9 +24,10 @@ All relevant files live under `setup/modules/FreeipaAnsible/`. │ │ │ │ │ • sssd │ │ • deploy-ansipa-*.yml playbooks │ │ • ipa CLI │ │ • collect-luks-keys.yml │ -│ • Ansible-deployed │ │ (uploads keys to ansipa-luks-keys │ -│ timers: │ │ share as luks-upload service acct) │ -│ ├── pkg installer │ └─────────────────────────────────────────┘ +│ • CheckMK agent │ │ (uploads keys to ansipa-luks-keys │ +│ • Ansible-deployed │ │ share as luks-upload service acct) │ +│ timers: │ └─────────────────────────────────────────┘ +│ ├── pkg installer │ │ ├── module install│ │ ├── Flatpak install │ ├── baseuser sync │ @@ -34,8 +35,7 @@ All relevant files live under `setup/modules/FreeipaAnsible/`. │ ├── binary blocking │ ├── daemon enable/disable │ ├── Timeshift backups -│ ├── security scans + SMB upload -│ └── alert fetch + desktop notify +│ └── dev_mon_clamscan → CheckMK └──────────────────────┘ ``` @@ -73,7 +73,6 @@ docker compose up -d freeipa | `IPA_SETUP_DNS` | — | `true` to enable integrated DNS (default `false`) | | `IPA_DNS_FORWARDER` | — | Upstream DNS when `IPA_SETUP_DNS=true` | | `IPA_SETUP_KRA` | — | `true` to enable the Key Recovery Authority | -| `SMB_SCAN_PASSWORD` | yes | Password for the `scanupload` Samba service account | | `LUKS_KEY_UPLOAD_PASSWORD` | yes | Password for the `luks-upload` Samba service account | | `KC_HOSTNAME` | yes | Public hostname of Keycloak | | `KC_REALM` | — | Keycloak realm name (default `corp`) | @@ -118,10 +117,8 @@ Data persisted to the `/data` volume: /data/ ├── samba/ │ ├── passdb.tdb # Samba password database (survives restarts) -│ └── ansipa-smb.env # Persisted SMB passwords (auto-written on first start) -├── scan-results/ -│ ├── archive// # Client scan logs (written by clients via SMB) -│ └── alerts// # Alert files generated by ansipa-check-scans (hourly) +│ └── ansipa-smb.env # Persisted service passwords (auto-written on first start) +├── policy-store/ # Policy files, edited live via the ansipa-policystore share └── luks-keys/ # LUKS backup keys (written by Ansible controller via SMB) ``` @@ -129,15 +126,12 @@ Data persisted to the `/data` volume: ## SMB Shares -The container exposes two Samba shares, both configured by `ansipa-smb-setup.sh`. +The container exposes two Samba shares (`ansipa-luks-keys` and +`ansipa-policystore`), configured by `ansipa-smb-setup.sh`. -### `ansipa-scans` — Scan Result Archive - -- **Path:** `/data/scan-results` -- **Authenticated user:** `scanupload` (write-only; no browse) -- **Purpose:** Clients enrolled in `policy-security-scan` upload their daily ClamAV / rkhunter / chkrootkit logs here after each scan run. -- **Analysis:** `ansipa-check-scans.sh` runs hourly via cronie; it reads each host's archive logs and writes `*.alert` files to the `alerts/` subdirectory when concerning patterns are found. -- **Credentials file on clients:** `/etc/ansipa-smb.creds` (deployed by `deploy-ansipa-policies.yml`) +> ClamAV scan results are **no longer** delivered over SMB. The `dev_mon_clamscan` +> policy now reports them to CheckMK via a local check (`Ansipa_ClamScan`), so +> there is no `ansipa-scans` share or `scanupload` account any more. ### `ansipa-luks-keys` — LUKS Backup Key Store @@ -150,7 +144,7 @@ The container exposes two Samba shares, both configured by `ansipa-smb-setup.sh` useradd -r -G KeyAdmin smbpasswd -a ``` - The `KeyAdmin` group and the `luks-upload` / `scanupload` service accounts are created by `ansipa-smb-setup.sh` on every container start. + The `KeyAdmin` group and the `luks-upload` service account are created by `ansipa-smb-setup.sh` on every container start. --- @@ -247,11 +241,10 @@ Deploys a `systemd.path` unit that triggers on login. Users who are members of t ### Deploy Policy Enforcer ```bash -ansible-playbook -i inventory deploy-ansipa-policies.yml \ - -e smb_scan_password= +ansible-playbook -i inventory deploy-ansipa-policies.yml ``` -Deploys `ansipa-enforce-policies.sh`, `ansipa-fetch-alerts.sh`, `ansipa-scan-notify.sh`, a systemd timer (every 30 min), and `/etc/ansipa-smb.creds`. Use `ansible-vault` for the password in production. +Deploys the shared policy library, all `policies.d/` modules, `ansipa-enforce-policies.sh`, and a systemd timer that runs the enforcer every 30 min. ### Collect LUKS Backup Keys @@ -292,17 +285,16 @@ Keys on the SMB share are accessible only to `KeyAdmin` group members (see [SMB | `BaseUser` | `auto-add-baseuser.sh` | Add user to local `baseusers` group | | `policy-daemon-enable-` | `ansipa-enforce-policies.sh` | `systemctl enable --now `; reverted on leave | | `policy-daemon-disable-` | `ansipa-enforce-policies.sh` | `systemctl disable --now `; reverted on leave | -| `policy-timeshift-backup` | `ansipa-enforce-policies.sh` | Daily Timeshift snapshot at 03:00 | -| `policy-security-scan` | `ansipa-enforce-policies.sh` | Daily ClamAV + rkhunter + chkrootkit scan + SMB upload | -| `no_local_users` | `ansipa-enforce-policies.sh` | Lock passwords for root and all local users (UID ≥ 1000); reverted on leave | +| `dev_timeshift-backup` | `ansipa-enforce-policies.sh` | Daily Timeshift snapshot at 03:00 | +| `dev_mon_clamscan` | `ansipa-enforce-policies.sh` | Daily ClamAV + rkhunter + chkrootkit scan at 02:00; result reported to CheckMK (`Ansipa_ClamScan` local check) | +| `dev_no-local-users` | `ansipa-enforce-policies.sh` | Lock passwords for root and all local users (UID ≥ 1000); reverted on leave | | `local_sudo_` | `ansipa-enforce-policies.sh` | Grant `` full sudo on this specific device; reverted on leave | ### User policies (user groups — follow the user across all enrolled devices) | Group prefix | Handled by | Effect | |--------------|-----------|--------| -| `policy-block-binary-` | `ansipa-enforce-policies.sh` | Prevent group members from running `` on any enrolled host; use `__` for `.` in Flatpak app IDs | -| `policy-scan-notify` | `ansipa-enforce-policies.sh` | Fetch server alerts and notify group members every 10 min until acknowledged; follows the user across all enrolled devices | +| `usr_block-binary-` | `ansipa-enforce-policies.sh` | Prevent group members from running `` on any enrolled host; use `__` for `.` in Flatpak app IDs | --- @@ -322,14 +314,6 @@ The enforcer queries all `policy-block-binary-*` user groups from FreeIPA on eve Deleting the IPA user group causes the wrapper to be removed on the next enforcer run. State is tracked in `/var/lib/ansipa-policies/blocked-binaries`. -### Scan Notifications (per user) - -`policy-scan-notify` is a FreeIPA **user group**, not a host group. Membership follows the user to every enrolled machine: group members receive scan alert notifications regardless of which device they log into. - -The enforcer checks whether the `policy-scan-notify` group exists in FreeIPA on every run. If it does, it installs the `ansipa-fetch-alerts` systemd timer fleet-wide (root service, fires every 10 min) and a `/etc/profile.d/ansipa-notify.sh` snippet. The profile.d snippet checks group membership at login via `id(1)` / SSSD and starts `ansipa-scan-notify.sh` as a background user daemon only for members. Non-members log in unaffected. - -Deleting the `policy-scan-notify` IPA user group causes the timer and profile.d snippet to be removed on the next enforcer run. - ### Daemon Enable / Disable | Group | Effect on join | Effect on leave | @@ -341,30 +325,28 @@ The `.service` suffix is optional — it is appended automatically when the unit State is tracked in `/var/lib/ansipa-policies/daemon-enabled` and `daemon-disabled` so revert actions are applied correctly when a host leaves a group. -### Security Scans & Alert Pipeline +### ClamAV Scan → CheckMK (`dev_mon_clamscan`) + +The scan results go straight to CheckMK — there is no SMB share or desktop-notification pipeline. ``` -Client (policy-security-scan) - └── daily 02:00: clamscan + rkhunter + chkrootkit - └── smbclient → //ipa-server/ansipa-scans/archive//.log +Client (dev_mon_clamscan) + └── cron 02:00: /usr/local/bin/ansipa-clamscan.sh + ├── clamscan + rkhunter + chkrootkit → /var/log/ansipa-clamscan.log + └── writes CheckMK local check /usr/lib/check_mk_agent/local/ansipa_clamscan -IPA container (hourly cron) - └── ansipa-check-scans.sh - └── grep for FOUND / Warning / Possible rootkit / etc. - └── writes /data/scan-results/alerts//.alert - -Client (policy-scan-notify, every 10 min via systemd timer) - └── ansipa-fetch-alerts.sh (root) - └── downloads *.alert files → ~/administration// per active user - └── ansipa-scan-notify.sh (user daemon, started on login via profile.d) - └── notify-send every 10 min while *.alert files remain - └── delete file to acknowledge → removed from server on next fetch +CheckMK agent (installed by dev_mon_base) + └── collects the ansipa_clamscan local check on each poll + └── surfaces the "Ansipa_ClamScan" service: + OK = clean · WARN = stale DB / rkhunter warnings · CRIT = infected ``` -**Prerequisites for scan policies:** -- Add host to `ansipa-module-anti-malware` before `policy-security-scan` (installs ClamAV, rkhunter, chkrootkit). -- `samba-client` is installed automatically by `deploy-ansipa-policies.yml`. -- SMB credentials are written to `/etc/ansipa-smb.creds` (root-only, `0600`). +**Prerequisites:** +- Add the host to `dev_mod_anti-malware` (installs ClamAV, rkhunter, chkrootkit). +- Add the host to `dev_mon_base` (installs the CheckMK agent that collects the check). +- Add the host to `dev_mon_clamscan` (runs the scan + emits the check). + +Leaving `dev_mon_clamscan` removes the cron, scan script, log, and local check on the next enforcer run. ### Local User Lockdown diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh index 41c2fd1..a978b97 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh @@ -12,7 +12,6 @@ # 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. @@ -37,9 +36,10 @@ # Credentials are auto-discovered from the IPA dev_mon_base # description (populated by ansipa-checkmk-setup.sh on the server). # Leaving the group deregisters the host and disables the agent. -# dev_mon_malware Local check reporting ClamAV scan results from the -# dev_security-scan log. Requires dev_security-scan to run first; -# both groups can coexist independently. +# dev_mon_clamscan Run a daily ClamAV + rkhunter + chkrootkit scan (02:00) and report +# the result to CheckMK via a local check (Ansipa_ClamScan). Requires +# dev_mod_anti-malware (tools) and dev_mon_base (agent that collects +# the check). Leaving the group removes the scan + check. # dev_mon_timeshift Local check for most-recent Timeshift snapshot age. # WARN if >5 days, CRIT if >10 days; CRIT if no snapshots found. # dev_mon_power Local check for CPU package power via Intel RAPL (preferred) @@ -65,12 +65,6 @@ # or all members leave, the printer is removed from CUPS on that # host. A profile.d snippet + NOPASSWD sudo rule trigger the # enforcer at login for immediate setup. -# 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. # usr_smb_r_ Mount a read-only Samba share as ~/‹name› for members on any # usr_smb_rw_ enrolled host where they are logged in. rw membership takes # precedence over r when both exist for the same share name. diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh deleted file mode 100644 index 50da0d7..0000000 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh +++ /dev/null @@ -1,157 +0,0 @@ -#!/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 (usr_scan-notify). -# -# For each alert on the server that hasn't been acknowledged yet: -# - Downloads it to ~/administration// for every active login session. -# - A local file that has been deleted counts as acknowledged and is removed -# from the server alerts directory on the next run. -# -# Prerequisites: -# /etc/ansipa-smb.creds — Samba credentials file (deployed by deploy-ansipa-policies.yml) -# /etc/ipa/default.conf — FreeIPA client config (provides server hostname) -# smbclient — from the samba-client package - -set -euo pipefail - -LOG_TAG="ansipa-fetch-alerts" -ADMIN_SUBDIR="administration" -CREDS_FILE="/etc/ansipa-smb.creds" -SMB_SHARE="ansipa-scans" -STATE_DIR="/var/lib/ansipa-policies" -FETCHED_STATE="$STATE_DIR/fetched-alerts" - -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; } - -# ── Prerequisites ───────────────────────────────────────────────────────────── -if [[ ! -f "$CREDS_FILE" ]]; then - warn "Credentials file $CREDS_FILE not found — run deploy-ansipa-policies.yml first." - exit 0 -fi -if ! command -v smbclient &>/dev/null; then - warn "smbclient not installed — install samba-client." - exit 0 -fi - -IPA_SERVER=$(awk '/^server[[:space:]]*=/{print $3}' /etc/ipa/default.conf 2>/dev/null || echo "") -if [[ -z "$IPA_SERVER" ]]; then - warn "Cannot read IPA server from /etc/ipa/default.conf — host enrolled?" - exit 0 -fi - -HOSTNAME=$(hostname -f 2>/dev/null || hostname) -mkdir -p "$STATE_DIR" -touch "$FETCHED_STATE" - -smb() { smbclient "//$IPA_SERVER/$SMB_SHARE" -A "$CREDS_FILE" "$@" 2>/dev/null; } - -# ── List active login sessions (usr_scan-notify members only) ───────────────── -# loginctl list-sessions --no-legend: SESSION UID USER SEAT TTY → $3 = USER -# who: USER TTY DATE TIME (HOST) → $1 = USER -# Only deliver to members of the usr_scan-notify FreeIPA user group. -ACTIVE_USERS=() -_parse_session_user() { - local line="$1" src="$2" - local user - if [[ "$src" == "loginctl" ]]; then - user=$(awk '{print $3}' <<< "$line") - else - user=$(awk '{print $1}' <<< "$line") - fi - [[ -z "$user" || "$user" == "root" || "$user" == "USER" ]] && return - # Only deliver to members of usr_scan-notify. - id -nG "$user" 2>/dev/null | tr ' ' '\n' | grep -qxF "usr_scan-notify" || return - local home - home=$(getent passwd "$user" | cut -d: -f6 2>/dev/null) || return - [[ -d "$home" ]] && ACTIVE_USERS+=("$user:$home") -} - -if loginctl list-sessions --no-legend &>/dev/null; then - while IFS= read -r LINE; do - _parse_session_user "$LINE" "loginctl" - done < <(loginctl list-sessions --no-legend 2>/dev/null) -else - while IFS= read -r LINE; do - _parse_session_user "$LINE" "who" - done < <(who 2>/dev/null) -fi -# Deduplicate by user. -mapfile -t ACTIVE_USERS < <(printf '%s\n' "${ACTIVE_USERS[@]+"${ACTIVE_USERS[@]}"}" | sort -u) - -# ── List alerts on server for this host ─────────────────────────────────────── -# smbclient `ls path\` treats the argument as a pattern, not a cd target; -# use `cd path; ls` to list directory contents instead. -SERVER_ALERTS=() -while IFS= read -r LINE; do - # smbclient ls output: " filename.alert A 1234 date" - FILE=$(awk '{print $1}' <<< "$LINE") - [[ "$FILE" == *.alert ]] && SERVER_ALERTS+=("$FILE") -done < <(smb -c "cd alerts\\$HOSTNAME; ls" 2>/dev/null || true) - -# ── Check for locally deleted alerts (acknowledged) ─────────────────────────── -ACKNOWLEDGED=() -while IFS= read -r ALERT_NAME; do - [[ -z "$ALERT_NAME" ]] && continue - # If none of the active users still have this alert file, it was acknowledged. - ALL_DELETED=true - for USER_INFO in "${ACTIVE_USERS[@]}"; do - HOME_DIR="${USER_INFO#*:}" - USER="${USER_INFO%%:*}" - LOCAL_FILE="$HOME_DIR/$ADMIN_SUBDIR/$HOSTNAME/$ALERT_NAME" - if [[ -f "$LOCAL_FILE" ]]; then - ALL_DELETED=false - break - fi - done - if [[ "$ALL_DELETED" == true ]] && [[ ${#ACTIVE_USERS[@]} -gt 0 ]]; then - log "Alert acknowledged (deleted locally): $ALERT_NAME — removing from server" - smb -c "del alerts\\$HOSTNAME\\$ALERT_NAME" 2>/dev/null || true - # Remove from state file and skip re-download in this run. - sed -i "/^$ALERT_NAME\$/d" "$FETCHED_STATE" 2>/dev/null || true - ACKNOWLEDGED+=("$ALERT_NAME") - fi -done < "$FETCHED_STATE" - -# ── Download new/pending alerts to user home dirs ───────────────────────────── -TMP_DIR=$(mktemp -d /tmp/ansipa-alerts.XXXXXX) -trap 'rm -rf "$TMP_DIR"' EXIT - -for ALERT_NAME in "${SERVER_ALERTS[@]}"; do - # Skip alerts already acknowledged (and deleted from server) in this run. - [[ " ${ACKNOWLEDGED[*]+"${ACKNOWLEDGED[*]}"} " == *" $ALERT_NAME "* ]] && continue - - TMP_FILE="$TMP_DIR/$ALERT_NAME" - - # Download alert content from server. - smb -c "get alerts\\$HOSTNAME\\$ALERT_NAME $TMP_FILE" 2>/dev/null || { - warn "Failed to download alert: $ALERT_NAME" - continue - } - - NEW=false - for USER_INFO in "${ACTIVE_USERS[@]}"; do - HOME_DIR="${USER_INFO#*:}" - USER="${USER_INFO%%:*}" - LOCAL_DIR="$HOME_DIR/$ADMIN_SUBDIR/$HOSTNAME" - LOCAL_FILE="$LOCAL_DIR/$ALERT_NAME" - mkdir -p "$LOCAL_DIR" - chown "$USER" "$LOCAL_DIR" 2>/dev/null || true - - if [[ ! -f "$LOCAL_FILE" ]]; then - cp "$TMP_FILE" "$LOCAL_FILE" - chown "$USER" "$LOCAL_FILE" - chmod 644 "$LOCAL_FILE" - NEW=true - fi - done - - # Track fetched alerts so we can detect acknowledgment on the next run. - if ! grep -qx "$ALERT_NAME" "$FETCHED_STATE" 2>/dev/null; then - echo "$ALERT_NAME" >> "$FETCHED_STATE" - fi - - [[ "$NEW" == true ]] && log "New alert delivered: $ALERT_NAME" -done - -log "Done. ${#SERVER_ALERTS[@]} server alert(s) for $HOSTNAME." diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-scan-notify.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-scan-notify.sh deleted file mode 100644 index df106d8..0000000 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-scan-notify.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -# ansipa-scan-notify.sh — user-session scan alert notification daemon. -# Started automatically on login via /etc/profile.d/ansipa-notify.sh. -# -# Behaviour: -# - Checks ~/administration/ for *.alert files every 10 minutes. -# - Sends a desktop notification (notify-send) for any unacknowledged alerts. -# - Re-notifies every 10 minutes as long as alert files remain. -# - Deleting an alert file counts as acknowledgment — notifications stop. -# - Exits when no alert files remain AND none have been seen this session, -# but keeps running once any alert is ever found (to catch future ones). - -ADMIN_DIR="$HOME/administration" -NOTIFY_INTERVAL=600 # 10 minutes -ICON="security-high" # freedesktop icon name - -notified_once=false - -notify_alerts() { - local alerts=() file count=0 - - mapfile -t alerts < <(find "$ADMIN_DIR" -name "*.alert" 2>/dev/null | sort) - count=${#alerts[@]} - - [[ $count -eq 0 ]] && return 0 - - local title body - if [[ $count -eq 1 ]]; then - local name - name=$(basename "${alerts[0]}" .alert) - title="Security alert: $name" - body="Check $ADMIN_DIR\nDelete the file to acknowledge." - else - title="$count unacknowledged security alerts" - body="Check $ADMIN_DIR\nDelete files to acknowledge." - fi - - notify-send -u critical -i "$ICON" -t 0 "$title" "$body" 2>/dev/null \ - || notify-send -u critical "$title" "$body" 2>/dev/null \ - || echo "[ansipa-notify] ALERT: $title — $body" >&2 - - notified_once=true -} - -mkdir -p "$ADMIN_DIR" - -while true; do - notify_alerts - sleep "$NOTIFY_INTERVAL" -done diff --git a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml index 2be68c7..6bcf7a5 100644 --- a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml +++ b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml @@ -10,7 +10,6 @@ # 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 # dev_ssh_ Distribute 's SSH public keys from IPA to authorized_keys @@ -22,7 +21,6 @@ # usr_admin Grant full sudo on every enrolled host (sudoers drop-in, SSSD-resolved) # usr_block-binary- Block execution of via a PATH-priority wrapper # usr_prt_ Auto-add printer at login (per-user); URI in IPA group description -# usr_scan-notify Fetch alerts from server, notify user every 10 min until acknowledged # usr_smb_r_ Mount read-only Samba share ~/‹name› for members; credentials in IPA group description # usr_smb_rw_ Mount read-write Samba share ~/‹name› for members (rw beats r if both) # usr_smb_adm_policystore Mount the policy store at ~/policystore (rw) for admin members; @@ -33,8 +31,10 @@ # Credentials auto-discovered from IPA dev_mon_base description (set by # ansipa-checkmk-setup.sh on the FreeIPA container). # CPU/RAM/disk/uptime reported by built-in agent sections. -# dev_mon_malware Local check reporting ClamAV scan results from dev_security-scan log. -# Complement — does not replace dev_security-scan; both can coexist. +# dev_mon_clamscan Run daily ClamAV + rkhunter + chkrootkit scan (02:00) and report the +# result to CheckMK via a local check (Ansipa_ClamScan). Also add the +# host to dev_mod_anti-malware (installs the tools) and dev_mon_base +# (installs the agent that collects the check). # dev_mon_timeshift Local check for most recent Timeshift snapshot age. # WARN if >5 days, CRIT if >10 days, CRIT if no snapshots found. # dev_mon_power Local check for CPU package TDP/power via Intel RAPL or lm-sensors. @@ -43,31 +43,25 @@ # # Prerequisites: # - Host enrolled in FreeIPA (sssd + ipa CLI available) -# - 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 +# - For dev_mon_clamscan tools: also add host to dev_mod_anti-malware group +# - For timeshift-backup: also add host to dev_mod_timeshift group # - For dev_mon_*: CheckMK CE must be up; run ansipa-checkmk-setup.sh on the FreeIPA # container first (seeds credentials + groups in IPA) # - For usr_smb_r_*/usr_smb_rw_*/usr_smb_adm_policystore: cifs-utils installed (handled below); # Samba service running on the IPA container (ansipa-smb.service auto-starts) # # Usage: -# ansible-playbook -i inventory deploy-ansipa-policies.yml \ -# -e smb_scan_password= # or use --vault-password-file +# ansible-playbook -i inventory deploy-ansipa-policies.yml # # To seed the policystore on the FreeIPA container for the first time (bootstrap only; # thereafter edit files directly via the SMB share at ~/policystore): # ansible-playbook -i inventory deploy-ansipa-policies.yml \ -# -e smb_scan_password= -e seed_policystore=true -e ipa_server_host= +# -e seed_policystore=true -e ipa_server_host= - name: Deploy FreeIPA policy enforcer hosts: all become: yes - vars: - smb_scan_password: "{{ smb_scan_password | mandatory('smb_scan_password is required — use -e smb_scan_password=... or ansible-vault') }}" - tasks: - name: Create ansipa library and policy directories @@ -104,35 +98,12 @@ - lm_sensors # needed for dev_mon_power fallback (Intel RAPL preferred) ignore_errors: yes - - name: Deploy SMB credentials file - copy: - dest: /etc/ansipa-smb.creds - mode: '0600' - owner: root - group: root - content: | - username = scanupload - password = {{ smb_scan_password }} - domain = WORKGROUP - - name: Deploy policy enforcer script copy: src: ansipa-enforce-policies.sh dest: /usr/local/bin/ansipa-enforce-policies.sh mode: '0755' - - name: Deploy alert fetch script - copy: - src: ansipa-fetch-alerts.sh - dest: /usr/local/bin/ansipa-fetch-alerts.sh - mode: '0755' - - - name: Deploy user notification daemon - copy: - src: ansipa-scan-notify.sh - dest: /usr/local/bin/ansipa-scan-notify.sh - mode: '0755' - - name: Create policy state directory file: path: /var/lib/ansipa-policies diff --git a/setup/modules/FreeipaAnsible/ansible/lib/ansipa-policy.sh b/setup/modules/FreeipaAnsible/ansible/lib/ansipa-policy.sh index 3acd550..c54db85 100644 --- a/setup/modules/FreeipaAnsible/ansible/lib/ansipa-policy.sh +++ b/setup/modules/FreeipaAnsible/ansible/lib/ansipa-policy.sh @@ -9,9 +9,9 @@ # RAW_GROUPS (comma-sep host-group string from ipa host-show) # _ALL_USER_GROUPS (newline-sep full list from one ipa group-find call) # ACTIVE_DAEMON_ENABLE ACTIVE_DAEMON_DISABLE (arrays, unit name suffixes) -# WANT_TIMESHIFT_BACKUP WANT_SECURITY_SCAN WANT_NO_LOCAL_USERS (booleans) +# WANT_TIMESHIFT_BACKUP WANT_CLAMSCAN WANT_NO_LOCAL_USERS (booleans) # ACTIVE_LOCAL_SUDO_USERS ACTIVE_SSH_USERS ACTIVE_MON_DEV (arrays) -# WANT_USR_ADMIN WANT_SCAN_NOTIFY (booleans) +# WANT_USR_ADMIN (boolean) # ACTIVE_BLOCK_BINARIES ACTIVE_BLOCK_IPA_GROUPS (parallel arrays) # ACTIVE_PRT_GROUPS ACTIVE_PRT_PRINTERS (parallel arrays) # ACTIVE_MON_USR (array, usr_mon_* suffixes) @@ -63,7 +63,7 @@ _ansipa_discover() { ACTIVE_SSH_USERS=() ACTIVE_MON_DEV=() WANT_TIMESHIFT_BACKUP=false - WANT_SECURITY_SCAN=false + WANT_CLAMSCAN=false WANT_NO_LOCAL_USERS=false if [[ -n "$RAW_GROUPS" ]]; then @@ -74,10 +74,13 @@ _ansipa_discover() { 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-}") ;; dev_ssh_*) ACTIVE_SSH_USERS+=("${g#dev_ssh_}") ;; + # dev_mon_clamscan runs the scan + emits its own CheckMK check; + # match it BEFORE the generic dev_mon_* catch-all so it sets + # WANT_CLAMSCAN instead of being treated as a dev_mon.sh check. + dev_mon_clamscan) WANT_CLAMSCAN=true ;; dev_mon_*) ACTIVE_MON_DEV+=("${g#dev_mon_}") ;; esac done @@ -89,7 +92,6 @@ _ansipa_discover() { | awk '/Group name:/ {print $NF}' | sort -u || true) WANT_USR_ADMIN=false - WANT_SCAN_NOTIFY=false ACTIVE_BLOCK_BINARIES=() ACTIVE_BLOCK_IPA_GROUPS=() ACTIVE_PRT_GROUPS=() @@ -100,7 +102,6 @@ _ansipa_discover() { [[ -z "$_grp" ]] && continue case "$_grp" in usr_admin) WANT_USR_ADMIN=true ;; - usr_scan-notify) WANT_SCAN_NOTIFY=true ;; usr_block-binary-*) local _raw="${_grp#usr_block-binary-}" ACTIVE_BLOCK_BINARIES+=("${_raw//__/.}") @@ -116,13 +117,13 @@ _ansipa_discover() { log "Device policies — daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none}" \ "| daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \ - "| timeshift-backup: $WANT_TIMESHIFT_BACKUP | security-scan: $WANT_SECURITY_SCAN" \ + "| timeshift-backup: $WANT_TIMESHIFT_BACKUP | clamscan: $WANT_CLAMSCAN" \ "| no-local-users: $WANT_NO_LOCAL_USERS | local-sudo: ${ACTIVE_LOCAL_SUDO_USERS[*]:-none}" \ "| ssh-keys: ${ACTIVE_SSH_USERS[*]:-none} | mon-dev: ${ACTIVE_MON_DEV[*]:-none}" log "User policies — admin: $WANT_USR_ADMIN" \ "| block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \ "| printers: ${ACTIVE_PRT_PRINTERS[*]:-none}" \ - "| scan-notify: $WANT_SCAN_NOTIFY | mon-usr: ${ACTIVE_MON_USR[*]:-none}" + "| mon-usr: ${ACTIVE_MON_USR[*]:-none}" } # ── SMB credential parser ───────────────────────────────────────────────────── diff --git a/setup/modules/FreeipaAnsible/ansible/policies.d/dev_mon.sh b/setup/modules/FreeipaAnsible/ansible/policies.d/dev_mon.sh index d9ec71f..7d09c50 100644 --- a/setup/modules/FreeipaAnsible/ansible/policies.d/dev_mon.sh +++ b/setup/modules/FreeipaAnsible/ansible/policies.d/dev_mon.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash # policy: dev_mon — CheckMK monitoring agent + local checks. # dev_mon_base install agent, register host, installed-packages check -# dev_mon_malware ClamAV scan result check # dev_mon_timeshift Timeshift snapshot age check # dev_mon_power CPU package power via RAPL / lm-sensors # usr_mon_logins SSH login audit check (user group, but requires dev_mon_base) +# (ClamAV scan reporting lives in its own policy, dev_mon_clamscan, which runs +# the scan and writes the Ansipa_ClamScan local check the agent here collects.) # Revert: deregisters the host and removes the agent socket when dev_mon_base leaves. MON_LOCAL_DIR="/usr/lib/check_mk_agent/local" @@ -218,34 +219,12 @@ elif command -v pacman &>/dev/null; then fi echo "0 Ansipa_Packages packages=${PKG_COUNT} ${PKG_COUNT} packages installed. Recent: ${RECENT}"' - # ── dev_mon_malware: ClamAV scan result check ───────────────────────── - if _mon_dev_active "malware"; then - _cmk_write_check "ansipa_clamav" '#!/bin/bash -LOG=/var/log/ansipa-security-scan.log -CLAMDB=/var/lib/clamav/main.cvd -[[ -f /var/lib/clamav/main.cld ]] && CLAMDB=/var/lib/clamav/main.cld -if [[ ! -f "$LOG" ]]; then - echo "3 Ansipa_ClamAV - No scan log found. Add host to dev_security-scan first." - exit 0 -fi -LAST_DATE=$(grep "^=== ansipa-security-scan:" "$LOG" 2>/dev/null | tail -1 \ - | grep -oE "[0-9]{4}-[0-9]{2}-[0-9]{2}" || echo "") -INFECTED=$(grep -c "FOUND$" "$LOG" 2>/dev/null) || INFECTED=0 -DB_DAYS=999 -if [[ -f "$CLAMDB" ]]; then - DB_DAYS=$(( ( $(date +%s) - $(stat -c %Y "$CLAMDB" 2>/dev/null || echo 0) ) / 86400 )) -fi -PERF="infected=${INFECTED};0;0 db_age_days=${DB_DAYS};7;30" -if [[ -z "$LAST_DATE" ]]; then - echo "1 Ansipa_ClamAV ${PERF} No scan found yet" -elif [[ "$INFECTED" -gt 0 ]]; then - echo "2 Ansipa_ClamAV ${PERF} INFECTED: ${INFECTED} threat(s) found — last scan: ${LAST_DATE}" -elif [[ "$DB_DAYS" -gt 7 ]]; then - echo "1 Ansipa_ClamAV ${PERF} DB stale (${DB_DAYS}d). Last scan: ${LAST_DATE}" -else - echo "0 Ansipa_ClamAV ${PERF} Clean — last scan: ${LAST_DATE}, DB age: ${DB_DAYS}d" -fi' - fi + # NOTE: ClamAV scan reporting is handled by the dev_mon_clamscan policy, + # which runs the scan and writes its own CheckMK local check + # (ansipa_clamscan) directly — so the old dev_mon_malware group and its + # ansipa_clamav check are gone. The stale ansipa_clamav file is cleaned + # up automatically by the local-check cleanup below (it is no longer in + # _WANT_CHECKS). # ── dev_mon_timeshift: snapshot age check ───────────────────────────── if _mon_dev_active "timeshift"; then diff --git a/setup/modules/FreeipaAnsible/ansible/policies.d/dev_mon_clamscan.sh b/setup/modules/FreeipaAnsible/ansible/policies.d/dev_mon_clamscan.sh new file mode 100644 index 0000000..e18d095 --- /dev/null +++ b/setup/modules/FreeipaAnsible/ansible/policies.d/dev_mon_clamscan.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# policy: dev_mon_clamscan — daily ClamAV + rkhunter + chkrootkit scan at 02:00, +# with results reported natively to CheckMK. +# Applied when the host is a member of the dev_mon_clamscan host-group. +# +# Results are handed to CheckMK via a local check (ansipa_clamscan) rather than +# uploaded to an SMB share: the CheckMK agent (installed by dev_mon_base) picks +# up the local check and surfaces the scan status as a monitored service. Add a +# host to BOTH dev_mon_clamscan (to run the scan + emit the check) and dev_mon_base +# (to install the agent that collects it) to see results in CheckMK. + +SCAN_CRON="$CRON_DIR/ansipa-clamscan" +SCAN_SCRIPT="/usr/local/bin/ansipa-clamscan.sh" +SCAN_LOG="/var/log/ansipa-clamscan.log" +CMK_LOCAL_DIR="/usr/lib/check_mk_agent/local" +CMK_CHECK="$CMK_LOCAL_DIR/ansipa_clamscan" + +if [[ "$WANT_CLAMSCAN" == true ]]; then + # (Re-)write the scan script so it stays current with this version of the enforcer. + cat > "$SCAN_SCRIPT" <<'SCAN' +#!/bin/bash +# ansipa-clamscan — daily ClamAV / rkhunter / chkrootkit run. +# Managed by ansipa-enforce-policies — do not edit manually. +# Results are read by the ansipa_clamscan CheckMK local check; no SMB upload. +LOG=/var/log/ansipa-clamscan.log +# `hostname` (inetutils) may be absent on minimal Arch; resolve without it. +HOSTNAME=$( { command -v hostname >/dev/null 2>&1 && hostname -f 2>/dev/null; } \ + || hostnamectl --static 2>/dev/null || cat /etc/hostname 2>/dev/null \ + || cat /proc/sys/kernel/hostname 2>/dev/null ) +DATE=$(date +%Y-%m-%d) +{ + echo "=== ansipa-clamscan: $DATE $HOSTNAME ===" + + if command -v freshclam &>/dev/null; then + freshclam --quiet 2>/dev/null || true + fi + if command -v clamscan &>/dev/null; then + clamscan -r --infected --quiet /home /etc /tmp /var/tmp 2>/dev/null || true + fi + if command -v rkhunter &>/dev/null; then + rkhunter --update --quiet 2>/dev/null || true + rkhunter --check --skip-keypress --quiet 2>/dev/null || true + fi + if command -v chkrootkit &>/dev/null; then + chkrootkit 2>/dev/null || true + fi + + echo "=== scan complete ===" +} >> "$LOG" 2>&1 +SCAN + chmod 755 "$SCAN_SCRIPT" + + # ── CheckMK local check: report scan results to CheckMK ─────────────────── + # Emitted regardless of whether the CheckMK agent is present yet; once + # dev_mon_base installs the agent, it collects this check automatically. + mkdir -p "$CMK_LOCAL_DIR" + cat > "$CMK_CHECK" <<'CHECK' +#!/bin/bash +# ansipa_clamscan — CheckMK local check reporting the ansipa-clamscan result. +# Managed by ansipa-enforce-policies (dev_mon_clamscan) — do not edit manually. +LOG=/var/log/ansipa-clamscan.log +CLAMDB=/var/lib/clamav/main.cvd +[[ -f /var/lib/clamav/main.cld ]] && CLAMDB=/var/lib/clamav/main.cld +if [[ ! -f "$LOG" ]]; then + echo "1 Ansipa_ClamScan - No scan has run yet (scans run daily at 02:00)." + exit 0 +fi +LAST_DATE=$(grep "^=== ansipa-clamscan:" "$LOG" 2>/dev/null | tail -1 \ + | grep -oE "[0-9]{4}-[0-9]{2}-[0-9]{2}" || echo "") +INFECTED=$(grep -c "FOUND$" "$LOG" 2>/dev/null) || INFECTED=0 +RKWARN=$(grep -c "Warning:" "$LOG" 2>/dev/null) || RKWARN=0 +DB_DAYS=999 +if [[ -f "$CLAMDB" ]]; then + DB_DAYS=$(( ( $(date +%s) - $(stat -c %Y "$CLAMDB" 2>/dev/null || echo 0) ) / 86400 )) +fi +PERF="infected=${INFECTED};1;1 rkhunter_warnings=${RKWARN} db_age_days=${DB_DAYS};7;30" +if [[ -z "$LAST_DATE" ]]; then + echo "1 Ansipa_ClamScan ${PERF} No completed scan found yet" +elif [[ "$INFECTED" -gt 0 ]]; then + echo "2 Ansipa_ClamScan ${PERF} INFECTED: ${INFECTED} threat(s) — last scan: ${LAST_DATE}" +elif [[ "$DB_DAYS" -gt 7 ]]; then + echo "1 Ansipa_ClamScan ${PERF} ClamAV DB stale (${DB_DAYS}d). Last scan: ${LAST_DATE}" +elif [[ "$RKWARN" -gt 0 ]]; then + echo "1 Ansipa_ClamScan ${PERF} Clean, but ${RKWARN} rkhunter warning(s) — last scan: ${LAST_DATE}" +else + echo "0 Ansipa_ClamScan ${PERF} Clean — last scan: ${LAST_DATE}, DB age: ${DB_DAYS}d" +fi +CHECK + chmod 755 "$CMK_CHECK" + + if [[ ! -f "$SCAN_CRON" ]]; then + log "Enabling daily ClamAV scan (dev_mon_clamscan) with CheckMK reporting" + # /etc/cron.d may not exist yet if cron isn't installed — create it so the + # drop-in lands (cronie/anti-malware tooling comes via dev_mod_anti-malware). + mkdir -p "$CRON_DIR" + cat > "$SCAN_CRON" <<'CRON' +# ansipa-dev_mon_clamscan: managed by ansipa-enforce-policies — do not edit manually. +# Install scan tools by adding the host to the dev_mod_anti-malware group. +# Results appear in CheckMK (Ansipa_ClamScan) when the host is also in dev_mon_base. +0 2 * * * root /usr/local/bin/ansipa-clamscan.sh +CRON + chmod 644 "$SCAN_CRON" + fi +else + if [[ -f "$SCAN_CRON" || -f "$SCAN_SCRIPT" || -f "$CMK_CHECK" ]]; then + rm -f "$SCAN_CRON" "$SCAN_SCRIPT" "$CMK_CHECK" "$SCAN_LOG" + log "Removed dev_mon_clamscan policy (host left dev_mon_clamscan group)" + fi +fi diff --git a/setup/modules/FreeipaAnsible/ansible/policies.d/dev_security-scan.sh b/setup/modules/FreeipaAnsible/ansible/policies.d/dev_security-scan.sh deleted file mode 100644 index 18285f2..0000000 --- a/setup/modules/FreeipaAnsible/ansible/policies.d/dev_security-scan.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash -# policy: dev_security-scan — daily ClamAV + rkhunter + chkrootkit scans + SMB upload at 02:00. -# Applied when host is member of dev_security-scan host-group. - -SCAN_CRON="$CRON_DIR/ansipa-security-scan" -SCAN_SCRIPT="/usr/local/bin/ansipa-security-scan.sh" - -if [[ "$WANT_SECURITY_SCAN" == true ]]; then - # (Re-)write the scan script so it stays current with this version of the enforcer. - cat > "$SCAN_SCRIPT" <<'SCAN' -#!/bin/bash -# ansipa-security-scan — daily ClamAV / rkhunter / chkrootkit run + SMB upload. -# Managed by ansipa-enforce-policies — do not edit manually. -LOG=/var/log/ansipa-security-scan.log -# `hostname` (inetutils) may be absent on minimal Arch; resolve without it. -HOSTNAME=$( { command -v hostname >/dev/null 2>&1 && hostname -f 2>/dev/null; } \ - || hostnamectl --static 2>/dev/null || cat /etc/hostname 2>/dev/null \ - || cat /proc/sys/kernel/hostname 2>/dev/null ) -DATE=$(date +%Y-%m-%d) -{ - echo "=== ansipa-security-scan: $DATE $HOSTNAME ===" - - if command -v freshclam &>/dev/null; then - freshclam --quiet 2>/dev/null || true - fi - if command -v clamscan &>/dev/null; then - clamscan -r --infected --quiet /home /etc /tmp /var/tmp 2>/dev/null || true - fi - if command -v rkhunter &>/dev/null; then - rkhunter --update --quiet 2>/dev/null || true - rkhunter --check --skip-keypress --quiet 2>/dev/null || true - fi - if command -v chkrootkit &>/dev/null; then - chkrootkit 2>/dev/null || true - fi - - echo "=== scan complete ===" -} >> "$LOG" 2>&1 - -# ── Upload to server SMB share ──────────────────────────────────────────────── -IPA_SERVER=$(awk '/^server[[:space:]]*=/{print $3}' /etc/ipa/default.conf 2>/dev/null || echo "") -if [[ -n "$IPA_SERVER" ]] && [[ -f /etc/ansipa-smb.creds ]] && command -v smbclient &>/dev/null; then - # Create host archive dir (mkdir is idempotent; errors suppressed). - smbclient "//$IPA_SERVER/ansipa-scans" -A /etc/ansipa-smb.creds \ - -c "mkdir archive; mkdir archive\\$HOSTNAME; put $LOG archive\\$HOSTNAME\\$DATE.log" \ - >> "$LOG" 2>&1 \ - && echo "[ansipa] Scan results uploaded to $IPA_SERVER/ansipa-scans/archive/$HOSTNAME/$DATE.log" >> "$LOG" \ - || echo "[ansipa][WARN] SMB upload failed — results remain local at $LOG" >> "$LOG" -else - echo "[ansipa] SMB upload skipped (no credentials or smbclient not found)." >> "$LOG" -fi -SCAN - chmod 755 "$SCAN_SCRIPT" - - if [[ ! -f "$SCAN_CRON" ]]; then - log "Enabling daily security scans (ClamAV / rkhunter / chkrootkit)" - cat > "$SCAN_CRON" <<'CRON' -# 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" - fi -else - if [[ -f "$SCAN_CRON" ]]; then - rm -f "$SCAN_CRON" - rm -f "$SCAN_SCRIPT" - log "Removed security scan policy (host left dev_security-scan group)" - fi -fi diff --git a/setup/modules/FreeipaAnsible/ansible/policies.d/usr_scan-notify.sh b/setup/modules/FreeipaAnsible/ansible/policies.d/usr_scan-notify.sh deleted file mode 100644 index 40bd6ba..0000000 --- a/setup/modules/FreeipaAnsible/ansible/policies.d/usr_scan-notify.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -# policy: usr_scan-notify — scan alert notification daemon for IPA group members. -# When the usr_scan-notify user group exists: installs a systemd fetch-alerts timer -# (fleet-wide) and a profile.d snippet that starts the notification daemon on login -# for group members (checked at login time via id/SSSD). - -FETCH_SVC="/etc/systemd/system/ansipa-fetch-alerts.service" -FETCH_TIMER="/etc/systemd/system/ansipa-fetch-alerts.timer" -NOTIFY_PROFILED="/etc/profile.d/ansipa-notify.sh" - -if [[ "$WANT_SCAN_NOTIFY" == true ]]; then - if [[ ! -x /usr/local/bin/ansipa-fetch-alerts.sh ]]; then - warn "ansipa-fetch-alerts.sh not found — run deploy-ansipa-policies.yml first." - fi - - if [[ ! -f "$FETCH_SVC" ]]; then - log "Installing ansipa-fetch-alerts systemd service + timer" - cat > "$FETCH_SVC" <<'UNIT' -[Unit] -Description=Fetch Ansipa security alerts from the server SMB share -After=network-online.target sssd.service -Wants=network-online.target - -[Service] -Type=oneshot -ExecStart=/usr/local/bin/ansipa-fetch-alerts.sh -StandardOutput=journal -StandardError=journal -UNIT - - cat > "$FETCH_TIMER" <<'UNIT' -[Unit] -Description=Periodic ansipa security alert fetch - -[Timer] -OnBootSec=2min -OnUnitActiveSec=10min - -[Install] -WantedBy=timers.target -UNIT - systemctl daemon-reload - systemctl enable --now ansipa-fetch-alerts.timer - log "ansipa-fetch-alerts.timer enabled" - fi - - if [[ ! -f "$NOTIFY_PROFILED" ]]; then - 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 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 "usr_scan-notify" && \ - ! pgrep -u "$(id -u)" -f "ansipa-scan-notify" >/dev/null 2>&1; then - "$_NOTIFY_DAEMON" & - disown -fi -unset _NOTIFY_DAEMON -PROFILED - chmod 644 "$NOTIFY_PROFILED" - fi -else - if [[ -f "$FETCH_TIMER" ]]; then - 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 (usr_scan-notify user group no longer exists)" - fi - if [[ -f "$NOTIFY_PROFILED" ]]; then - rm -f "$NOTIFY_PROFILED" - log "Removed /etc/profile.d/ansipa-notify.sh" - fi -fi diff --git a/setup/modules/FreeipaAnsible/image/.env.example b/setup/modules/FreeipaAnsible/image/.env.example index 6827c3d..c710588 100644 --- a/setup/modules/FreeipaAnsible/image/.env.example +++ b/setup/modules/FreeipaAnsible/image/.env.example @@ -9,15 +9,14 @@ IPA_DNS_FORWARDER= IPA_SETUP_KRA=false # ── Ansipa SMB shares ───────────────────────────────────────────────────────── -# SMB_SCAN_PASSWORD — password for 'scanupload'; deploy to clients via Ansible -# with smb_scan_password= (use ansible-vault). # LUKS_KEY_UPLOAD_PASSWORD — password for the 'luks-upload' service account used # by the Ansible controller to write LUKS backup keys to # the ansipa-luks-keys share. Pass to collect-luks-keys.yml # with -e luks_upload_password=. # To grant read access, add a Samba user to KeyAdmin on the # container: useradd -r -G KeyAdmin && smbpasswd -a -SMB_SCAN_PASSWORD=ChangeMe_ScanPass! +# (ClamAV scan results now go to CheckMK via the dev_mon_clamscan local check — +# there is no longer an ansipa-scans SMB share or scanupload account.) LUKS_KEY_UPLOAD_PASSWORD=ChangeMe_LuksUpload! # ── CheckMK CE monitoring ───────────────────────────────────────────────────── diff --git a/setup/modules/FreeipaAnsible/image/Dockerfile b/setup/modules/FreeipaAnsible/image/Dockerfile index 2a83f7e..d4f1b12 100644 --- a/setup/modules/FreeipaAnsible/image/Dockerfile +++ b/setup/modules/FreeipaAnsible/image/Dockerfile @@ -12,7 +12,6 @@ # -e IPA_DOMAIN=example.com \ # -e IPA_ADMIN_PASSWORD=Secret123 \ # -e IPA_DM_PASSWORD=Secret456 \ -# -e SMB_SCAN_PASSWORD=Secret789 \ # -p 443:443 -p 389:389 -p 636:636 -p 88:88 -p 445:445 \ # freeipa-server # @@ -64,12 +63,10 @@ COPY ipa-first-boot.sh /usr/local/sbin/ipa-first-boot.sh COPY ipa-first-boot.service /etc/systemd/system/ipa-first-boot.service COPY ansipa-smb-setup.sh /usr/local/sbin/ansipa-smb-setup.sh COPY ansipa-smb.service /etc/systemd/system/ansipa-smb.service -COPY ansipa-check-scans.sh /usr/local/sbin/ansipa-check-scans.sh COPY ansipa-checkmk-setup.sh /usr/local/sbin/ansipa-checkmk-setup.sh COPY ansipa-checkmk.service /etc/systemd/system/ansipa-checkmk.service RUN chmod +x /usr/local/sbin/ipa-first-boot.sh \ && chmod +x /usr/local/sbin/ansipa-smb-setup.sh \ - && chmod +x /usr/local/sbin/ansipa-check-scans.sh \ && chmod +x /usr/local/sbin/ansipa-checkmk-setup.sh \ && systemctl enable docker-env.service \ && systemctl enable ipa-first-boot.service \ diff --git a/setup/modules/FreeipaAnsible/image/ansipa-check-scans.sh b/setup/modules/FreeipaAnsible/image/ansipa-check-scans.sh deleted file mode 100644 index 6a0644f..0000000 --- a/setup/modules/FreeipaAnsible/image/ansipa-check-scans.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash -# ansipa-check-scans.sh — analyse client scan logs and create alert files. -# Runs hourly via /etc/cron.d/ansipa-check-scans (installed by ansipa-smb-setup.sh). -# -# Input: /data/scan-results/archive//.log -# Output: /data/scan-results/alerts//.alert -# (created only when concerning patterns are found; client deletes to acknowledge) - -SCAN_BASE="/data/scan-results" -ARCHIVE_DIR="$SCAN_BASE/archive" -ALERT_DIR="$SCAN_BASE/alerts" -LOG=/var/log/ansipa-check-scans.log - -log() { printf '[%s] [ansipa-check-scans] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG"; } - -# Patterns that indicate a concerning scan result (case-insensitive). -CONCERN_PATTERNS=( - "FOUND" # ClamAV: virus or trojan found - "Infected files: [^0]" # ClamAV summary with non-zero count - "Warning:" # rkhunter warning - "Possible rootkit" # rkhunter - "INFECTED" # generic - "Suspicious file" # chkrootkit - "INFECTED SOURCE" # chkrootkit -) - -shopt -s nullglob - -for HOST_DIR in "$ARCHIVE_DIR"/*/; do - [[ -d "$HOST_DIR" ]] || continue - HOSTNAME=$(basename "$HOST_DIR") - mkdir -p "$ALERT_DIR/$HOSTNAME" - - for SCAN_LOG in "$HOST_DIR"*.log; do - [[ -f "$SCAN_LOG" ]] || continue - LOG_DATE=$(basename "$SCAN_LOG" .log) - ALERT_FILE="$ALERT_DIR/$HOSTNAME/$LOG_DATE.alert" - - # Skip if we already generated an alert for this log. - [[ -f "$ALERT_FILE" ]] && continue - - FINDINGS=() - for PATTERN in "${CONCERN_PATTERNS[@]}"; do - while IFS= read -r LINE; do - FINDINGS+=("$LINE") - done < <(grep -iE "$PATTERN" "$SCAN_LOG" 2>/dev/null || true) - done - - # Deduplicate. - mapfile -t FINDINGS < <(printf '%s\n' "${FINDINGS[@]}" | sort -u) - - if [[ ${#FINDINGS[@]} -gt 0 ]]; then - log "ALERT: $HOSTNAME / $LOG_DATE — ${#FINDINGS[@]} finding(s)" - { - printf '=== Ansipa Security Alert ===\n' - printf 'Host: %s\n' "$HOSTNAME" - printf 'Scan: %s\n' "$LOG_DATE" - printf 'Findings: %d\n' "${#FINDINGS[@]}" - printf '\nConcerning lines:\n' - printf ' %s\n' "${FINDINGS[@]}" - printf '\nFull log: %s\n' "$SCAN_LOG" - printf '\nTo acknowledge: delete this file on the client.\n' - printf '=== Generated: %s ===\n' "$(date)" - } > "$ALERT_FILE" - else - log "OK: $HOSTNAME / $LOG_DATE — clean" - fi - done -done - -log "Check complete." diff --git a/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh b/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh index 1a5b65c..8df3efe 100644 --- a/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh +++ b/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh @@ -166,7 +166,7 @@ else fi # Create supplementary monitoring host-groups (no credentials needed in description). -for _hgrp in dev_mon_malware dev_mon_timeshift dev_mon_power; do +for _hgrp in dev_mon_clamscan dev_mon_timeshift dev_mon_power; do ipa hostgroup-show "$_hgrp" &>/dev/null 2>&1 || \ ipa hostgroup-add "$_hgrp" &>/dev/null || true done diff --git a/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh b/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh index 7043257..eda6fdc 100644 --- a/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh +++ b/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh @@ -9,7 +9,6 @@ # 2. /data/samba/ansipa-smb.env (persisted from first boot) # # Shares: -# ansipa-scans — write-only for 'scanupload'; clients push scan results here. # ansipa-luks-keys — write-only for 'luks-upload' (Ansible controller); # read for members of the 'KeyAdmin' Linux group. # Add a Samba user to KeyAdmin to grant key-read access: @@ -24,12 +23,10 @@ set -euo pipefail LOG_TAG="ansipa-smb-setup" -SCAN_BASE="/data/scan-results" LUKS_BASE="/data/luks-keys" SHARES_BASE="/data/smb-shares" POLICY_STORE_BASE="/data/policy-store" SMB_CONF="/etc/samba/smb.conf" -SMB_USER="scanupload" LUKS_UPLOAD_USER="luks-upload" POLICY_STORE_USER="smb-policystore" KEYADMIN_GROUP="KeyAdmin" @@ -40,32 +37,27 @@ warn() { echo "[$LOG_TAG][WARN] $*" >&2; } die() { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; } # ── Resolve passwords ───────────────────────────────────────────────────────── -SMB_PASS="${SMB_SCAN_PASSWORD:-}" LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-}" IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-}" if [[ -f "$ENV_FILE" ]]; then # shellcheck source=/dev/null source "$ENV_FILE" - SMB_PASS="${SMB_SCAN_PASSWORD:-$SMB_PASS}" LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-$LUKS_PASS}" IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-$IPA_ADMIN_PASS}" fi -[[ -z "$SMB_PASS" ]] && die "SMB_SCAN_PASSWORD not set and $ENV_FILE not present. Set it in .env." [[ -z "$LUKS_PASS" ]] && die "LUKS_KEY_UPLOAD_PASSWORD not set and $ENV_FILE not present. Set it in .env." # ── Persist for subsequent restarts ────────────────────────────────────────── mkdir -p "$(dirname "$ENV_FILE")" { - printf 'SMB_SCAN_PASSWORD=%q\n' "$SMB_PASS" printf 'LUKS_KEY_UPLOAD_PASSWORD=%q\n' "$LUKS_PASS" [[ -n "$IPA_ADMIN_PASS" ]] && printf 'IPA_ADMIN_PASSWORD=%q\n' "$IPA_ADMIN_PASS" || true } > "$ENV_FILE" chmod 600 "$ENV_FILE" # ── Directory structure (idempotent) ────────────────────────────────────────── -mkdir -p "$SCAN_BASE/archive" "$SCAN_BASE/alerts" mkdir -p "$LUKS_BASE" mkdir -p "$SHARES_BASE" mkdir -p "$POLICY_STORE_BASE/policies.d" @@ -77,11 +69,6 @@ if ! getent group "$KEYADMIN_GROUP" &>/dev/null; then fi # ── System users ────────────────────────────────────────────────────────────── -if ! id "$SMB_USER" &>/dev/null; then - useradd -r -s /sbin/nologin -d "$SCAN_BASE" -M "$SMB_USER" - log "Created system user: $SMB_USER" -fi - if ! id "$POLICY_STORE_USER" &>/dev/null; then useradd -r -s /sbin/nologin -d "$POLICY_STORE_BASE" -M "$POLICY_STORE_USER" log "Created system user: $POLICY_STORE_USER" @@ -95,7 +82,6 @@ else usermod -aG "$KEYADMIN_GROUP" "$LUKS_UPLOAD_USER" fi -chown -R "$SMB_USER:$SMB_USER" "$SCAN_BASE" chown -R "root:$KEYADMIN_GROUP" "$LUKS_BASE" chmod 2750 "$LUKS_BASE" # setgid so new files inherit KeyAdmin group chown -R "$POLICY_STORE_USER:$POLICY_STORE_USER" "$POLICY_STORE_BASE" @@ -120,16 +106,6 @@ _write_smb_conf() { printcap name = /dev/null disable spoolss = yes -[ansipa-scans] - comment = Ansipa scan results — managed by ansipa-enforce-policies - path = $SCAN_BASE - valid users = $SMB_USER - read only = no - browseable = no - create mask = 0644 - directory mask = 0755 - force user = $SMB_USER - [ansipa-luks-keys] comment = Ansipa LUKS backup keys — KeyAdmin read, luks-upload write only path = $LUKS_BASE @@ -365,7 +341,6 @@ _setup_policystore() { } # ── Write smb.conf, set passwords, configure shares ────────────────────────── -_smb_set_pass "$SMB_USER" "$SMB_PASS" _smb_set_pass "$LUKS_UPLOAD_USER" "$LUKS_PASS" # Write base smb.conf first, then append dynamic share stanzas. @@ -376,14 +351,4 @@ _setup_policystore # Reload smbd if it is already running (picks up new user shares without restart). smbcontrol smbd reload-config 2>/dev/null || true -# ── Server-side scan checker cron (hourly, analysed on the IPA server itself) ─ -# Always (re-)write: /etc/cron.d is on the ephemeral container layer and is -# lost on container recreation, so we must restore it on every start. -cat > /etc/cron.d/ansipa-check-scans <<'CRON' -# ansipa: analyze client scan logs and write alerts — managed, do not edit. -0 * * * * root /usr/local/sbin/ansipa-check-scans.sh 2>&1 | logger -t ansipa-check-scans -CRON -chmod 644 /etc/cron.d/ansipa-check-scans -log "Installed hourly scan-checker cron" - -log "Samba setup complete. Shares: ansipa-scans ($SMB_USER), ansipa-policystore ($POLICY_STORE_USER)" +log "Samba setup complete. Shares: ansipa-luks-keys, ansipa-policystore ($POLICY_STORE_USER)" diff --git a/setup/modules/FreeipaAnsible/image/docker-compose.yml b/setup/modules/FreeipaAnsible/image/docker-compose.yml index 008e306..25864ce 100644 --- a/setup/modules/FreeipaAnsible/image/docker-compose.yml +++ b/setup/modules/FreeipaAnsible/image/docker-compose.yml @@ -78,7 +78,6 @@ services: IPA_SETUP_DNS: ${IPA_SETUP_DNS:-false} IPA_DNS_FORWARDER: ${IPA_DNS_FORWARDER:-} IPA_SETUP_KRA: ${IPA_SETUP_KRA:-false} - SMB_SCAN_PASSWORD: ${SMB_SCAN_PASSWORD:?set SMB_SCAN_PASSWORD in .env} CMK_URL: http://172.30.0.12:5000 # Client-facing CheckMK URL stored in the dev_mon_base description — # must be routable from enrolled clients (e.g. http://:8090). diff --git a/setup/modules/FreeipaAnsible/image/run.sh b/setup/modules/FreeipaAnsible/image/run.sh index 32371a5..1243804 100755 --- a/setup/modules/FreeipaAnsible/image/run.sh +++ b/setup/modules/FreeipaAnsible/image/run.sh @@ -72,7 +72,6 @@ start_freeipa() { -e IPA_SETUP_DNS="${IPA_SETUP_DNS:-false}" \ -e IPA_DNS_FORWARDER="${IPA_DNS_FORWARDER:-}" \ -e IPA_SETUP_KRA="${IPA_SETUP_KRA:-false}" \ - -e SMB_SCAN_PASSWORD="${SMB_SCAN_PASSWORD:?}" \ -e LUKS_KEY_UPLOAD_PASSWORD="${LUKS_KEY_UPLOAD_PASSWORD:?}" \ -e CMK_URL="http://172.30.0.12:5000" \ -e CMK_ADVERTISED_URL="${CMK_ADVERTISED_URL:-}" \ diff --git a/setup/tools/ansipa-proxmox-lxc.sh b/setup/tools/ansipa-proxmox-lxc.sh index 1f98d76..8d0b066 100755 --- a/setup/tools/ansipa-proxmox-lxc.sh +++ b/setup/tools/ansipa-proxmox-lxc.sh @@ -77,7 +77,7 @@ mkdir -p "$OUT_DIR" section "Generating secrets" IPA_ADMIN_PASSWORD=$(gen_pw); IPA_DM_PASSWORD=$(gen_pw) CMK_ADMIN_PASSWORD=$(gen_pw); KC_ADMIN_PASSWORD=$(gen_pw); KC_DB_PASSWORD=$(gen_pw) -SMB_SCAN_PASSWORD=$(gen_pw); LUKS_KEY_UPLOAD_PASSWORD=$(gen_pw) +LUKS_KEY_UPLOAD_PASSWORD=$(gen_pw) log "Generated admin/service passwords (saved in $OUT_DIR/.env — keep it safe)." # ─── .env for the ansipa stack ──────────────────────────────────────────────── @@ -91,7 +91,6 @@ IPA_DM_PASSWORD=$IPA_DM_PASSWORD IPA_SETUP_DNS=false IPA_SETUP_KRA=false -SMB_SCAN_PASSWORD=$SMB_SCAN_PASSWORD LUKS_KEY_UPLOAD_PASSWORD=$LUKS_KEY_UPLOAD_PASSWORD CMK_ADMIN_PASSWORD=$CMK_ADMIN_PASSWORD