From 11e66dbdddbb23d905c6a007c7b25bde70088f66 Mon Sep 17 00:00:00 2001 From: The_miro Date: Wed, 20 May 2026 12:32:21 +0200 Subject: [PATCH] feat(freeipa): scan result reporting, alert notifications, and SMB share MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container (ansipa image): - Add samba + cronie to Dockerfile; expose ports 445/139 - ansipa-smb-setup.sh: idempotent setup of smbd + scanupload user + /data/scan-results/{archive,alerts}/ on every container start - ansipa-smb.service: runs setup before smb.service on each boot - ansipa-check-scans.sh: hourly cron on server; analyses archive logs for ClamAV/rkhunter/chkrootkit findings and writes /.alert files - docker-compose.yml: add SMB_SCAN_PASSWORD env var + port mappings - .env.example: document SMB_SCAN_PASSWORD Client (policy-security-scan): - Scan script now uploads log to //ipa-server/ansipa-scans/archive// via smbclient after each run Client (policy-scan-notify — new policy group): - ansipa-fetch-alerts.sh: root timer (10 min) downloads alerts from SMB into ~/administration// for each active login session; deletes server alert when user removes local file (acknowledgment) - ansipa-scan-notify.sh: user daemon started via /etc/profile.d/ansipa-notify.sh; sends notify-send every 10 min while *.alert files remain in ~/administration/ - deploy-ansipa-policies.yml: installs samba-client, deploys SMB creds file (/etc/ansipa-smb.creds, 0600), and deploys both notification scripts Co-Authored-By: Claude Sonnet 4.6 --- .../ansible/ansipa-enforce-policies.sh | 103 +++++++++++++- .../ansible/ansipa-fetch-alerts.sh | 128 ++++++++++++++++++ .../ansible/ansipa-scan-notify.sh | 50 +++++++ .../ansible/deploy-ansipa-policies.yml | 49 ++++++- .../modules/FreeipaAnsible/image/.env.example | 5 + setup/modules/FreeipaAnsible/image/Dockerfile | 15 +- .../image/ansipa-check-scans.sh | 71 ++++++++++ .../FreeipaAnsible/image/ansipa-smb-setup.sh | 93 +++++++++++++ .../FreeipaAnsible/image/ansipa-smb.service | 18 +++ .../FreeipaAnsible/image/docker-compose.yml | 3 + 10 files changed, 523 insertions(+), 12 deletions(-) create mode 100644 setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh create mode 100644 setup/modules/FreeipaAnsible/ansible/ansipa-scan-notify.sh create mode 100644 setup/modules/FreeipaAnsible/image/ansipa-check-scans.sh create mode 100644 setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh create mode 100644 setup/modules/FreeipaAnsible/image/ansipa-smb.service diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh index 77e5466..936d6d4 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh @@ -45,6 +45,7 @@ RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \ ACTIVE_BLOCK_BINARIES=() WANT_TIMESHIFT_BACKUP=false WANT_SECURITY_SCAN=false +WANT_SCAN_NOTIFY=false if [[ -n "$RAW_GROUPS" ]]; then while IFS=',' read -ra GRP_ARRAY; do @@ -54,13 +55,15 @@ if [[ -n "$RAW_GROUPS" ]]; then policy-block-binary-*) ACTIVE_BLOCK_BINARIES+=("${g#policy-block-binary-}") ;; policy-timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;; policy-security-scan) WANT_SECURITY_SCAN=true ;; + policy-scan-notify) WANT_SCAN_NOTIFY=true ;; esac done done <<< "$RAW_GROUPS" fi log "Active policies — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \ - "| timeshift-backup: $WANT_TIMESHIFT_BACKUP | security-scan: $WANT_SECURITY_SCAN" + "| timeshift-backup: $WANT_TIMESHIFT_BACKUP" \ + "| security-scan: $WANT_SECURITY_SCAN | scan-notify: $WANT_SCAN_NOTIFY" # ── Helpers ─────────────────────────────────────────────────────────────────── in_active_list() { @@ -208,11 +211,13 @@ 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. +# 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=$(hostname -f 2>/dev/null || hostname) +DATE=$(date +%Y-%m-%d) { - echo "=== ansipa-security-scan: $(date) ===" + echo "=== ansipa-security-scan: $DATE $HOSTNAME ===" if command -v freshclam &>/dev/null; then freshclam --quiet 2>/dev/null || true @@ -230,6 +235,19 @@ LOG=/var/log/ansipa-security-scan.log 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" @@ -250,4 +268,83 @@ else fi fi +# ── Scan notification daemon ────────────────────────────────────────────────── +# policy-scan-notify: +# - Root timer (every 10 min): ansipa-fetch-alerts.sh downloads alerts from the +# server SMB share and places them in ~/administration// per active user. +# - profile.d snippet: starts ansipa-scan-notify.sh as a user daemon on login; +# the daemon sends notify-send every 10 min while *.alert files remain. +# Deleting a file from ~/administration/ counts as acknowledgment. +# +# Requires: ansipa-fetch-alerts.sh and ansipa-scan-notify.sh deployed by +# deploy-ansipa-policies.yml (static scripts — not written inline here). + +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. +# Managed by ansipa-enforce-policies — do not edit manually. +_NOTIFY_DAEMON=/usr/local/bin/ansipa-scan-notify.sh +if [[ -x "$_NOTIFY_DAEMON" ]] && \ + ! 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 (host left policy-scan-notify group)" + fi + if [[ -f "$NOTIFY_PROFILED" ]]; then + rm -f "$NOTIFY_PROFILED" + log "Removed /etc/profile.d/ansipa-notify.sh" + fi +fi + log "Policy enforcement complete." diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh new file mode 100644 index 0000000..77c7dc1 --- /dev/null +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-fetch-alerts.sh @@ -0,0 +1,128 @@ +#!/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). +# +# 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 ──────────────────────────────────────────────── +ACTIVE_USERS=() +while IFS= read -r LINE; do + USER=$(echo "$LINE" | awk '{print $3}') + [[ -z "$USER" || "$USER" == "root" ]] && continue + HOME_DIR=$(getent passwd "$USER" | cut -d: -f6) || continue + [[ -d "$HOME_DIR" ]] && ACTIVE_USERS+=("$USER:$HOME_DIR") +done < <(loginctl list-sessions --no-legend 2>/dev/null || who 2>/dev/null || true) +# Deduplicate by user. +mapfile -t ACTIVE_USERS < <(printf '%s\n' "${ACTIVE_USERS[@]}" | sort -u) + +# ── List alerts on server for this host ─────────────────────────────────────── +SERVER_ALERTS=() +while IFS= read -r LINE; do + # smbclient ls output: " filename.alert A 1234 date" + FILE=$(echo "$LINE" | awk '{print $1}') + [[ "$FILE" == *.alert ]] && SERVER_ALERTS+=("$FILE") +done < <(smb -c "ls alerts\\$HOSTNAME\\" 2>/dev/null || true) + +# ── Check for locally deleted alerts (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. + sed -i "/^$ALERT_NAME\$/d" "$FETCHED_STATE" 2>/dev/null || true + 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 + 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 && 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 new file mode 100644 index 0000000..df106d8 --- /dev/null +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-scan-notify.sh @@ -0,0 +1,50 @@ +#!/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 e9d632e..bd0ab8e 100644 --- a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml +++ b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml @@ -4,31 +4,68 @@ # 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 +# 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 (02: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) -# - For security-scan: also add host to ansipa-module-anti-malware group -# - For timeshift-backup: also add host to ansipa-module-timeshift group and -# configure Timeshift (type + target device) on the host +# - 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 # # Usage: -# ansible-playbook -i inventory deploy-ansipa-policies.yml +# ansible-playbook -i inventory deploy-ansipa-policies.yml \ +# -e smb_scan_password= # or use --vault-password-file - 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: Install samba-client (required for scan upload and alert fetch) + package: + name: "{{ item }}" + state: present + loop: + - samba-client + 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/image/.env.example b/setup/modules/FreeipaAnsible/image/.env.example index 65d4e23..bca6ab1 100644 --- a/setup/modules/FreeipaAnsible/image/.env.example +++ b/setup/modules/FreeipaAnsible/image/.env.example @@ -8,6 +8,11 @@ IPA_SETUP_DNS=false IPA_DNS_FORWARDER= IPA_SETUP_KRA=false +# ── Ansipa SMB scan-results share ───────────────────────────────────────────── +# Password for the 'scanupload' Samba user. Deploy to clients via Ansible with +# smb_scan_password= (use ansible-vault for production). +SMB_SCAN_PASSWORD=ChangeMe_ScanPass! + # ── Keycloak ────────────────────────────────────────────────────────────────── KC_HOSTNAME=keycloak.corp.example.com KC_REALM=corp diff --git a/setup/modules/FreeipaAnsible/image/Dockerfile b/setup/modules/FreeipaAnsible/image/Dockerfile index c0f63d9..cfd8571 100644 --- a/setup/modules/FreeipaAnsible/image/Dockerfile +++ b/setup/modules/FreeipaAnsible/image/Dockerfile @@ -37,6 +37,8 @@ RUN dnf install -y --setopt=install_weak_deps=False \ net-tools \ rsync \ hostname \ + samba \ + cronie \ && dnf clean all \ && rm -rf /var/cache/dnf @@ -56,13 +58,20 @@ RUN systemctl mask \ 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 RUN chmod +x /usr/local/sbin/ipa-first-boot.sh \ - && systemctl enable ipa-first-boot.service + && chmod +x /usr/local/sbin/ansipa-smb-setup.sh \ + && chmod +x /usr/local/sbin/ansipa-check-scans.sh \ + && systemctl enable ipa-first-boot.service \ + && systemctl enable ansipa-smb.service \ + && systemctl enable smb.service nmb.service crond.service VOLUME ["/data"] -# LDAP, LDAPS, Kerberos, kpasswd, HTTPS, DNS, NTP -EXPOSE 389 636 88/tcp 88/udp 464/tcp 464/udp 443 80 53/tcp 53/udp 123/udp +# LDAP, LDAPS, Kerberos, kpasswd, HTTPS, DNS, NTP, SMB +EXPOSE 389 636 88/tcp 88/udp 464/tcp 464/udp 443 80 53/tcp 53/udp 123/udp 445/tcp 445/udp 137/udp 138/udp 139/tcp STOPSIGNAL SIGRTMIN+3 CMD ["/sbin/init"] diff --git a/setup/modules/FreeipaAnsible/image/ansipa-check-scans.sh b/setup/modules/FreeipaAnsible/image/ansipa-check-scans.sh new file mode 100644 index 0000000..6a0644f --- /dev/null +++ b/setup/modules/FreeipaAnsible/image/ansipa-check-scans.sh @@ -0,0 +1,71 @@ +#!/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-smb-setup.sh b/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh new file mode 100644 index 0000000..b2665d8 --- /dev/null +++ b/setup/modules/FreeipaAnsible/image/ansipa-smb-setup.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# ansipa-smb-setup.sh — configure the Samba scan-results share on the IPA container. +# +# Runs on every container start via ansipa-smb.service so that smb.conf and +# the Samba user are always in place after container restarts (ephemeral rootfs). +# +# Password source (first match wins): +# 1. SMB_SCAN_PASSWORD environment variable (first boot / explicit override) +# 2. /data/samba/ansipa-smb.env (persisted from first boot) + +set -euo pipefail + +LOG_TAG="ansipa-smb-setup" +SCAN_BASE="/data/scan-results" +SMB_CONF="/etc/samba/smb.conf" +SMB_USER="scanupload" +ENV_FILE="/data/samba/ansipa-smb.env" + +log() { echo "[$LOG_TAG] $*"; } +die() { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; } + +# ── Resolve password ────────────────────────────────────────────────────────── +SMB_PASS="${SMB_SCAN_PASSWORD:-}" + +if [[ -z "$SMB_PASS" ]] && [[ -f "$ENV_FILE" ]]; then + # shellcheck source=/dev/null + source "$ENV_FILE" + SMB_PASS="${SMB_SCAN_PASSWORD:-}" +fi + +[[ -z "$SMB_PASS" ]] && die "SMB_SCAN_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=%s\n' "$SMB_PASS" > "$ENV_FILE" +chmod 600 "$ENV_FILE" + +# ── Directory structure (idempotent) ────────────────────────────────────────── +mkdir -p "$SCAN_BASE/archive" "$SCAN_BASE/alerts" + +# ── System user ─────────────────────────────────────────────────────────────── +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 +chown -R "$SMB_USER:$SMB_USER" "$SCAN_BASE" + +# ── smb.conf ────────────────────────────────────────────────────────────────── +log "Writing $SMB_CONF" +cat > "$SMB_CONF" </dev/null || \ +printf '%s\n%s\n' "$SMB_PASS" "$SMB_PASS" | smbpasswd -s "$SMB_USER" 2>/dev/null || \ +log "WARN: smbpasswd returned non-zero (user may already exist with correct password)" + +# ── Server-side scan checker cron (hourly, analysed on the IPA server itself) ─ +if [[ ! -f /etc/cron.d/ansipa-check-scans ]]; then + 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" +fi + +log "Samba setup complete. Share: //localhost/ansipa-scans user: $SMB_USER" diff --git a/setup/modules/FreeipaAnsible/image/ansipa-smb.service b/setup/modules/FreeipaAnsible/image/ansipa-smb.service new file mode 100644 index 0000000..5b7b2f1 --- /dev/null +++ b/setup/modules/FreeipaAnsible/image/ansipa-smb.service @@ -0,0 +1,18 @@ +[Unit] +Description=Ansipa Scan Results SMB Share Setup +# Run before smb so smb.conf and the Samba user exist when smbd starts. +Before=smb.service +After=network.target + +[Service] +Type=oneshot +RemainAfterExit=yes +# SMB_SCAN_PASSWORD comes from the container environment on first boot. +# On subsequent restarts it is read from /data/samba/ansipa-smb.env by the script. +PassEnvironment=SMB_SCAN_PASSWORD +ExecStart=/usr/local/sbin/ansipa-smb-setup.sh +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=smb.service diff --git a/setup/modules/FreeipaAnsible/image/docker-compose.yml b/setup/modules/FreeipaAnsible/image/docker-compose.yml index 59ac8d8..848d266 100644 --- a/setup/modules/FreeipaAnsible/image/docker-compose.yml +++ b/setup/modules/FreeipaAnsible/image/docker-compose.yml @@ -48,6 +48,7 @@ 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} ports: - "389:389" - "636:636" @@ -56,6 +57,8 @@ services: - "464:464" - "464:464/udp" - "443:443" + - "445:445" + - "139:139" networks: ipa-net: ipv4_address: 172.30.0.10