feat(ansipa): integrate CheckMK CE monitoring

Adds full CheckMK CE 2.3 monitoring integration to the ansipa policy
enforcement system. Monitoring is group-driven — adding a host to an
IPA hostgroup installs the relevant agent checks automatically.

New monitoring groups:
  dev_mon_base      — install check-mk-agent, register host in CMK;
                      packages check + built-in CPU/RAM/disk/uptime
  dev_mon_malware   — ClamAV scan results from dev_security-scan log
  dev_mon_timeshift — Timeshift snapshot age (WARN >5d, CRIT >10d)
  dev_mon_power     — CPU package TDP via Intel RAPL or lm-sensors
  usr_mon_logins    — SSH login attempts (success/failed/invalid-user)

Key implementation details:
- ansipa-checkmk-setup.sh: one-time folder/group creation on FreeIPA
  container; reads automation secret from shared cmk-creds volume;
  stores credentials in IPA dev_mon_base description for clients
- ansipa-checkmk.service: starts after IPA first-boot, retries until
  both IPA and CheckMK are ready
- docker-compose.yml: CheckMK container adds port 8000 (agent receiver
  for push mode); command override writes automation.secret to shared
  volume, patches cookie_auth.conf for API access, and polls
  /cmk-creds/push-mode-*.req to configure push-agent hosts in hosts.mk
- ansipa-enforce-policies.sh: local check scripts for all 5 groups;
  cmk-agent-ctl registration + auto push timer when push mode is active
- CheckMK REST API path corrected to /cmk/check_mk/api/1.0/ (not
  /cmk/api/1.0/ which returns 404 in 2.3)
- .gitignore added to prevent accidental commit of .env test creds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DHops6PU4c2Mv5UyhUj8Ms
feat/astal-menu
Amir Alexander Abdelbaki 2026-07-01 18:32:28 +02:00
parent 76c3c2c1c5
commit 6bb19cea78
7 changed files with 732 additions and 17 deletions

View File

@ -82,6 +82,7 @@ ACTIVE_DAEMON_ENABLE=()
ACTIVE_DAEMON_DISABLE=()
ACTIVE_LOCAL_SUDO_USERS=()
ACTIVE_SSH_USERS=()
ACTIVE_MON_DEV=() # dev_mon_* suffixes from host-group membership
WANT_TIMESHIFT_BACKUP=false
WANT_SECURITY_SCAN=false
WANT_SCAN_NOTIFY=false
@ -102,6 +103,7 @@ if [[ -n "$RAW_GROUPS" ]]; then
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_*) ACTIVE_MON_DEV+=("${g#dev_mon_}") ;;
esac
done
done <<< "$RAW_GROUPS"
@ -153,15 +155,28 @@ if ipa group-show usr_scan-notify >/dev/null 2>&1; then
WANT_SCAN_NOTIFY=true
fi
# ── Fetch usr_mon_* user-group monitoring policies from FreeIPA ───────────────
# usr_mon_<type> are FreeIPA *user* groups; when the group exists the monitoring
# check is installed fleet-wide (same logic as usr_scan-notify).
ACTIVE_MON_USR=()
_MON_USR_LIST=$(ipa group-find --pkey-only 2>/dev/null \
| awk '/Group name:/ {print $NF}' \
| grep "^usr_mon_" | sort -u || true)
while IFS= read -r _mgrp; do
[[ -z "$_mgrp" ]] && continue
ACTIVE_MON_USR+=("${_mgrp#usr_mon_}")
done <<< "$_MON_USR_LIST"
unset _MON_USR_LIST _mgrp
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" \
"| no-local-users: $WANT_NO_LOCAL_USERS | local-sudo: ${ACTIVE_LOCAL_SUDO_USERS[*]:-none}" \
"| ssh-keys: ${ACTIVE_SSH_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"
"| scan-notify: $WANT_SCAN_NOTIFY | mon-usr: ${ACTIVE_MON_USR[*]:-none}"
# ── Helpers ───────────────────────────────────────────────────────────────────
_in_block_list() {
@ -1061,4 +1076,423 @@ else
fi
unset _SSH_APPLIED _NEW_SSH_STATE
# ── CheckMK monitoring policies ───────────────────────────────────────────────
# dev_mon_base: Install CheckMK agent + register host; built-in agent checks
# handle CPU/RAM/disk/uptime; custom check reports package count.
# dev_mon_malware: ClamAV scan result local check (reads dev_security-scan log).
# dev_mon_timeshift: Timeshift snapshot age check (warn >5d, crit >10d).
# dev_mon_power: CPU package power via Intel RAPL / lm-sensors.
# usr_mon_logins: SSH login attempts (success + failure) in last 24h.
#
# CheckMK agent runs on port 6556 (pull mode — requires CMK server network access
# to clients). The host registers itself via the CMK REST API on policy apply.
# Local check scripts are in /usr/lib/check_mk_agent/local/ — executed by the
# agent on every poll and also visible via `check_mk_agent` on the client.
#
# Credentials are stored in the IPA dev_mon_base hostgroup description by
# ansipa-checkmk-setup.sh on the IPA container:
# cmk://<host>:<port>/<site>:automation:<secret>
MON_LOCAL_DIR="/usr/lib/check_mk_agent/local"
MON_STATE="$STATE_DIR/mon-registered" # present when host is registered in CMK
MON_CHECKS_STATE="$STATE_DIR/mon-checks" # list of installed local check names
MON_CMK_CREDS="$STATE_DIR/mon-cmk-creds" # CMK connection vars for push scripts
[[ -f "$MON_CHECKS_STATE" ]] || touch "$MON_CHECKS_STATE"
# Helpers: check active group lists
_mon_dev_active() { local n="$1"; for _m in "${ACTIVE_MON_DEV[@]+"${ACTIVE_MON_DEV[@]}"}"; do [[ "$_m" == "$n" ]] && return 0; done; return 1; }
_mon_usr_active() { local n="$1"; for _m in "${ACTIVE_MON_USR[@]+"${ACTIVE_MON_USR[@]}"}"; do [[ "$_m" == "$n" ]] && return 0; done; return 1; }
# Parse CheckMK credentials from IPA dev_mon_base hostgroup description.
# Sets CMK_HOST_PORT, CMK_SITE, CMK_USER, CMK_SECRET, CMK_URL, CMK_API.
CMK_URL="" CMK_SITE="" CMK_USER="" CMK_SECRET="" CMK_API="" CMK_HOST_PORT=""
_cmk_parse_creds() {
local _desc
_desc=$(ipa hostgroup-show dev_mon_base --all 2>/dev/null \
| awk -F': ' '/Description:/{print $2; exit}' || true)
# cmk://172.30.0.12:5000/cmk:automation:SECRET
if [[ "$_desc" =~ ^cmk://([^/]+)(/[^:]+):([^:]+):(.+)$ ]]; then
CMK_HOST_PORT="${BASH_REMATCH[1]}"
CMK_SITE="${BASH_REMATCH[2]#/}"
CMK_USER="${BASH_REMATCH[3]}"
CMK_SECRET="${BASH_REMATCH[4]}"
CMK_URL="http://${CMK_HOST_PORT}"
CMK_API="${CMK_URL}/${CMK_SITE}/check_mk/api/1.0"
fi
}
# Install or refresh a local check script.
# _cmk_write_check <name> <content-heredoc>
# Marks the check as wanted in the current run (for cleanup comparison).
_WANT_CHECKS=()
_cmk_write_check() {
local _name="$1" _body="$2"
mkdir -p "$MON_LOCAL_DIR"
printf '%s\n' "$_body" > "$MON_LOCAL_DIR/$_name"
chmod 755 "$MON_LOCAL_DIR/$_name"
_WANT_CHECKS+=("$_name")
}
if _mon_dev_active "base"; then
_cmk_parse_creds
if [[ -z "$CMK_SECRET" ]]; then
warn "dev_mon_base: no CheckMK credentials in IPA dev_mon_base description"
warn " — run ansipa-checkmk-setup on the FreeIPA container first"
else
# ── Install CheckMK agent (pull mode; server connects to port 6556) ────
_AGENT_INSTALLED=false
if rpm -q check-mk-agent &>/dev/null || command -v check_mk_agent &>/dev/null; then
_AGENT_INSTALLED=true
else
log "dev_mon_base: downloading CheckMK agent from ${CMK_URL}"
# Try RPM first (proper package with socket unit)
_RPM=$(curl -sf -u "${CMK_USER}:${CMK_SECRET}" \
"${CMK_URL}/${CMK_SITE}/check_mk/agents/" 2>/dev/null \
| grep -oE 'check-mk-agent-[0-9][^"]*\.noarch\.rpm' | head -1 || true)
if [[ -n "$_RPM" ]]; then
curl -sf -u "${CMK_USER}:${CMK_SECRET}" \
"${CMK_URL}/${CMK_SITE}/check_mk/agents/${_RPM}" \
-o /tmp/cmk-agent.rpm 2>/dev/null \
&& (rpm -ivh --nodeps /tmp/cmk-agent.rpm 2>/dev/null \
|| dnf install -y /tmp/cmk-agent.rpm 2>/dev/null) \
&& _AGENT_INSTALLED=true \
|| warn "dev_mon_base: CheckMK RPM install failed"
rm -f /tmp/cmk-agent.rpm
fi
# Fall back to shell agent
if [[ "$_AGENT_INSTALLED" == false ]]; then
curl -sf -u "${CMK_USER}:${CMK_SECRET}" \
"${CMK_URL}/${CMK_SITE}/check_mk/agents/check_mk_agent.linux" \
-o /usr/local/bin/check_mk_agent 2>/dev/null \
&& chmod 755 /usr/local/bin/check_mk_agent \
&& _AGENT_INSTALLED=true \
|| warn "dev_mon_base: shell agent download failed"
fi
fi
# Ensure the systemd socket is in place when using the shell agent without RPM
if [[ "$_AGENT_INSTALLED" == true ]] && [[ ! -f /etc/systemd/system/check_mk.socket ]]; then
cat > /etc/systemd/system/check_mk.socket <<'UNIT'
[Unit]
Description=Check_MK agent socket (ansipa-managed)
[Socket]
ListenStream=6556
Accept=yes
[Install]
WantedBy=sockets.target
UNIT
cat > /etc/systemd/system/check_mk@.service <<'UNIT'
[Unit]
Description=Check_MK per-connection agent (ansipa-managed)
[Service]
ExecStart=/usr/local/bin/check_mk_agent
StandardInput=socket
StandardOutput=socket
StandardError=null
UNIT
systemctl daemon-reload
fi
if [[ "$_AGENT_INSTALLED" == true ]]; then
systemctl enable --now check_mk.socket 2>/dev/null \
&& log "dev_mon_base: check_mk.socket enabled (port 6556)" \
|| warn "dev_mon_base: could not enable check_mk.socket"
fi
# ── Register host in CheckMK ──────────────────────────────────────────
_HOST_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "")
_CMK_REG_URL="${CMK_API}/objects/host_config/${HOST_FQDN}"
_ALREADY_REG=false
curl -sf -o /dev/null \
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
"$_CMK_REG_URL" 2>/dev/null && _ALREADY_REG=true || true
if [[ "$_ALREADY_REG" == false ]]; then
_reg_body="{\"host_name\":\"${HOST_FQDN}\",\"folder\":\"/ansipa\""
[[ -n "$_HOST_IP" ]] && _reg_body+=",\"attributes\":{\"ipaddress\":\"${_HOST_IP}\"}"
_reg_body+="}"
_reg_http=$(curl -sf -o /dev/null -w '%{http_code}' \
-X POST "${CMK_API}/domain-types/host_config/collections/all" \
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "$_reg_body" 2>/dev/null || echo "000")
if [[ "$_reg_http" =~ ^2 ]]; then
log "dev_mon_base: host ${HOST_FQDN} registered in CheckMK"
# Trigger service discovery
curl -sf \
-X POST "${CMK_API}/domain-types/service_discovery_run/actions/start/invoke" \
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{\"host_name\":\"${HOST_FQDN}\",\"mode\":\"refresh\"}" \
>/dev/null 2>&1 || true
# Activate changes
curl -sf \
-X POST "${CMK_API}/domain-types/activation_run/actions/activate-changes/invoke" \
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"redirect":false,"sites":[],"force_foreign_changes":true}' \
>/dev/null 2>&1 || true
printf '%s\n' "${CMK_API}" > "$MON_STATE"
else
warn "dev_mon_base: host registration failed (HTTP ${_reg_http})"
fi
else
log "dev_mon_base: ${HOST_FQDN} already registered in CheckMK"
printf '%s\n' "${CMK_API}" > "$MON_STATE"
fi
unset _HOST_IP _CMK_REG_URL _ALREADY_REG _reg_body _reg_http
# ── cmk-agent-ctl: register for TLS/push agent transport ─────────────
# cmk-agent-ctl is part of the check-mk-agent RPM (2.2+).
# It registers with the CheckMK server for encrypted agent communication.
# The server decides pull (TLS-encrypted) vs push mode based on host config.
# Push mode is needed when CheckMK can't reach this host on port 6556 (NAT).
if command -v cmk-agent-ctl &>/dev/null; then
_CMK_RECV_HOST=$(echo "$CMK_URL" | sed 's|http://||;s|:.*||')
_CMK_CTL_STATUS_FILE="$STATE_DIR/mon-cmk-ctl-mode"
# Register if not already registered for this server
_ALREADY_CTL=$(cmk-agent-ctl status 2>/dev/null | grep -c "${_CMK_RECV_HOST}/${CMK_SITE}" || true)
if [[ "${_ALREADY_CTL:-0}" -eq 0 ]]; then
_CTL_OUT=$(cmk-agent-ctl register \
--server "${_CMK_RECV_HOST}:8000" \
--site "${CMK_SITE}" \
--user "${CMK_USER}" \
--password "${CMK_SECRET}" \
--hostname "${HOST_FQDN}" \
--trust-cert 2>&1) && \
log "dev_mon_base: cmk-agent-ctl registered with ${_CMK_RECV_HOST}:8000" || \
warn "dev_mon_base: cmk-agent-ctl registration failed (non-fatal)"
fi
# Detect push mode and set up periodic push timer
_CTL_MODE=$(cmk-agent-ctl status 2>/dev/null | awk '/Connection mode:/{print $NF; exit}')
echo "$_CTL_MODE" > "$_CMK_CTL_STATUS_FILE" 2>/dev/null || true
if [[ "$_CTL_MODE" == "push-agent" ]]; then
log "dev_mon_base: push mode active — setting up push timer"
cmk-agent-ctl push 2>/dev/null || true
# Create systemd timer for periodic push (every 60s)
if [[ ! -f /etc/systemd/system/cmk-agent-push.timer ]]; then
cat > /etc/systemd/system/cmk-agent-push.service <<'UNIT'
[Unit]
Description=CheckMK agent push (ansipa-managed)
[Service]
Type=oneshot
ExecStart=/usr/bin/cmk-agent-ctl push
UNIT
cat > /etc/systemd/system/cmk-agent-push.timer <<'UNIT'
[Unit]
Description=CheckMK agent push timer (ansipa-managed)
[Timer]
OnBootSec=30s
OnUnitActiveSec=60s
[Install]
WantedBy=timers.target
UNIT
systemctl daemon-reload
systemctl enable --now cmk-agent-push.timer 2>/dev/null \
&& log "dev_mon_base: cmk-agent-push.timer enabled (push every 60s)" \
|| warn "dev_mon_base: could not enable push timer"
fi
fi
unset _CMK_RECV_HOST _ALREADY_CTL _CTL_OUT _CTL_MODE _CMK_CTL_STATUS_FILE
fi
# ── Persist CMK credentials for local scripts / push cron ─────────────
printf 'CMK_API=%q\nCMK_USER=%q\nCMK_SECRET=%q\n' \
"$CMK_API" "$CMK_USER" "$CMK_SECRET" > "$MON_CMK_CREDS"
chmod 600 "$MON_CMK_CREDS"
# ── dev_mon_base: installed-packages local check ───────────────────────
# CPU, RAM, disk, uptime are reported by the built-in check_mk_agent sections.
_cmk_write_check "ansipa_packages" '#!/bin/bash
PKG_COUNT=$(rpm -qa --qf "%{NAME}\n" 2>/dev/null | wc -l || dpkg-query -f "${Package}\n" -W 2>/dev/null | wc -l || echo 0)
RECENT=$(rpm -qa --qf "%{INSTALLTIME} %{NAME}\n" 2>/dev/null | sort -rn | head -5 | awk "{print \$2}" | tr "\n" "," | sed "s/,\$//" || echo "n/a")
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
# ── dev_mon_timeshift: snapshot age check ──────────────────────────────
if _mon_dev_active "timeshift"; then
_cmk_write_check "ansipa_timeshift" '#!/bin/bash
if ! command -v timeshift &>/dev/null; then
echo "3 Ansipa_Timeshift - Timeshift not installed on this host"
exit 0
fi
LAST=$(timeshift --list 2>/dev/null | grep -E "^\s*[0-9]+" \
| awk "{print \$3, \$4}" | tail -1 || echo "")
if [[ -z "$LAST" ]]; then
echo "2 Ansipa_Timeshift last_backup_days=9999;5;10 No Timeshift snapshots found"
exit 0
fi
LAST_TS=$(date -d "$LAST" +%s 2>/dev/null || echo 0)
DAYS=$(( ( $(date +%s) - LAST_TS ) / 86400 ))
PERF="last_backup_days=${DAYS};5;10"
if [[ "$DAYS" -ge 10 ]]; then
echo "2 Ansipa_Timeshift ${PERF} Last backup: ${DAYS}d ago (${LAST}) — CRIT"
elif [[ "$DAYS" -ge 5 ]]; then
echo "1 Ansipa_Timeshift ${PERF} Last backup: ${DAYS}d ago (${LAST}) — WARN"
else
echo "0 Ansipa_Timeshift ${PERF} Last backup: ${DAYS}d ago (${LAST})"
fi'
fi
# ── dev_mon_power: CPU package power via RAPL / lm-sensors ───────────
if _mon_dev_active "power"; then
_cmk_write_check "ansipa_power" '#!/bin/bash
RAPL=/sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj
POWER_W=0; SOURCE=""
if [[ -r "$RAPL" ]]; then
E1=$(cat "$RAPL" 2>/dev/null || echo 0)
sleep 1
E2=$(cat "$RAPL" 2>/dev/null || echo 0)
if [[ "$E2" -gt "$E1" ]]; then
POWER_W=$(( (E2 - E1) / 1000000 ))
SOURCE="RAPL"
fi
fi
if [[ -z "$SOURCE" ]] && command -v sensors &>/dev/null; then
W=$(sensors 2>/dev/null | grep -iE "package.*power|cpu.*power" \
| grep -oP "[0-9]+\.[0-9]+" | head -1 | cut -d. -f1 || echo 0)
[[ "${W:-0}" -gt 0 ]] && POWER_W="$W" && SOURCE="lm-sensors"
fi
if [[ -z "$SOURCE" ]]; then
echo "3 Ansipa_Power power_w=0 Power monitoring unavailable (needs Intel RAPL or lm-sensors)"
exit 0
fi
PERF="power_w=${POWER_W};65;95"
if [[ "$POWER_W" -ge 95 ]]; then
echo "2 Ansipa_Power ${PERF} CPU power: ${POWER_W}W via ${SOURCE} — CRIT"
elif [[ "$POWER_W" -ge 65 ]]; then
echo "1 Ansipa_Power ${PERF} CPU power: ${POWER_W}W via ${SOURCE} — WARN"
else
echo "0 Ansipa_Power ${PERF} CPU power: ${POWER_W}W via ${SOURCE}"
fi'
fi
# ── usr_mon_logins: SSH login audit (last 24h) ─────────────────────────
if _mon_usr_active "logins"; then
_cmk_write_check "ansipa_logins" '#!/bin/bash
count_journal() {
local pat="$1"
journalctl -u sshd --since "24 hours ago" --no-pager -q 2>/dev/null \
| grep -c "$pat" 2>/dev/null || true
}
count_secure() {
local pat="$1"
{ grep "$(date "+%b %e")" /var/log/secure 2>/dev/null
grep "$(date -d yesterday "+%b %e" 2>/dev/null || true)" /var/log/secure 2>/dev/null; } \
| grep -c "$pat" 2>/dev/null || true
}
if command -v journalctl &>/dev/null && journalctl -u sshd --since "24 hours ago" -q &>/dev/null; then
ACCEPTED=$(count_journal "Accepted ")
FAILED=$(count_journal "Failed ")
INVALID=$(count_journal "Invalid user ")
else
ACCEPTED=$(count_secure "sshd.*Accepted")
FAILED=$(count_secure "sshd.*Failed")
INVALID=$(count_secure "sshd.*Invalid user")
fi
PERF="successful=${ACCEPTED};; failed=${FAILED};10;50 invalid=${INVALID};5;20"
MSG="Logins 24h: ${ACCEPTED} ok / ${FAILED} failed / ${INVALID} invalid user"
if [[ "$FAILED" -ge 50 ]] || [[ "$INVALID" -ge 20 ]]; then
echo "2 Ansipa_Logins ${PERF} HIGH ACTIVITY — ${MSG}"
elif [[ "$FAILED" -ge 10 ]] || [[ "$INVALID" -ge 5 ]]; then
echo "1 Ansipa_Logins ${PERF} ELEVATED — ${MSG}"
else
echo "0 Ansipa_Logins ${PERF} ${MSG}"
fi'
fi
fi # CMK_SECRET non-empty
else
# ── Revert: host left dev_mon_base → deregister and clean up ─────────────
if [[ -f "$MON_STATE" ]]; then
_OLD_CMK_API=$(cat "$MON_STATE" 2>/dev/null || true)
if [[ -n "$_OLD_CMK_API" ]] && [[ -f "$MON_CMK_CREDS" ]]; then
source "$MON_CMK_CREDS" 2>/dev/null || true
_del_http=$(curl -sf -o /dev/null -w '%{http_code}' \
-X DELETE "${_OLD_CMK_API}/objects/host_config/${HOST_FQDN}" \
-H "Authorization: Bearer ${CMK_USER:-} ${CMK_SECRET:-}" \
-H "Accept: application/json" 2>/dev/null || echo "000")
if [[ "$_del_http" =~ ^(2|404) ]]; then
log "Deregistered ${HOST_FQDN} from CheckMK (HTTP ${_del_http})"
curl -sf \
-X POST "${_OLD_CMK_API}/domain-types/activation_run/actions/activate-changes/invoke" \
-H "Authorization: Bearer ${CMK_USER:-} ${CMK_SECRET:-}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"redirect":false,"sites":[],"force_foreign_changes":true}' \
>/dev/null 2>&1 || true
else
warn "CheckMK deregistration returned HTTP ${_del_http} — may need manual cleanup"
fi
fi
rm -f "$MON_STATE" "$MON_CMK_CREDS"
systemctl disable --now check_mk.socket 2>/dev/null || true
systemctl disable --now cmk-agent-push.timer 2>/dev/null || true
command -v cmk-agent-ctl &>/dev/null && cmk-agent-ctl delete-all 2>/dev/null || true
log "dev_mon_base reverted: check_mk.socket disabled, host deregistered"
unset _OLD_CMK_API _del_http
fi
fi
# ── Cleanup: remove local check scripts for groups no longer active ───────────
# _WANT_CHECKS is built by _cmk_write_check during the apply phase above, so it
# already contains exactly the checks that were installed this run. Anything in
# the state file that is not in _WANT_CHECKS needs to be removed.
while IFS= read -r _old_check; do
[[ -z "$_old_check" ]] && continue
_still_wanted=false
for _wc in "${_WANT_CHECKS[@]+"${_WANT_CHECKS[@]}"}"; do
[[ "$_wc" == "$_old_check" ]] && _still_wanted=true && break
done
if [[ "$_still_wanted" == false ]] && [[ -f "$MON_LOCAL_DIR/$_old_check" ]]; then
rm -f "$MON_LOCAL_DIR/$_old_check"
log "Removed CheckMK local check: $_old_check (group left)"
fi
done < "$MON_CHECKS_STATE"
unset _old_check _still_wanted _wc
# Persist current wanted check list.
if [[ ${#_WANT_CHECKS[@]} -gt 0 ]]; then
printf '%s\n' "${_WANT_CHECKS[@]+"${_WANT_CHECKS[@]}"}" | sort -u > "$MON_CHECKS_STATE"
else
> "$MON_CHECKS_STATE"
fi
unset _WANT_CHECKS _AGENT_INSTALLED
log "Policy enforcement complete."

View File

@ -22,6 +22,19 @@
# usr_smb_r_<name> Mount read-only Samba share ~/name for members; credentials in IPA group description
# usr_smb_rw_<name> Mount read-write Samba share ~/name for members (rw beats r if both)
#
# CheckMK monitoring policies (device host-groups, unless noted):
# dev_mon_base Install CheckMK agent; register host in CMK; check: installed packages.
# 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_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.
# usr_mon_logins (User group) Local check for SSH login attempts in last 24h.
# Reports: successful + failed + invalid-user counts with thresholds.
#
# Prerequisites:
# - Host enrolled in FreeIPA (sssd + ipa CLI available)
# - For dev_security-scan / usr_scan-notify: samba-client installed (handled below)
@ -42,6 +55,12 @@
tasks:
- name: Create local check scripts directory (CheckMK agent)
file:
path: /usr/lib/check_mk_agent/local
state: directory
mode: '0755'
- name: Install required packages
package:
name: "{{ item }}"
@ -49,8 +68,9 @@
loop:
- samba-client
- cups
- cifs-utils # needed for usr_smb_* CIFS mounts
- cifs-utils # needed for usr_smb_* CIFS mounts
- openssh-server # needed for dev_ssh_* (sshd must be running to accept keys)
- lm_sensors # needed for dev_mon_power fallback (Intel RAPL preferred)
ignore_errors: yes
- name: Deploy SMB credentials file

View File

@ -0,0 +1,2 @@
# Test credentials — never commit
.env

View File

@ -59,18 +59,22 @@ RUN systemctl mask \
network.service \
NetworkManager.service
COPY docker-env.service /etc/systemd/system/docker-env.service
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 docker-env.service /etc/systemd/system/docker-env.service
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 \
&& systemctl enable ansipa-smb.service \
&& systemctl enable ansipa-checkmk.service \
&& systemctl enable smb.service nmb.service crond.service
VOLUME ["/data"]

View File

@ -0,0 +1,162 @@
#!/bin/bash
# ansipa-checkmk-setup.sh — CheckMK CE integration for the IPA container.
#
# Phase 1 (one-time, guarded by /data/ansipa-checkmk.done):
# Creates /ansipa folder, IPA monitoring groups, stores CMK credentials.
#
# Phase 2 (runs every invocation after phase 1):
# Writes push-mode requests for all hosts in dev_mon_base to /cmk-creds/.
# The CheckMK container polls /cmk-creds/push-mode-*.req and updates hosts.mk.
# This lets clients in NAT environments push agent data without pull reachability.
#
# Credential format in IPA dev_mon_base description:
# cmk://<host>:<port>/<site>:automation:<secret>
set -euo pipefail
LOG_TAG="ansipa-checkmk-setup"
DONE_FLAG="/data/ansipa-checkmk.done"
CMK_CREDS_FILE="/cmk-creds/automation.secret"
log() { echo "[$LOG_TAG] $*"; }
warn() { echo "[$LOG_TAG][WARN] $*" >&2; }
# ── Phase 2: write push-mode requests for enrolled monitoring hosts ───────────
# Runs on EVERY invocation (not guarded by done flag). The CheckMK container
# polls /cmk-creds/push-mode-*.req and adds explicit_host_conf push-agent entries.
_write_push_requests() {
[[ -d /cmk-creds ]] || return 0
local _hosts _h
_hosts=$(ipa hostgroup-show dev_mon_base --all 2>/dev/null \
| awk '/Member hosts:/{p=1; next} /^$/{p=0} p{print}' \
| grep -oE '[a-zA-Z0-9._-]+\.[a-zA-Z]{2,}' | sort -u || true)
[[ -z "$_hosts" ]] && return 0
while IFS= read -r _h; do
[[ -z "$_h" ]] && continue
_req="/cmk-creds/push-mode-${_h}.req"
[[ -f "$_req" ]] && continue
echo "$_h" > "$_req" && log "Push-mode request created for ${_h}"
done <<< "$_hosts"
}
# Skip re-initialization but still run push-mode requests if already set up
if [[ -f "$DONE_FLAG" ]]; then
[[ -f /etc/ipa/default.conf ]] && \
echo "$IPA_ADMIN_PASS" | kinit "admin@${IPA_REALM}" &>/dev/null && \
_write_push_requests 2>/dev/null; \
kdestroy &>/dev/null || true
log "Already configured (delete $DONE_FLAG to force re-initialization)"
exit 0
fi
# ── Resolve configuration ──────────────────────────────────────────────────────
# docker-env.service writes container env to /etc/container.env; source it.
[[ -f /etc/container.env ]] && source /etc/container.env || true
CMK_URL="${CMK_URL:-http://172.30.0.12:5000}"
CMK_SITE="${CMK_SITE_ID:-cmk}"
CMK_API="${CMK_URL}/${CMK_SITE}/check_mk/api/1.0"
IPA_REALM="${IPA_REALM:-}"
IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-}"
if [[ -z "$IPA_REALM" || -z "$IPA_ADMIN_PASS" ]]; then
warn "IPA_REALM or IPA_ADMIN_PASSWORD not set — cannot configure IPA side"
exit 1
fi
# ── Wait for IPA to be fully configured ──────────────────────────────────────
if [[ ! -f /etc/ipa/default.conf ]]; then
warn "IPA not yet configured — waiting for ipa-first-boot to complete"
exit 1
fi
# ── Wait for CheckMK automation secret (written by checkmk container) ─────────
if [[ ! -f "$CMK_CREDS_FILE" ]]; then
warn "CheckMK automation secret not yet available at $CMK_CREDS_FILE"
warn " (CheckMK container writes it after site init — will retry)"
exit 1
fi
CMK_SECRET=$(tr -d '[:space:]' < "$CMK_CREDS_FILE")
if [[ -z "$CMK_SECRET" ]]; then
warn "CheckMK automation secret is empty — CheckMK not ready yet"
exit 1
fi
# ── Wait for CheckMK web API (use automation secret — no admin password needed) ──
log "Waiting for CheckMK API at ${CMK_API}..."
_wait=0
until curl -sf \
-H "Authorization: Bearer automation ${CMK_SECRET}" \
"${CMK_API}/version" >/dev/null 2>&1; do
_wait=$(( _wait + 15 ))
if [[ $_wait -ge 300 ]]; then
warn "CheckMK API not reachable after 5 minutes — will retry on next start"
exit 1
fi
sleep 15
done
log "CheckMK API is up"
# ── Create /ansipa folder in CheckMK ─────────────────────────────────────────
_folder_result=$(curl -sf -o /dev/null -w '%{http_code}' \
-X POST "${CMK_API}/domain-types/folder_config/collections/all" \
-H "Authorization: Bearer automation ${CMK_SECRET}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"name":"ansipa","title":"Ansipa Managed Hosts","parent":"/"}' \
2>/dev/null || echo "000")
if [[ "$_folder_result" =~ ^2 ]]; then
log "Created /ansipa folder in CheckMK"
elif [[ "$_folder_result" == "422" ]]; then
log "/ansipa folder already exists"
else
warn "Unexpected response creating /ansipa folder: HTTP $_folder_result"
fi
# ── Activate changes in CheckMK ───────────────────────────────────────────────
curl -sf \
-X POST "${CMK_API}/domain-types/activation_run/actions/activate-changes/invoke" \
-H "Authorization: Bearer automation ${CMK_SECRET}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"redirect":false,"sites":[],"force_foreign_changes":false}' \
>/dev/null 2>&1 && log "Changes activated" || warn "Activation failed (non-fatal)"
# ── Store CheckMK credentials in IPA host-group descriptions ─────────────────
# Format: cmk://<host>:<port>/<site>:automation:<secret>
CMK_HOST=$(echo "$CMK_URL" | sed 's|http://||')
CMK_DESC="cmk://${CMK_HOST}/${CMK_SITE}:automation:${CMK_SECRET}"
echo "$IPA_ADMIN_PASS" | kinit "admin@${IPA_REALM}" &>/dev/null || {
warn "kinit admin failed"
exit 1
}
# dev_mon_base stores the credentials; clients parse them to register.
if ipa hostgroup-show dev_mon_base &>/dev/null 2>&1; then
ipa hostgroup-mod dev_mon_base --desc="$CMK_DESC" &>/dev/null \
&& log "Updated dev_mon_base description with CMK credentials"
else
ipa hostgroup-add dev_mon_base --desc="$CMK_DESC" &>/dev/null \
&& log "Created IPA hostgroup dev_mon_base with CMK credentials"
fi
# Create supplementary monitoring host-groups (no credentials needed in description).
for _hgrp in dev_mon_malware dev_mon_timeshift dev_mon_power; do
ipa hostgroup-show "$_hgrp" &>/dev/null 2>&1 || \
ipa hostgroup-add "$_hgrp" &>/dev/null || true
done
# usr_mon_logins is a USER group (monitoring follows the user, not the device).
ipa group-show usr_mon_logins &>/dev/null 2>&1 || \
ipa group-add usr_mon_logins \
--desc="Members: SSH login monitoring enabled on enrolled devices" &>/dev/null || true
kdestroy &>/dev/null || true
touch "$DONE_FLAG"
log "CheckMK setup complete."
log " Phase 2 (push-mode requests) will run on next invocation."
log " Web UI: ${CMK_URL}/${CMK_SITE}/"
log " Login: cmkadmin / (CMK_ADMIN_PASSWORD from .env)"
log " Clients: add to dev_mon_base hostgroup — agent self-registers via ansipa"

View File

@ -0,0 +1,23 @@
[Unit]
Description=Ansipa CheckMK CE integration setup (one-time, after IPA + CMK are up)
# Wants to run after IPA is configured; however ipa-first-boot.service only runs
# once (ConditionPathExists=!/etc/ipa/default.conf) and takes up to 10 minutes.
# The script checks for /etc/ipa/default.conf itself and exits 1 when not ready,
# causing Restart=on-failure to retry until both IPA and CheckMK are available.
After=network.target docker-env.service
Wants=network.target
[Service]
Type=simple
ExecStart=/usr/local/sbin/ansipa-checkmk-setup.sh
StandardOutput=journal
StandardError=journal
# Retry until IPA first-boot and CheckMK container are both ready.
Restart=on-failure
RestartSec=30s
# Stop retrying once the done-flag file appears (script exits 0).
# RemainAfterExit ensures the unit stays 'active' after successful exit.
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target

View File

@ -28,6 +28,8 @@
volumes:
freeipa-data:
keycloak-db:
cmk-data: # CheckMK site data (persistent across restarts)
cmk-creds: # shared: CheckMK writes automation.secret here; FreeIPA reads it
networks:
ipa-net:
@ -52,15 +54,18 @@ services:
volumes:
- freeipa-data:/data
- /sys/fs/cgroup:/sys/fs/cgroup:rw
- cmk-creds:/cmk-creds # read CMK secret + write push-mode requests for hosts
environment:
IPA_DOMAIN: ${IPA_DOMAIN:?set IPA_DOMAIN in .env}
IPA_REALM: ${IPA_REALM:-}
IPA_ADMIN_PASSWORD: ${IPA_ADMIN_PASSWORD:?set IPA_ADMIN_PASSWORD in .env}
IPA_DM_PASSWORD: ${IPA_DM_PASSWORD:?set IPA_DM_PASSWORD in .env}
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}
IPA_DOMAIN: ${IPA_DOMAIN:?set IPA_DOMAIN in .env}
IPA_REALM: ${IPA_REALM:-}
IPA_ADMIN_PASSWORD: ${IPA_ADMIN_PASSWORD:?set IPA_ADMIN_PASSWORD in .env}
IPA_DM_PASSWORD: ${IPA_DM_PASSWORD:?set IPA_DM_PASSWORD in .env}
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
CMK_SITE_ID: ${CMK_SITE_ID:-cmk}
ports:
- "389:389"
- "636:636"
@ -101,6 +106,71 @@ services:
interval: 10s
retries: 5
# ── CheckMK CE ───────────────────────────────────────────────────────────────
# Monitoring server. Clients register themselves via ansipa-enforce-policies.sh
# when added to dev_mon_base. The automation user's secret is written to the
# cmk-creds shared volume so ansipa-checkmk-setup.sh (in the freeipa container)
# can store it in the IPA dev_mon_base hostgroup description for client discovery.
#
# Web UI: http://localhost:8090/cmk/ (user: cmkadmin, pass: CMK_ADMIN_PASSWORD)
# Agent dl: http://localhost:8090/cmk/check_mk/agents/
# Note: CheckMK 2.3 uses port 5000 (OMD apache); host port 8090 maps to it.
checkmk:
image: checkmk/check-mk-raw:2.3.0-latest
container_name: checkmk
hostname: checkmk.test.local
restart: unless-stopped
# Override entrypoint to copy the auto-generated automation secret to the
# shared volume so the freeipa container can read it without docker exec.
# $$ → $ after Compose expansion (i.e., $$! → $! in bash = background PID).
command:
- bash
- -c
- >-
/docker-entrypoint.sh & CMK_PID=$$!;
until [ -f /omd/sites/${CMK_SITE_ID:-cmk}/var/check_mk/web/automation/automation.secret ];
do sleep 3; done; sleep 5;
mkdir -p /cmk-creds;
cp /omd/sites/${CMK_SITE_ID:-cmk}/var/check_mk/web/automation/automation.secret /cmk-creds/automation.secret;
chmod 644 /cmk-creds/automation.secret;
printf '<LocationMatch ^/cmk/(omd|wiki|nagvis|check_mk|pnp4nagios|api)>\n Order allow,deny\n Allow from all\n Satisfy any\n</LocationMatch>\n'
> /omd/sites/${CMK_SITE_ID:-cmk}/etc/apache/conf.d/cookie_auth.conf;
omd reload ${CMK_SITE_ID:-cmk} apache 2>/dev/null || true;
while true; do
for _req in /cmk-creds/push-mode-*.req; do
[ -f "$$_req" ] || continue;
_hn=$$(cat "$$_req");
_hk=$$(echo "$$_hn" | tr . _);
_mk=/omd/sites/${CMK_SITE_ID:-cmk}/etc/check_mk/conf.d/wato/ansipa/hosts.mk;
[ -f "$$_mk" ] || continue;
grep -q "explicit_host_conf.*$$_hk\|'$$_hn'.*push-agent" "$$_mk" 2>/dev/null && { rm -f "$$_req"; continue; };
printf '\nexplicit_host_conf.setdefault('"'"'cmk_agent_connection'"'"', {})\nexplicit_host_conf['"'"'cmk_agent_connection'"'"'].update({'"'"'%s'"'"': '"'"'push-agent'"'"'})\n' "$$_hn" >> "$$_mk";
su - ${CMK_SITE_ID:-cmk} -c 'cmk -O' 2>/dev/null || true;
rm -f "$$_req";
done;
sleep 30;
done &
wait $$CMK_PID
environment:
CMK_SITE_ID: ${CMK_SITE_ID:-cmk}
CMK_PASSWORD: ${CMK_ADMIN_PASSWORD:?set CMK_ADMIN_PASSWORD in .env}
ports:
- "8090:5000" # web UI (CheckMK 2.3 uses port 5000 for OMD apache)
- "8000:8000" # agent receiver (push mode / cmk-agent-ctl)
- "6557:6557" # livestatus (optional, for external tools)
volumes:
- cmk-data:/omd/sites
- cmk-creds:/cmk-creds
networks:
ipa-net:
ipv4_address: 172.30.0.12
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:5000/${CMK_SITE_ID:-cmk}/check_mk/api/1.0/version >/dev/null 2>&1"]
interval: 30s
timeout: 15s
retries: 20
start_period: 180s
# ── Keycloak ─────────────────────────────────────────────────────────────────
# After first start, run ./keycloak-configure.sh to wire FreeIPA LDAP federation.
keycloak: