Compare commits
2 Commits
00ce4f5260
...
6bb19cea78
| Author | SHA1 | Date |
|---|---|---|
|
|
6bb19cea78 | |
|
|
76c3c2c1c5 |
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
# Test credentials — never commit
|
||||
.env
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue