Compare commits

...

2 Commits

Author SHA1 Message Date
Amir Alexander Abdelbaki 6bb19cea78 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
2026-07-01 18:32:28 +02:00
Amir Alexander Abdelbaki 76c3c2c1c5 feat(ansipa): add usr_smb_* and dev_ssh_* policies
usr_smb_r_<name> / usr_smb_rw_<name> (user groups):
- Server creates /data/smb-shares/<name>/ with smb-<name> system user,
  setgid directory, and Samba share stanzas [usr-<name>-r/rw] on startup
- Auto-generates credentials and stores them in IPA group descriptions
  (cifs://<host>:<samba-user>:<password>) so no out-of-band secret distribution
- Client mounts //ipa/<share> at ~/‹name› for logged-in members;
  rw membership wins over r when user is in both groups
- Revert: unmounts and rmdir the mount point on group leave

dev_ssh_<userid> (host group):
- Reads IPA user's SSH public keys (ipasshpubkey / --all output)
- Writes them into /home/<userid>/.ssh/authorized_keys in an
  ansipa-managed section (preserves manually-added keys)
- Home dir is created if it doesn't exist (user may not have logged in yet)
- Revert: removes only the ansipa-managed section on group leave
- Enables same-key SSH access from any registered device to enrolled hosts

Also: add cifs-utils + openssh-server to Ansible package list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DHops6PU4c2Mv5UyhUj8Ms
2026-07-01 13:52:26 +02:00
8 changed files with 1195 additions and 19 deletions

View File

@ -81,6 +81,8 @@ RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \
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
@ -100,6 +102,8 @@ if [[ -n "$RAW_GROUPS" ]]; then
dev_security-scan) WANT_SECURITY_SCAN=true ;;
dev_no-local-users) WANT_NO_LOCAL_USERS=true ;;
dev_local-sudo-*) ACTIVE_LOCAL_SUDO_USERS+=("${g#dev_local-sudo-}") ;;
dev_ssh_*) ACTIVE_SSH_USERS+=("${g#dev_ssh_}") ;;
dev_mon_*) ACTIVE_MON_DEV+=("${g#dev_mon_}") ;;
esac
done
done <<< "$RAW_GROUPS"
@ -151,14 +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}"
"| no-local-users: $WANT_NO_LOCAL_USERS | local-sudo: ${ACTIVE_LOCAL_SUDO_USERS[*]:-none}" \
"| ssh-keys: ${ACTIVE_SSH_USERS[*]:-none} | mon-dev: ${ACTIVE_MON_DEV[*]:-none}"
log "User policies — admin: $WANT_USR_ADMIN" \
"| block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
"| printers: ${ACTIVE_PRT_PRINTERS[*]:-none}" \
"| scan-notify: $WANT_SCAN_NOTIFY"
"| scan-notify: $WANT_SCAN_NOTIFY | mon-usr: ${ACTIVE_MON_USR[*]:-none}"
# ── Helpers ───────────────────────────────────────────────────────────────────
_in_block_list() {
@ -771,4 +789,710 @@ else
fi
unset _ap
# ── usr_smb_<r|rw>_<name>: auto-mount network shares in user home dirs ────────
# For each usr_smb_r_<name> or usr_smb_rw_<name> group in FreeIPA:
# - The FreeIPA container hosts a Samba share [usr-<name>-r] or [usr-<name>-rw]
# under /data/smb-shares/<name>/ with UNIX group permissions.
# - When a logged-in user is a member of the group the share is mounted at
# ~/name (a new folder named after the share, no r/rw suffix).
# - rw membership takes precedence over r if the user is in both groups for
# the same share name.
# - The mount is tracked in the state file; leaving the group unmounts it.
#
# Credential string (server, Samba user, password) is stored in the IPA group
# description by ansipa-smb-setup.sh: cifs://<ipa-host>:<samba-user>:<password>
#
# Requires: cifs-utils installed on the client.
SMB_MOUNT_STATE="$STATE_DIR/smb-mounts"
[[ -f "$SMB_MOUNT_STATE" ]] || touch "$SMB_MOUNT_STATE"
SMB_CREDS_DIR="$STATE_DIR/smb-creds"
mkdir -p "$SMB_CREDS_DIR"
chmod 700 "$SMB_CREDS_DIR"
if command -v mount.cifs &>/dev/null; then
# ── Discover usr_smb_* groups from IPA ───────────────────────────────────
declare -A _SMB_R_GRP _SMB_RW_GRP # share-name → IPA group name
while IFS= read -r _g; do
[[ -z "$_g" ]] && continue
if [[ "$_g" =~ ^usr_smb_r_(.+)$ ]]; then _SMB_R_GRP["${BASH_REMATCH[1]}"]="$_g"
elif [[ "$_g" =~ ^usr_smb_rw_(.+)$ ]]; then _SMB_RW_GRP["${BASH_REMATCH[1]}"]="$_g"
fi
done < <(ipa group-find --pkey-only 2>/dev/null \
| awk '/Group name:/ {print $NF}' \
| grep "^usr_smb_" | sort -u || true)
unset _g
# Union of r + rw share names.
declare -A _SMB_NAMES
for _n in "${!_SMB_R_GRP[@]}" "${!_SMB_RW_GRP[@]}"; do _SMB_NAMES["$_n"]=1; done
unset _n
# ── Helpers ───────────────────────────────────────────────────────────────
# Outputs "host samba_user password" when description contains a cifs:// credential.
_smb_parse_cred() {
local _desc
_desc=$(ipa group-show "$1" --all 2>/dev/null \
| awk -F': ' '/Description:/{print $2; exit}' || true)
[[ "$_desc" =~ ^cifs://([^:]+):([^:]+):(.+)$ ]] && \
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]}"
}
_smb_member() { id -nG "$1" 2>/dev/null | tr ' ' '\n' | grep -qxF "$2"; }
# ── Collect active logged-in users (loginctl → who fallback) ─────────────
declare -A _SMB_SESS # user → home
_smb_add_sess() {
local _u
[[ "$2" == "loginctl" ]] && _u=$(awk '{print $3}' <<< "$1") \
|| _u=$(awk '{print $1}' <<< "$1")
[[ -z "$_u" || "$_u" == "root" || "$_u" == "USER" ]] && return 0
local _h; _h=$(getent passwd "$_u" | cut -d: -f6 2>/dev/null) || return 0
[[ -d "$_h" ]] && _SMB_SESS["$_u"]="$_h"
}
if loginctl list-sessions --no-legend &>/dev/null; then
while IFS= read -r _L; do _smb_add_sess "$_L" "loginctl"
done < <(loginctl list-sessions --no-legend 2>/dev/null)
else
while IFS= read -r _L; do _smb_add_sess "$_L" "who"
done < <(who 2>/dev/null)
fi
unset _L
# ── Apply: mount shares for logged-in members ─────────────────────────────
for _SMB_NAME in "${!_SMB_NAMES[@]}"; do
_R_GRP="${_SMB_R_GRP[$_SMB_NAME]:-}"
_RW_GRP="${_SMB_RW_GRP[$_SMB_NAME]:-}"
# Both access levels use the same credential; read from whichever has it.
_CRED_LINE=""
[[ -n "$_RW_GRP" ]] && _CRED_LINE=$(_smb_parse_cred "$_RW_GRP")
[[ -z "$_CRED_LINE" && -n "$_R_GRP" ]] && _CRED_LINE=$(_smb_parse_cred "$_R_GRP")
if [[ -z "$_CRED_LINE" ]]; then
warn "No credential in IPA description for share $_SMB_NAME — skipped (run ansipa-smb.service on FreeIPA container)"
continue
fi
read -r _SMB_HOST _SMB_SUSER _SMB_SPASS <<< "$_CRED_LINE"
_CREDS_FILE="$SMB_CREDS_DIR/${_SMB_NAME}.creds"
printf 'username=%s\npassword=%s\ndomain=WORKGROUP\n' "$_SMB_SUSER" "$_SMB_SPASS" \
> "$_CREDS_FILE"
chmod 600 "$_CREDS_FILE"
for _SMB_U in "${!_SMB_SESS[@]}"; do
# Determine per-user access level (rw beats r).
_SMB_LVL=""
[[ -n "$_RW_GRP" ]] && _smb_member "$_SMB_U" "$_RW_GRP" && _SMB_LVL="rw"
[[ -z "$_SMB_LVL" && -n "$_R_GRP" ]] && _smb_member "$_SMB_U" "$_R_GRP" && _SMB_LVL="r"
[[ -z "$_SMB_LVL" ]] && continue
_SMB_H="${_SMB_SESS[$_SMB_U]}"
_SMB_MP="$_SMB_H/$_SMB_NAME"
_SMB_SHARE="usr-${_SMB_NAME}-${_SMB_LVL}"
if mountpoint -q "$_SMB_MP" 2>/dev/null; then
grep -qxF "${_SMB_U}:${_SMB_NAME}" "$SMB_MOUNT_STATE" 2>/dev/null || \
echo "${_SMB_U}:${_SMB_NAME}" >> "$SMB_MOUNT_STATE"
continue
fi
mkdir -p "$_SMB_MP"
chown "$_SMB_U" "$_SMB_MP" 2>/dev/null || true
if mount -t cifs "//${_SMB_HOST}/${_SMB_SHARE}" "$_SMB_MP" \
-o "credentials=${_CREDS_FILE},uid=${_SMB_U},gid=${_SMB_U},file_mode=0644,dir_mode=0755,nounix,noserverino" \
2>/dev/null; then
log "Mounted //${_SMB_HOST}/${_SMB_SHARE}$_SMB_MP for $_SMB_U"
grep -qxF "${_SMB_U}:${_SMB_NAME}" "$SMB_MOUNT_STATE" 2>/dev/null || \
echo "${_SMB_U}:${_SMB_NAME}" >> "$SMB_MOUNT_STATE"
else
warn "Failed to mount //${_SMB_HOST}/${_SMB_SHARE} for $_SMB_U"
rmdir "$_SMB_MP" 2>/dev/null || true
fi
done
done
unset _SMB_NAME _R_GRP _RW_GRP _CRED_LINE _SMB_HOST _SMB_SUSER _SMB_SPASS \
_CREDS_FILE _SMB_U _SMB_H _SMB_MP _SMB_SHARE _SMB_LVL
# ── Revert: unmount shares whose group was removed or user left ───────────
_NEW_SMB_MOUNTS=()
while IFS=: read -r _OLD_USER _OLD_NAME; do
[[ -z "$_OLD_USER" || -z "$_OLD_NAME" ]] && continue
_OLD_HOME=$(getent passwd "$_OLD_USER" | cut -d: -f6 2>/dev/null) || continue
_OLD_MPT="$_OLD_HOME/$_OLD_NAME"
_STILL_MEMBER=false
if [[ -n "${_SMB_R_GRP[$_OLD_NAME]+x}" ]] && \
_smb_member "$_OLD_USER" "${_SMB_R_GRP[$_OLD_NAME]}"; then
_STILL_MEMBER=true
elif [[ -n "${_SMB_RW_GRP[$_OLD_NAME]+x}" ]] && \
_smb_member "$_OLD_USER" "${_SMB_RW_GRP[$_OLD_NAME]}"; then
_STILL_MEMBER=true
fi
if [[ "$_STILL_MEMBER" == true ]]; then
_NEW_SMB_MOUNTS+=("${_OLD_USER}:${_OLD_NAME}")
else
umount -l "$_OLD_MPT" 2>/dev/null || true
rmdir "$_OLD_MPT" 2>/dev/null || true
log "Unmounted ${_OLD_MPT} for ${_OLD_USER} (left usr_smb_*_${_OLD_NAME})"
fi
done < "$SMB_MOUNT_STATE"
unset _OLD_USER _OLD_NAME _OLD_HOME _OLD_MPT _STILL_MEMBER
if [[ ${#_NEW_SMB_MOUNTS[@]} -gt 0 ]]; then
printf '%s\n' "${_NEW_SMB_MOUNTS[@]}" | sort -u > "$SMB_MOUNT_STATE"
else
> "$SMB_MOUNT_STATE"
fi
# Credential files are written fresh on each run; remove stale ones.
rm -f "$SMB_CREDS_DIR"/*.creds 2>/dev/null || true
unset _SMB_R_GRP _SMB_RW_GRP _SMB_NAMES _SMB_SESS _NEW_SMB_MOUNTS
else
warn "mount.cifs not found — install cifs-utils to enable usr_smb_* policies"
[[ -s "$SMB_MOUNT_STATE" ]] && \
warn "smb-mounts state is non-empty — cannot revert existing mounts without cifs-utils" || true
fi
# ── dev_ssh_<userid>: distribute SSH public keys to user home dirs ────────────
# When this host is in dev_ssh_<userid>, the IPA user <userid>'s public keys are
# written to /home/<userid>/.ssh/authorized_keys in an ansipa-managed section.
# This enables Amir (or any named IPA user) to SSH into this device using the key
# registered in their IPA profile — the same key reaches every enrolled device.
#
# Keys are stored in the IPA user object (not on each device); add via:
# ipa user-mod <userid> --sshpubkey="ssh-ed25519 AAAA..."
# Multiple keys (e.g. workstation + laptop) are each added with --sshpubkey.
#
# The ansipa-managed section in authorized_keys is marked with:
# # BEGIN ansipa-ssh-managed … # END ansipa-ssh-managed
# This preserves any manually-added keys and is cleanly removed on revert.
#
# Leaving the host group removes only the ansipa-managed section on the next run.
SSH_KEY_STATE="$STATE_DIR/ssh-keys"
[[ -f "$SSH_KEY_STATE" ]] || touch "$SSH_KEY_STATE"
# Write or clear the ansipa-managed section in an authorized_keys file.
# Usage: _ssh_write_keys <user> <homedir> [key1 key2 ...]
# Passing no keys removes the section (revert).
_ssh_write_keys() {
local _u="$1" _h="$2"; shift 2
local _auth="$_h/.ssh/authorized_keys"
mkdir -p "$_h/.ssh"
chmod 700 "$_h/.ssh"
[[ -f "$_auth" ]] || touch "$_auth"
# Strip any existing ansipa section.
local _rest
_rest=$(awk '
/^# BEGIN ansipa-ssh-managed$/ { skip=1; next }
skip && /^# END ansipa-ssh-managed$/ { skip=0; next }
!skip
' "$_auth" 2>/dev/null || true)
if [[ $# -gt 0 ]]; then
{ [[ -n "$_rest" ]] && printf '%s\n' "$_rest"; \
echo "# BEGIN ansipa-ssh-managed"; \
printf '%s\n' "$@"; \
echo "# END ansipa-ssh-managed"; } > "$_auth"
else
[[ -n "$_rest" ]] && printf '%s\n' "$_rest" > "$_auth" || > "$_auth"
fi
chmod 600 "$_auth"
chown "$_u:$_u" "$_h/.ssh" "$_auth" 2>/dev/null || true
}
declare -A _SSH_APPLIED # userid → 1 (applied this run)
for _SSH_UID in "${ACTIVE_SSH_USERS[@]+"${ACTIVE_SSH_USERS[@]}"}"; do
# Verify the IPA user exists.
ipa user-show "$_SSH_UID" &>/dev/null || {
warn "dev_ssh_${_SSH_UID}: IPA user '$_SSH_UID' not found — skipping"
continue
}
# Fetch SSH public keys from IPA user profile.
# `ipa user-show --all` shows: " SSH public key: <key>"
# " SSH public key fingerprint: ..." is a separate field — exclude it.
_SSH_KEYS=()
mapfile -t _SSH_KEYS < <(
ipa user-show "$_SSH_UID" --all 2>/dev/null \
| grep "^ SSH public key:" | grep -v "fingerprint" \
| sed 's/^ SSH public key: //' || true
)
if [[ ${#_SSH_KEYS[@]} -eq 0 ]]; then
warn "dev_ssh_${_SSH_UID}: '$_SSH_UID' has no SSH keys in IPA — add via:"
warn " ipa user-mod $_SSH_UID --sshpubkey='ssh-ed25519 AAAA...'"
continue
fi
# Resolve home directory via SSSD (works for IPA users once SSSD is running).
_SSH_HOME=$(getent passwd "$_SSH_UID" 2>/dev/null | cut -d: -f6 || true)
if [[ -z "$_SSH_HOME" ]]; then
warn "dev_ssh_${_SSH_UID}: '$_SSH_UID' not resolvable via getent — is SSSD running?"
continue
fi
# Create home dir now if it doesn't yet exist (IPA user may not have logged in).
if [[ ! -d "$_SSH_HOME" ]]; then
log "Creating home dir for $_SSH_UID: $_SSH_HOME"
mkdir -p "$_SSH_HOME"
chown "$_SSH_UID:$_SSH_UID" "$_SSH_HOME" 2>/dev/null || true
chmod 700 "$_SSH_HOME"
fi
_ssh_write_keys "$_SSH_UID" "$_SSH_HOME" "${_SSH_KEYS[@]}"
log "SSH keys for $_SSH_UID: ${#_SSH_KEYS[@]} key(s) written to $_SSH_HOME/.ssh/authorized_keys"
_SSH_APPLIED["$_SSH_UID"]=1
grep -qxF "$_SSH_UID" "$SSH_KEY_STATE" 2>/dev/null || echo "$_SSH_UID" >> "$SSH_KEY_STATE"
done
unset _SSH_UID _SSH_KEYS _SSH_HOME
# Revert: remove ansipa keys for users whose dev_ssh_* group was removed from this host.
_NEW_SSH_STATE=()
while IFS= read -r _OLD_SSH_UID; do
[[ -z "$_OLD_SSH_UID" ]] && continue
if [[ -n "${_SSH_APPLIED[$_OLD_SSH_UID]+x}" ]]; then
_NEW_SSH_STATE+=("$_OLD_SSH_UID")
else
_OLD_SSH_HOME=$(getent passwd "$_OLD_SSH_UID" 2>/dev/null | cut -d: -f6 || true)
if [[ -n "$_OLD_SSH_HOME" ]]; then
_ssh_write_keys "$_OLD_SSH_UID" "$_OLD_SSH_HOME" # no keys = remove section
log "Removed ansipa SSH keys for $_OLD_SSH_UID (host left dev_ssh_${_OLD_SSH_UID})"
fi
fi
done < "$SSH_KEY_STATE"
unset _OLD_SSH_UID _OLD_SSH_HOME
if [[ ${#_NEW_SSH_STATE[@]} -gt 0 ]]; then
printf '%s\n' "${_NEW_SSH_STATE[@]}" | sort -u > "$SSH_KEY_STATE"
else
> "$SSH_KEY_STATE"
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

@ -9,12 +9,31 @@
# dev_security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans + SMB upload (02:00)
# dev_no-local-users Lock local account passwords; only FreeIPA accounts can auth
# dev_local-sudo-<username> Grant <username> local sudo on this device; reverted when host leaves
# dev_ssh_<userid> Distribute <userid>'s SSH public keys from IPA to authorized_keys
# on this device; allows that IPA user to SSH in from any of their
# registered keys without a password. Keys stored in IPA via:
# ipa user-mod <userid> --sshpubkey='ssh-ed25519 AAAA...'
#
# User policies (FreeIPA user groups — follow the user across all enrolled devices):
# usr_admin Grant full sudo on every enrolled host (sudoers drop-in, SSSD-resolved)
# usr_block-binary-<name> Block execution of <name> via a PATH-priority wrapper
# usr_prt_<printer> Auto-add printer at login (per-user); URI in IPA group description
# usr_scan-notify Fetch alerts from server, notify user every 10 min until acknowledged
# usr_smb_r_<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)
@ -36,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 }}"
@ -43,6 +68,9 @@
loop:
- samba-client
- cups
- 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

@ -21,6 +21,7 @@ set -euo pipefail
LOG_TAG="ansipa-smb-setup"
SCAN_BASE="/data/scan-results"
LUKS_BASE="/data/luks-keys"
SHARES_BASE="/data/smb-shares"
SMB_CONF="/etc/samba/smb.conf"
SMB_USER="scanupload"
LUKS_UPLOAD_USER="luks-upload"
@ -28,17 +29,20 @@ KEYADMIN_GROUP="KeyAdmin"
ENV_FILE="/data/samba/ansipa-smb.env"
log() { echo "[$LOG_TAG] $*"; }
warn() { echo "[$LOG_TAG][WARN] $*" >&2; }
die() { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; }
# ── Resolve passwords ─────────────────────────────────────────────────────────
SMB_PASS="${SMB_SCAN_PASSWORD:-}"
LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-}"
IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-}"
if [[ -f "$ENV_FILE" ]]; then
# shellcheck source=/dev/null
source "$ENV_FILE"
SMB_PASS="${SMB_SCAN_PASSWORD:-$SMB_PASS}"
LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-$LUKS_PASS}"
IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-$IPA_ADMIN_PASS}"
fi
[[ -z "$SMB_PASS" ]] && die "SMB_SCAN_PASSWORD not set and $ENV_FILE not present. Set it in .env."
@ -49,12 +53,14 @@ mkdir -p "$(dirname "$ENV_FILE")"
{
printf 'SMB_SCAN_PASSWORD=%q\n' "$SMB_PASS"
printf 'LUKS_KEY_UPLOAD_PASSWORD=%q\n' "$LUKS_PASS"
[[ -n "$IPA_ADMIN_PASS" ]] && printf 'IPA_ADMIN_PASSWORD=%q\n' "$IPA_ADMIN_PASS" || true
} > "$ENV_FILE"
chmod 600 "$ENV_FILE"
# ── Directory structure (idempotent) ──────────────────────────────────────────
mkdir -p "$SCAN_BASE/archive" "$SCAN_BASE/alerts"
mkdir -p "$LUKS_BASE"
mkdir -p "$SHARES_BASE"
# ── KeyAdmin group ────────────────────────────────────────────────────────────
if ! getent group "$KEYADMIN_GROUP" &>/dev/null; then
@ -80,9 +86,10 @@ chown -R "$SMB_USER:$SMB_USER" "$SCAN_BASE"
chown -R "root:$KEYADMIN_GROUP" "$LUKS_BASE"
chmod 2750 "$LUKS_BASE" # setgid so new files inherit KeyAdmin group
# ── smb.conf ──────────────────────────────────────────────────────────────────
log "Writing $SMB_CONF"
cat > "$SMB_CONF" <<CONF
# ── smb.conf (base only; _setup_user_shares appends stanzas afterwards) ───────
_write_smb_conf() {
log "Writing $SMB_CONF"
cat > "$SMB_CONF" <<CONF
[global]
workgroup = WORKGROUP
server string = Ansipa Security Server
@ -118,6 +125,7 @@ cat > "$SMB_CONF" <<CONF
create mask = 0640
directory mask = 0750
CONF
}
# ── Samba passwords (idempotent — smbpasswd -a adds or updates) ───────────────
_smb_set_pass() {
@ -128,9 +136,164 @@ _smb_set_pass() {
log "WARN: smbpasswd returned non-zero for $user (may already be set correctly)"
}
# ── User-share management (usr_smb_r_<name> / usr_smb_rw_<name> IPA groups) ──
# Each group maps to a Samba share under $SHARES_BASE/<name>/:
# usr_smb_r_<name> → read-only share [usr-<name>-r]
# usr_smb_rw_<name> → read-write share [usr-<name>-rw]
#
# Both access levels for the same share use one Linux user (smb-<name>) and one
# password; the difference is read-only vs read-write enforced by Samba.
# Credentials are auto-generated on first setup and stored in the IPA group
# description so clients can retrieve them:
# cifs://<ipa-host>:<samba-user>:<password>
#
# The server kinits as admin to query/update IPA. If IPA is not yet configured
# (first-boot race), this function exits gracefully; the next smb.service restart
# will pick up the groups once IPA is ready.
# Appends user share stanzas directly to SMB_CONF; logs go to stdout→journal.
# Split from _write_smb_conf to avoid stdout capture swallowing log lines.
_setup_user_shares() {
# Require IPA to be configured on this host already.
if [[ ! -f /etc/ipa/default.conf ]]; then
warn "IPA not yet configured — skipping user share setup (restart after ipa-first-boot)"
return 0
fi
if [[ -z "$IPA_ADMIN_PASS" ]]; then
warn "IPA_ADMIN_PASSWORD not set — skipping user share setup"
return 0
fi
local realm
realm=$(awk -F'[[:space:]]*=[[:space:]]*' '/^realm[[:space:]]*=/{print $2; exit}' \
/etc/ipa/default.conf 2>/dev/null || echo "")
# On the IPA server itself the 'server' field points to itself; prefer hostname -f.
local ipa_host
ipa_host=$(hostname -f 2>/dev/null || echo "ipa.test.local")
echo "$IPA_ADMIN_PASS" | kinit "admin@${realm}" &>/dev/null || {
warn "kinit admin@${realm} failed — skipping user share setup"
return 0
}
# Discover usr_smb_* groups.
local -a smb_groups
mapfile -t smb_groups < <(
ipa group-find --pkey-only 2>/dev/null \
| awk '/Group name:/ {print $NF}' \
| grep "^usr_smb_" | sort -u || true
)
if [[ ${#smb_groups[@]} -eq 0 ]]; then
log "No usr_smb_* groups found — no user shares to configure"
kdestroy &>/dev/null || true
return 0
fi
# Collect unique share names and track which access levels exist.
declare -A _HAS_R _HAS_RW _DONE
for _g in "${smb_groups[@]}"; do
if [[ "$_g" =~ ^usr_smb_r_(.+)$ ]]; then _HAS_R["${BASH_REMATCH[1]}"]=1
elif [[ "$_g" =~ ^usr_smb_rw_(.+)$ ]]; then _HAS_RW["${BASH_REMATCH[1]}"]=1
fi
done
for NAME in "${!_HAS_R[@]}" "${!_HAS_RW[@]}"; do
[[ -n "${_DONE[$NAME]+x}" ]] && continue
_DONE["$NAME"]=1
local SUSER="smb-${NAME}"
local SDIR="$SHARES_BASE/$NAME"
log "User share: $NAME path=$SDIR user=$SUSER"
# Create directory and Linux user.
mkdir -p "$SDIR"
if ! id "$SUSER" &>/dev/null; then
useradd -r -s /sbin/nologin -d "$SDIR" -M "$SUSER"
log " Created system user: $SUSER"
fi
chown "$SUSER:$SUSER" "$SDIR"
chmod 2770 "$SDIR" # setgid — new files inherit group; no world access
# Retrieve existing password from IPA group description or generate one.
local PASS=""
local _desc _grp
for _grp in "usr_smb_r_${NAME}" "usr_smb_rw_${NAME}"; do
ipa group-show "$_grp" &>/dev/null || continue
_desc=$(ipa group-show "$_grp" --all 2>/dev/null \
| awk -F': ' '/Description:/{print $2; exit}' || true)
# Description format: cifs://<host>:<samba-user>:<password>
if [[ "$_desc" =~ ^cifs://[^:]*:smb-[^:]+:(.+)$ ]]; then
PASS="${BASH_REMATCH[1]}"
break
fi
done
if [[ -z "$PASS" ]]; then
PASS=$(tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 32 \
|| openssl rand -base64 24 | tr -d '/+=\n')
log " Generated new password for $SUSER"
fi
# Set Samba password.
_smb_set_pass "$SUSER" "$PASS"
# Store credential in both group descriptions so clients can read it.
local CRED="cifs://${ipa_host}:${SUSER}:${PASS}"
if [[ -n "${_HAS_R[$NAME]+x}" ]]; then
ipa group-mod "usr_smb_r_${NAME}" --desc="$CRED" &>/dev/null && \
log " Stored credential in usr_smb_r_${NAME}" || true
fi
if [[ -n "${_HAS_RW[$NAME]+x}" ]]; then
ipa group-mod "usr_smb_rw_${NAME}" --desc="$CRED" &>/dev/null && \
log " Stored credential in usr_smb_rw_${NAME}" || true
fi
# Append share stanzas directly to smb.conf.
# Clients mount [usr-<name>-r] or [usr-<name>-rw]; folder in ~ is just <name>.
if [[ -n "${_HAS_R[$NAME]+x}" ]]; then
cat >> "$SMB_CONF" <<STANZA
[usr-${NAME}-r]
comment = Ansipa user share: ${NAME} (read-only)
path = ${SDIR}
valid users = ${SUSER}
read only = yes
browseable = no
force user = ${SUSER}
force group = ${SUSER}
STANZA
fi
if [[ -n "${_HAS_RW[$NAME]+x}" ]]; then
cat >> "$SMB_CONF" <<STANZA
[usr-${NAME}-rw]
comment = Ansipa user share: ${NAME} (read-write)
path = ${SDIR}
valid users = ${SUSER}
read only = no
write list = ${SUSER}
browseable = no
force user = ${SUSER}
force group = ${SUSER}
create mask = 0664
directory mask = 02775
STANZA
fi
done
unset _HAS_R _HAS_RW _DONE _g NAME SUSER SDIR PASS _desc _grp CRED
kdestroy &>/dev/null || true
}
# ── Write smb.conf, set passwords, configure user shares ─────────────────────
_smb_set_pass "$SMB_USER" "$SMB_PASS"
_smb_set_pass "$LUKS_UPLOAD_USER" "$LUKS_PASS"
# Write base smb.conf first, then have _setup_user_shares append to it.
_write_smb_conf
_setup_user_shares
# Reload smbd if it is already running (picks up new user shares without restart).
smbcontrol smbd reload-config 2>/dev/null || true
# ── Server-side scan checker cron (hourly, analysed on the IPA server itself) ─
# Always (re-)write: /etc/cron.d is on the ephemeral container layer and is
# lost on container recreation, so we must restore it on every start.

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: