feat(freeipa): scan result reporting, alert notifications, and SMB share
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 <host>/<date>.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/<host>/
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/<hostname>/ 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 <noreply@anthropic.com>
main
parent
fb8ca498ef
commit
11e66dbddd
|
|
@ -45,6 +45,7 @@ RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \
|
||||||
ACTIVE_BLOCK_BINARIES=()
|
ACTIVE_BLOCK_BINARIES=()
|
||||||
WANT_TIMESHIFT_BACKUP=false
|
WANT_TIMESHIFT_BACKUP=false
|
||||||
WANT_SECURITY_SCAN=false
|
WANT_SECURITY_SCAN=false
|
||||||
|
WANT_SCAN_NOTIFY=false
|
||||||
|
|
||||||
if [[ -n "$RAW_GROUPS" ]]; then
|
if [[ -n "$RAW_GROUPS" ]]; then
|
||||||
while IFS=',' read -ra GRP_ARRAY; do
|
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-block-binary-*) ACTIVE_BLOCK_BINARIES+=("${g#policy-block-binary-}") ;;
|
||||||
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 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
done <<< "$RAW_GROUPS"
|
done <<< "$RAW_GROUPS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Active policies — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
|
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 ───────────────────────────────────────────────────────────────────
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
in_active_list() {
|
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.
|
# (Re-)write the scan script so it stays current with this version of the enforcer.
|
||||||
cat > "$SCAN_SCRIPT" <<'SCAN'
|
cat > "$SCAN_SCRIPT" <<'SCAN'
|
||||||
#!/bin/bash
|
#!/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.
|
# Managed by ansipa-enforce-policies — do not edit manually.
|
||||||
LOG=/var/log/ansipa-security-scan.log
|
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
|
if command -v freshclam &>/dev/null; then
|
||||||
freshclam --quiet 2>/dev/null || true
|
freshclam --quiet 2>/dev/null || true
|
||||||
|
|
@ -230,6 +235,19 @@ LOG=/var/log/ansipa-security-scan.log
|
||||||
|
|
||||||
echo "=== scan complete ==="
|
echo "=== scan complete ==="
|
||||||
} >> "$LOG" 2>&1
|
} >> "$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
|
SCAN
|
||||||
chmod 755 "$SCAN_SCRIPT"
|
chmod 755 "$SCAN_SCRIPT"
|
||||||
|
|
||||||
|
|
@ -250,4 +268,83 @@ else
|
||||||
fi
|
fi
|
||||||
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/<hostname>/ 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."
|
log "Policy enforcement complete."
|
||||||
|
|
|
||||||
|
|
@ -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/<hostname>/ 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."
|
||||||
|
|
@ -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
|
||||||
|
|
@ -4,31 +4,68 @@
|
||||||
# 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
|
# policy-block-binary-<name> Block execution of <name> via a PATH-priority wrapper + AppArmor
|
||||||
# policy-timeshift-backup Enforce daily Timeshift snapshots (03:00)
|
# 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:
|
# Prerequisites:
|
||||||
# - Host enrolled in FreeIPA (sssd + ipa CLI available)
|
# - Host enrolled in FreeIPA (sssd + ipa CLI available)
|
||||||
# - For security-scan: also add host to ansipa-module-anti-malware group
|
# - For security-scan / scan-notify: samba-client installed (handled below)
|
||||||
# - For timeshift-backup: also add host to ansipa-module-timeshift group and
|
# - For security-scan / scan-notify: smb_scan_password set (use ansible-vault in production)
|
||||||
# configure Timeshift (type + target device) on the host
|
# - 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:
|
# Usage:
|
||||||
# ansible-playbook -i inventory deploy-ansipa-policies.yml
|
# ansible-playbook -i inventory deploy-ansipa-policies.yml \
|
||||||
|
# -e smb_scan_password=<password> # or use --vault-password-file
|
||||||
|
|
||||||
- name: Deploy FreeIPA policy enforcer
|
- name: Deploy FreeIPA policy enforcer
|
||||||
hosts: all
|
hosts: all
|
||||||
become: yes
|
become: yes
|
||||||
|
|
||||||
|
vars:
|
||||||
|
smb_scan_password: "{{ smb_scan_password | mandatory('smb_scan_password is required — use -e smb_scan_password=... or ansible-vault') }}"
|
||||||
|
|
||||||
tasks:
|
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
|
- name: Deploy policy enforcer script
|
||||||
copy:
|
copy:
|
||||||
src: ansipa-enforce-policies.sh
|
src: ansipa-enforce-policies.sh
|
||||||
dest: /usr/local/bin/ansipa-enforce-policies.sh
|
dest: /usr/local/bin/ansipa-enforce-policies.sh
|
||||||
mode: '0755'
|
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
|
- name: Create policy state directory
|
||||||
file:
|
file:
|
||||||
path: /var/lib/ansipa-policies
|
path: /var/lib/ansipa-policies
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,11 @@ IPA_SETUP_DNS=false
|
||||||
IPA_DNS_FORWARDER=
|
IPA_DNS_FORWARDER=
|
||||||
IPA_SETUP_KRA=false
|
IPA_SETUP_KRA=false
|
||||||
|
|
||||||
|
# ── Ansipa SMB scan-results share ─────────────────────────────────────────────
|
||||||
|
# Password for the 'scanupload' Samba user. Deploy to clients via Ansible with
|
||||||
|
# smb_scan_password=<this value> (use ansible-vault for production).
|
||||||
|
SMB_SCAN_PASSWORD=ChangeMe_ScanPass!
|
||||||
|
|
||||||
# ── Keycloak ──────────────────────────────────────────────────────────────────
|
# ── Keycloak ──────────────────────────────────────────────────────────────────
|
||||||
KC_HOSTNAME=keycloak.corp.example.com
|
KC_HOSTNAME=keycloak.corp.example.com
|
||||||
KC_REALM=corp
|
KC_REALM=corp
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ RUN dnf install -y --setopt=install_weak_deps=False \
|
||||||
net-tools \
|
net-tools \
|
||||||
rsync \
|
rsync \
|
||||||
hostname \
|
hostname \
|
||||||
|
samba \
|
||||||
|
cronie \
|
||||||
&& dnf clean all \
|
&& dnf clean all \
|
||||||
&& rm -rf /var/cache/dnf
|
&& 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.sh /usr/local/sbin/ipa-first-boot.sh
|
||||||
COPY ipa-first-boot.service /etc/systemd/system/ipa-first-boot.service
|
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 \
|
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"]
|
VOLUME ["/data"]
|
||||||
|
|
||||||
# LDAP, LDAPS, Kerberos, kpasswd, HTTPS, DNS, NTP
|
# 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
|
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
|
STOPSIGNAL SIGRTMIN+3
|
||||||
CMD ["/sbin/init"]
|
CMD ["/sbin/init"]
|
||||||
|
|
|
||||||
|
|
@ -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/<hostname>/<YYYY-MM-DD>.log
|
||||||
|
# Output: /data/scan-results/alerts/<hostname>/<YYYY-MM-DD>.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."
|
||||||
|
|
@ -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" <<CONF
|
||||||
|
[global]
|
||||||
|
workgroup = WORKGROUP
|
||||||
|
server string = Ansipa Security Server
|
||||||
|
security = user
|
||||||
|
map to guest = never
|
||||||
|
# Store passdb on the persistent volume so passwords survive container restarts.
|
||||||
|
passdb backend = tdbsam:/data/samba/passdb.tdb
|
||||||
|
log file = /var/log/samba/log.%m
|
||||||
|
max log size = 50
|
||||||
|
# Disable printing subsystem entirely.
|
||||||
|
load printers = no
|
||||||
|
printing = bsd
|
||||||
|
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
|
||||||
|
CONF
|
||||||
|
|
||||||
|
# ── Samba password (idempotent — smbpasswd -a adds or updates) ────────────────
|
||||||
|
log "Setting Samba password for $SMB_USER"
|
||||||
|
printf '%s\n%s\n' "$SMB_PASS" "$SMB_PASS" | smbpasswd -a -s "$SMB_USER" 2>/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"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -48,6 +48,7 @@ services:
|
||||||
IPA_SETUP_DNS: ${IPA_SETUP_DNS:-false}
|
IPA_SETUP_DNS: ${IPA_SETUP_DNS:-false}
|
||||||
IPA_DNS_FORWARDER: ${IPA_DNS_FORWARDER:-}
|
IPA_DNS_FORWARDER: ${IPA_DNS_FORWARDER:-}
|
||||||
IPA_SETUP_KRA: ${IPA_SETUP_KRA:-false}
|
IPA_SETUP_KRA: ${IPA_SETUP_KRA:-false}
|
||||||
|
SMB_SCAN_PASSWORD: ${SMB_SCAN_PASSWORD:?set SMB_SCAN_PASSWORD in .env}
|
||||||
ports:
|
ports:
|
||||||
- "389:389"
|
- "389:389"
|
||||||
- "636:636"
|
- "636:636"
|
||||||
|
|
@ -56,6 +57,8 @@ services:
|
||||||
- "464:464"
|
- "464:464"
|
||||||
- "464:464/udp"
|
- "464:464/udp"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
|
- "445:445"
|
||||||
|
- "139:139"
|
||||||
networks:
|
networks:
|
||||||
ipa-net:
|
ipa-net:
|
||||||
ipv4_address: 172.30.0.10
|
ipv4_address: 172.30.0.10
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue