From 6bb19cea786f941e6a9b3fbe945ec9089994c703 Mon Sep 17 00:00:00 2001 From: The_miro Date: Wed, 1 Jul 2026 18:32:28 +0200 Subject: [PATCH] feat(ansipa): integrate CheckMK CE monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01DHops6PU4c2Mv5UyhUj8Ms --- .../ansible/ansipa-enforce-policies.sh | 438 +++++++++++++++++- .../ansible/deploy-ansipa-policies.yml | 22 +- setup/modules/FreeipaAnsible/image/.gitignore | 2 + setup/modules/FreeipaAnsible/image/Dockerfile | 16 +- .../image/ansipa-checkmk-setup.sh | 162 +++++++ .../image/ansipa-checkmk.service | 23 + .../FreeipaAnsible/image/docker-compose.yml | 86 +++- 7 files changed, 732 insertions(+), 17 deletions(-) create mode 100644 setup/modules/FreeipaAnsible/image/.gitignore create mode 100644 setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh create mode 100644 setup/modules/FreeipaAnsible/image/ansipa-checkmk.service diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh index 7aaa2a1..5e5a370 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh @@ -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_ 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://:/:automation: + +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 +# 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." diff --git a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml index 8970409..0817215 100644 --- a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml +++ b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml @@ -22,6 +22,19 @@ # usr_smb_r_ Mount read-only Samba share ~/‹name› for members; credentials in IPA group description # usr_smb_rw_ Mount read-write Samba share ~/‹name› for members (rw beats r if both) # +# 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 diff --git a/setup/modules/FreeipaAnsible/image/.gitignore b/setup/modules/FreeipaAnsible/image/.gitignore new file mode 100644 index 0000000..65f6a9c --- /dev/null +++ b/setup/modules/FreeipaAnsible/image/.gitignore @@ -0,0 +1,2 @@ +# Test credentials — never commit +.env diff --git a/setup/modules/FreeipaAnsible/image/Dockerfile b/setup/modules/FreeipaAnsible/image/Dockerfile index f4e3708..2a83f7e 100644 --- a/setup/modules/FreeipaAnsible/image/Dockerfile +++ b/setup/modules/FreeipaAnsible/image/Dockerfile @@ -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"] diff --git a/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh b/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh new file mode 100644 index 0000000..07a220e --- /dev/null +++ b/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh @@ -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://:/:automation: + +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://:/:automation: +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" diff --git a/setup/modules/FreeipaAnsible/image/ansipa-checkmk.service b/setup/modules/FreeipaAnsible/image/ansipa-checkmk.service new file mode 100644 index 0000000..de8dd5d --- /dev/null +++ b/setup/modules/FreeipaAnsible/image/ansipa-checkmk.service @@ -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 diff --git a/setup/modules/FreeipaAnsible/image/docker-compose.yml b/setup/modules/FreeipaAnsible/image/docker-compose.yml index 106c6f9..68371fc 100644 --- a/setup/modules/FreeipaAnsible/image/docker-compose.yml +++ b/setup/modules/FreeipaAnsible/image/docker-compose.yml @@ -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 '\n Order allow,deny\n Allow from all\n Satisfy any\n\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: