1499 lines
67 KiB
Bash
Executable File
1499 lines
67 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ansipa-enforce-policies.sh — enforce FreeIPA host-group-driven policies on this client.
|
||
#
|
||
# Policies are idempotent and reversible: joining a group applies the policy;
|
||
# leaving the group removes it on the next run (every 30 min via systemd timer).
|
||
#
|
||
# Host-group naming conventions (device policies — applied to the whole machine):
|
||
# dev_daemon-enable-<unit> Ensure <unit> is enabled and running (systemctl enable --now).
|
||
# Leaving the group reverts: service is disabled and stopped.
|
||
# dev_daemon-disable-<unit> Ensure <unit> is disabled and stopped (systemctl disable --now).
|
||
# Leaving the group reverts: service is re-enabled and started.
|
||
# <unit> may omit the .service suffix; all systemd unit types work.
|
||
# If a unit appears in both enable and disable groups it is skipped.
|
||
# dev_timeshift-backup Enforce a daily Timeshift snapshot (requires timeshift installed)
|
||
# dev_security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans
|
||
# dev_no-local-users Lock passwords for root and all local users (UID >= 1000) so
|
||
# that only FreeIPA domain accounts with centrally-managed sudo
|
||
# rules can authenticate and gain elevated privileges.
|
||
# Leaving the group reverts: every account locked by this policy
|
||
# is unlocked on the next run.
|
||
# dev_local-sudo-<username> Grant <username> full sudo on this specific device by adding
|
||
# them to the local sudoers drop-in. Leaving the group removes
|
||
# the drop-in on the next run.
|
||
#
|
||
# User-group naming conventions (per-user policies — follow the user across devices):
|
||
# usr_admin Members get full sudo on every enrolled host via a sudoers
|
||
# drop-in (/etc/sudoers.d/ansipa-usr-admin). Applied while the
|
||
# FreeIPA user group exists; drop-in is removed when the group is
|
||
# deleted. Uses SSSD so group membership resolves without local
|
||
# accounts.
|
||
# usr_block-binary-<name> Prevent members of this FreeIPA user group from running <name>
|
||
# on any enrolled host. Use __ in place of . to support Flatpak
|
||
# app IDs (e.g. usr_block-binary-org__gimp__Gimp blocks the
|
||
# Flatpak org.gimp.Gimp). Enforced via a PATH-priority wrapper
|
||
# that checks group membership at runtime via SSSD/id(1).
|
||
# Removing the user group from FreeIPA reverts the wrapper.
|
||
# usr_prt_<printer> Auto-add <printer> to CUPS on any enrolled host where a member
|
||
# is logged in. The printer URI is stored in the IPA group
|
||
# description (e.g. ipps://host/printers/name). Access is
|
||
# per-user only (allow:<username>); each new member who logs in
|
||
# gets added to the allow list. When the IPA group is deleted
|
||
# or all members leave, the printer is removed from CUPS on that
|
||
# host. A profile.d snippet + NOPASSWD sudo rule trigger the
|
||
# enforcer at login for immediate setup.
|
||
# usr_scan-notify Members receive scan alert notifications on every enrolled device
|
||
# they log into. The fetch-alerts timer is installed fleet-wide
|
||
# when the group exists; the notification daemon starts on login
|
||
# only for group members (checked via id(1) / SSSD).
|
||
# Deleting the IPA user group removes the timer and profile.d
|
||
# snippet on the next enforcer run.
|
||
#
|
||
# Notes:
|
||
# - Install scan tools first: add the host to dev_mod_anti-malware.
|
||
# - Configure Timeshift (type + target device) before enabling dev_timeshift-backup.
|
||
|
||
set -euo pipefail
|
||
|
||
LOG_TAG="ansipa-policies"
|
||
STATE_DIR="/var/lib/ansipa-policies"
|
||
BLOCK_DIR="/usr/local/bin"
|
||
CRON_DIR="/etc/cron.d"
|
||
|
||
log() { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; }
|
||
warn() { echo "[$LOG_TAG][WARN] $*" >&2; logger -t "$LOG_TAG" "WARN: $*" 2>/dev/null || true; }
|
||
|
||
HOST_FQDN=$(hostname -f 2>/dev/null || hostname)
|
||
|
||
if ! command -v ipa &>/dev/null; then
|
||
warn "ipa command not found — host not enrolled in FreeIPA. Exiting."
|
||
exit 0
|
||
fi
|
||
|
||
kinit -k "host/$HOST_FQDN" &>/dev/null || true
|
||
mkdir -p "$STATE_DIR"
|
||
|
||
# ── Fetch host group membership ───────────────────────────────────────────────
|
||
RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \
|
||
| grep -i "Member of host-groups:" | sed 's/.*: //' || true)
|
||
|
||
# ── Parse active host-group (device) policies ─────────────────────────────────
|
||
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
|
||
WANT_NO_LOCAL_USERS=false
|
||
WANT_USR_ADMIN=false
|
||
ACTIVE_PRT_GROUPS=()
|
||
ACTIVE_PRT_PRINTERS=()
|
||
|
||
if [[ -n "$RAW_GROUPS" ]]; then
|
||
while IFS=',' read -ra GRP_ARRAY; do
|
||
for g in "${GRP_ARRAY[@]}"; do
|
||
g="${g// /}"
|
||
case "$g" in
|
||
dev_daemon-enable-*) ACTIVE_DAEMON_ENABLE+=("${g#dev_daemon-enable-}") ;;
|
||
dev_daemon-disable-*) ACTIVE_DAEMON_DISABLE+=("${g#dev_daemon-disable-}") ;;
|
||
dev_timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;;
|
||
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"
|
||
fi
|
||
|
||
# ── Fetch user-group-based binary block policies from FreeIPA ─────────────────
|
||
# usr_block-binary-<name> groups are FreeIPA *user* groups — membership follows
|
||
# the user to every enrolled host rather than being tied to a device.
|
||
ACTIVE_BLOCK_BINARIES=()
|
||
ACTIVE_BLOCK_IPA_GROUPS=()
|
||
|
||
_BLOCK_LIST=$(ipa group-find --pkey-only 2>/dev/null \
|
||
| awk '/Group name:/ {print $NF}' \
|
||
| grep "^usr_block-binary-" | sort -u || true)
|
||
|
||
while IFS= read -r _grp; do
|
||
[[ -z "$_grp" ]] && continue
|
||
_raw="${_grp#usr_block-binary-}"
|
||
ACTIVE_BLOCK_BINARIES+=("${_raw//__/.}")
|
||
ACTIVE_BLOCK_IPA_GROUPS+=("$_grp")
|
||
done <<< "$_BLOCK_LIST"
|
||
unset _BLOCK_LIST _grp _raw
|
||
|
||
# ── Fetch user-group-based admin policy from FreeIPA ─────────────────────────
|
||
# usr_admin is a FreeIPA *user* group. When it exists, a sudoers drop-in grants
|
||
# full sudo to the group on every enrolled host (resolved via SSSD/NSS).
|
||
if ipa group-show usr_admin >/dev/null 2>&1; then
|
||
WANT_USR_ADMIN=true
|
||
fi
|
||
|
||
# ── Fetch user-group-based printer policies from FreeIPA ─────────────────────
|
||
# usr_prt_<printer> groups grant CUPS access to <printer> for members.
|
||
_PRT_LIST=$(ipa group-find --pkey-only 2>/dev/null \
|
||
| awk '/Group name:/ {print $NF}' \
|
||
| grep "^usr_prt_" | sort -u || true)
|
||
|
||
while IFS= read -r _pgrp; do
|
||
[[ -z "$_pgrp" ]] && continue
|
||
ACTIVE_PRT_GROUPS+=("$_pgrp")
|
||
ACTIVE_PRT_PRINTERS+=("${_pgrp#usr_prt_}")
|
||
done <<< "$_PRT_LIST"
|
||
unset _PRT_LIST _pgrp
|
||
|
||
# ── Fetch user-group-based scan-notify policy from FreeIPA ────────────────────
|
||
# usr_scan-notify is a FreeIPA *user* group — membership follows the user to
|
||
# every enrolled host. Install the fetch-alerts timer on any device where the
|
||
# group exists; the profile.d snippet gates daemon start on runtime membership.
|
||
if ipa group-show usr_scan-notify >/dev/null 2>&1; then
|
||
WANT_SCAN_NOTIFY=true
|
||
fi
|
||
|
||
# ── Fetch usr_mon_* user-group monitoring policies from FreeIPA ───────────────
|
||
# usr_mon_<type> are FreeIPA *user* groups; when the group exists the monitoring
|
||
# check is installed fleet-wide (same logic as usr_scan-notify).
|
||
ACTIVE_MON_USR=()
|
||
_MON_USR_LIST=$(ipa group-find --pkey-only 2>/dev/null \
|
||
| awk '/Group name:/ {print $NF}' \
|
||
| grep "^usr_mon_" | sort -u || true)
|
||
while IFS= read -r _mgrp; do
|
||
[[ -z "$_mgrp" ]] && continue
|
||
ACTIVE_MON_USR+=("${_mgrp#usr_mon_}")
|
||
done <<< "$_MON_USR_LIST"
|
||
unset _MON_USR_LIST _mgrp
|
||
|
||
log "Device policies — daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none}" \
|
||
"| daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \
|
||
"| timeshift-backup: $WANT_TIMESHIFT_BACKUP | security-scan: $WANT_SECURITY_SCAN" \
|
||
"| no-local-users: $WANT_NO_LOCAL_USERS | local-sudo: ${ACTIVE_LOCAL_SUDO_USERS[*]:-none}" \
|
||
"| ssh-keys: ${ACTIVE_SSH_USERS[*]:-none} | 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 | mon-usr: ${ACTIVE_MON_USR[*]:-none}"
|
||
|
||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||
_in_block_list() {
|
||
local needle="$1"
|
||
for b in "${ACTIVE_BLOCK_BINARIES[@]}"; do
|
||
[[ "$b" == "$needle" ]] && return 0
|
||
done
|
||
return 1
|
||
}
|
||
|
||
# ── Binary blocking (user-based) ──────────────────────────────────────────────
|
||
# A PATH-priority wrapper is installed in /usr/local/bin/ for every binary named
|
||
# by a usr_block-binary-* FreeIPA *user* group. The wrapper checks the
|
||
# caller's group membership at runtime (via id + SSSD) and only blocks members;
|
||
# non-members are transparently passed through to the real binary.
|
||
# __ in the group suffix decodes to . so Flatpak app IDs are fully supported.
|
||
# Removing the IPA user group causes the wrapper to be cleaned up on the next run.
|
||
|
||
BLOCK_STATE="$STATE_DIR/blocked-binaries"
|
||
[[ -f "$BLOCK_STATE" ]] || touch "$BLOCK_STATE"
|
||
|
||
for _idx in "${!ACTIVE_BLOCK_BINARIES[@]}"; do
|
||
BIN="${ACTIVE_BLOCK_BINARIES[$_idx]}"
|
||
IPA_GRP="${ACTIVE_BLOCK_IPA_GROUPS[$_idx]}"
|
||
WRAPPER="$BLOCK_DIR/$BIN"
|
||
# Write (or refresh) the wrapper when it is absent, not ours, or the group name changed.
|
||
if [[ ! -f "$WRAPPER" ]] \
|
||
|| ! grep -q "blocked by ansipa policy" "$WRAPPER" 2>/dev/null \
|
||
|| ! grep -qF "$IPA_GRP" "$WRAPPER" 2>/dev/null; then
|
||
log "Installing user-aware block wrapper: $BIN (group: $IPA_GRP)"
|
||
cat > "$WRAPPER" <<WRAPPER
|
||
#!/bin/bash
|
||
# blocked by ansipa policy (user-based)
|
||
if id -Gn 2>/dev/null | tr ' ' '\n' | grep -qxF "${IPA_GRP}"; then
|
||
echo "[ansipa-policies] '${BIN}' is blocked by system policy for your account." >&2
|
||
exit 1
|
||
fi
|
||
_real=\$(PATH="/usr/bin:/usr/sbin:/bin:/sbin:/usr/local/sbin:/opt/bin:/var/lib/flatpak/exports/bin:/usr/share/flatpak/exports/bin" command -v "${BIN}" 2>/dev/null)
|
||
[[ -n "\$_real" ]] && exec "\$_real" "\$@"
|
||
command -v flatpak &>/dev/null && exec flatpak run "${BIN}" "\$@" 2>/dev/null
|
||
echo "${BIN}: command not found" >&2
|
||
exit 127
|
||
WRAPPER
|
||
chmod 755 "$WRAPPER"
|
||
fi
|
||
done
|
||
unset _idx
|
||
|
||
# Remove wrappers whose IPA user group no longer exists.
|
||
while IFS= read -r OLD_BIN; do
|
||
[[ -z "$OLD_BIN" ]] && continue
|
||
if ! _in_block_list "$OLD_BIN"; then
|
||
WRAPPER="$BLOCK_DIR/$OLD_BIN"
|
||
if [[ -f "$WRAPPER" ]] && grep -q "blocked by ansipa policy" "$WRAPPER" 2>/dev/null; then
|
||
rm -f "$WRAPPER"
|
||
log "Removed binary block wrapper: $OLD_BIN"
|
||
fi
|
||
fi
|
||
done < "$BLOCK_STATE"
|
||
|
||
# Persist current blocked list.
|
||
if [[ ${#ACTIVE_BLOCK_BINARIES[@]} -gt 0 ]]; then
|
||
printf '%s\n' "${ACTIVE_BLOCK_BINARIES[@]}" | sort -u > "$BLOCK_STATE"
|
||
else
|
||
> "$BLOCK_STATE"
|
||
fi
|
||
|
||
# ── Timeshift daily backup ─────────────────────────────────────────────────────
|
||
TIMESHIFT_CRON="$CRON_DIR/ansipa-timeshift-backup"
|
||
|
||
if [[ "$WANT_TIMESHIFT_BACKUP" == true ]]; then
|
||
if [[ ! -f "$TIMESHIFT_CRON" ]]; then
|
||
if ! command -v timeshift &>/dev/null; then
|
||
warn "timeshift not found — add host to dev_mod_timeshift first. Cron will be installed anyway."
|
||
fi
|
||
log "Enabling daily Timeshift backups"
|
||
cat > "$TIMESHIFT_CRON" <<'CRON'
|
||
# ansipa-dev_timeshift-backup: managed by ansipa-enforce-policies — do not edit manually.
|
||
# Timeshift must be configured on this host (type + target device) before snapshots work.
|
||
0 3 * * * root /usr/bin/timeshift --create --comments "ansipa-daily" --tags D 2>&1 | logger -t timeshift-backup
|
||
CRON
|
||
chmod 644 "$TIMESHIFT_CRON"
|
||
fi
|
||
else
|
||
if [[ -f "$TIMESHIFT_CRON" ]]; then
|
||
rm -f "$TIMESHIFT_CRON"
|
||
log "Removed Timeshift backup cron (host left dev_timeshift-backup group)"
|
||
fi
|
||
fi
|
||
|
||
# ── Security scan ─────────────────────────────────────────────────────────────
|
||
SCAN_CRON="$CRON_DIR/ansipa-security-scan"
|
||
SCAN_SCRIPT="/usr/local/bin/ansipa-security-scan.sh"
|
||
|
||
if [[ "$WANT_SECURITY_SCAN" == true ]]; then
|
||
# (Re-)write the scan script so it stays current with this version of the enforcer.
|
||
cat > "$SCAN_SCRIPT" <<'SCAN'
|
||
#!/bin/bash
|
||
# ansipa-security-scan — daily ClamAV / rkhunter / chkrootkit run + SMB upload.
|
||
# Managed by ansipa-enforce-policies — do not edit manually.
|
||
LOG=/var/log/ansipa-security-scan.log
|
||
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
|
||
DATE=$(date +%Y-%m-%d)
|
||
{
|
||
echo "=== ansipa-security-scan: $DATE $HOSTNAME ==="
|
||
|
||
if command -v freshclam &>/dev/null; then
|
||
freshclam --quiet 2>/dev/null || true
|
||
fi
|
||
if command -v clamscan &>/dev/null; then
|
||
clamscan -r --infected --quiet /home /etc /tmp /var/tmp 2>/dev/null || true
|
||
fi
|
||
if command -v rkhunter &>/dev/null; then
|
||
rkhunter --update --quiet 2>/dev/null || true
|
||
rkhunter --check --skip-keypress --quiet 2>/dev/null || true
|
||
fi
|
||
if command -v chkrootkit &>/dev/null; then
|
||
chkrootkit 2>/dev/null || true
|
||
fi
|
||
|
||
echo "=== scan complete ==="
|
||
} >> "$LOG" 2>&1
|
||
|
||
# ── Upload to server SMB share ────────────────────────────────────────────────
|
||
IPA_SERVER=$(awk '/^server[[:space:]]*=/{print $3}' /etc/ipa/default.conf 2>/dev/null || echo "")
|
||
if [[ -n "$IPA_SERVER" ]] && [[ -f /etc/ansipa-smb.creds ]] && command -v smbclient &>/dev/null; then
|
||
# Create host archive dir (mkdir is idempotent; errors suppressed).
|
||
smbclient "//$IPA_SERVER/ansipa-scans" -A /etc/ansipa-smb.creds \
|
||
-c "mkdir archive; mkdir archive\\$HOSTNAME; put $LOG archive\\$HOSTNAME\\$DATE.log" \
|
||
>> "$LOG" 2>&1 \
|
||
&& echo "[ansipa] Scan results uploaded to $IPA_SERVER/ansipa-scans/archive/$HOSTNAME/$DATE.log" >> "$LOG" \
|
||
|| echo "[ansipa][WARN] SMB upload failed — results remain local at $LOG" >> "$LOG"
|
||
else
|
||
echo "[ansipa] SMB upload skipped (no credentials or smbclient not found)." >> "$LOG"
|
||
fi
|
||
SCAN
|
||
chmod 755 "$SCAN_SCRIPT"
|
||
|
||
if [[ ! -f "$SCAN_CRON" ]]; then
|
||
log "Enabling daily security scans (ClamAV / rkhunter / chkrootkit)"
|
||
cat > "$SCAN_CRON" <<'CRON'
|
||
# ansipa-dev_security-scan: managed by ansipa-enforce-policies — do not edit manually.
|
||
# Install scan tools by adding the host to the dev_mod_anti-malware group.
|
||
0 2 * * * root /usr/local/bin/ansipa-security-scan.sh
|
||
CRON
|
||
chmod 644 "$SCAN_CRON"
|
||
fi
|
||
else
|
||
if [[ -f "$SCAN_CRON" ]]; then
|
||
rm -f "$SCAN_CRON"
|
||
rm -f "$SCAN_SCRIPT"
|
||
log "Removed security scan policy (host left dev_security-scan group)"
|
||
fi
|
||
fi
|
||
|
||
# ── Scan notification daemon ──────────────────────────────────────────────────
|
||
# usr_scan-notify is a FreeIPA *user* group (not a host group). The fetch-
|
||
# alerts timer runs fleet-wide on any host where the group exists; the profile.d
|
||
# snippet starts the notification daemon on login only for group members
|
||
# (checked via id(1) / SSSD so no IPA query is needed at login time).
|
||
# - Root timer (every 10 min): ansipa-fetch-alerts.sh downloads alerts from the
|
||
# server SMB share and places them in ~/administration/<hostname>/ per active user.
|
||
# - profile.d snippet: starts ansipa-scan-notify.sh as a user daemon on login;
|
||
# the daemon sends notify-send every 10 min while *.alert files remain.
|
||
# Deleting a file from ~/administration/ counts as acknowledgment.
|
||
#
|
||
# Requires: ansipa-fetch-alerts.sh and ansipa-scan-notify.sh deployed by
|
||
# deploy-ansipa-policies.yml (static scripts — not written inline here).
|
||
|
||
FETCH_SVC="/etc/systemd/system/ansipa-fetch-alerts.service"
|
||
FETCH_TIMER="/etc/systemd/system/ansipa-fetch-alerts.timer"
|
||
NOTIFY_PROFILED="/etc/profile.d/ansipa-notify.sh"
|
||
|
||
if [[ "$WANT_SCAN_NOTIFY" == true ]]; then
|
||
if [[ ! -x /usr/local/bin/ansipa-fetch-alerts.sh ]]; then
|
||
warn "ansipa-fetch-alerts.sh not found — run deploy-ansipa-policies.yml first."
|
||
fi
|
||
|
||
if [[ ! -f "$FETCH_SVC" ]]; then
|
||
log "Installing ansipa-fetch-alerts systemd service + timer"
|
||
cat > "$FETCH_SVC" <<'UNIT'
|
||
[Unit]
|
||
Description=Fetch Ansipa security alerts from the server SMB share
|
||
After=network-online.target sssd.service
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=/usr/local/bin/ansipa-fetch-alerts.sh
|
||
StandardOutput=journal
|
||
StandardError=journal
|
||
UNIT
|
||
|
||
cat > "$FETCH_TIMER" <<'UNIT'
|
||
[Unit]
|
||
Description=Periodic ansipa security alert fetch
|
||
|
||
[Timer]
|
||
OnBootSec=2min
|
||
OnUnitActiveSec=10min
|
||
|
||
[Install]
|
||
WantedBy=timers.target
|
||
UNIT
|
||
systemctl daemon-reload
|
||
systemctl enable --now ansipa-fetch-alerts.timer
|
||
log "ansipa-fetch-alerts.timer enabled"
|
||
fi
|
||
|
||
if [[ ! -f "$NOTIFY_PROFILED" ]]; then
|
||
log "Installing /etc/profile.d/ansipa-notify.sh"
|
||
cat > "$NOTIFY_PROFILED" <<'PROFILED'
|
||
# ansipa-notify: launch the scan alert notification daemon on login for
|
||
# members of the usr_scan-notify FreeIPA user group.
|
||
# Managed by ansipa-enforce-policies — do not edit manually.
|
||
_NOTIFY_DAEMON=/usr/local/bin/ansipa-scan-notify.sh
|
||
if [[ -x "$_NOTIFY_DAEMON" ]] && \
|
||
id -nG 2>/dev/null | grep -qw "usr_scan-notify" && \
|
||
! pgrep -u "$(id -u)" -f "ansipa-scan-notify" >/dev/null 2>&1; then
|
||
"$_NOTIFY_DAEMON" &
|
||
disown
|
||
fi
|
||
unset _NOTIFY_DAEMON
|
||
PROFILED
|
||
chmod 644 "$NOTIFY_PROFILED"
|
||
fi
|
||
else
|
||
if [[ -f "$FETCH_TIMER" ]]; then
|
||
systemctl disable --now ansipa-fetch-alerts.timer 2>/dev/null || true
|
||
rm -f "$FETCH_SVC" "$FETCH_TIMER"
|
||
systemctl daemon-reload
|
||
log "Removed ansipa-fetch-alerts timer (usr_scan-notify user group no longer exists)"
|
||
fi
|
||
if [[ -f "$NOTIFY_PROFILED" ]]; then
|
||
rm -f "$NOTIFY_PROFILED"
|
||
log "Removed /etc/profile.d/ansipa-notify.sh"
|
||
fi
|
||
fi
|
||
|
||
# ── Daemon enable / disable ───────────────────────────────────────────────────
|
||
# dev_daemon-enable-<unit>: ensure the unit is enabled and running.
|
||
# Leaving the group reverts: unit is disabled and stopped.
|
||
# dev_daemon-disable-<unit>: ensure the unit is disabled and stopped.
|
||
# Leaving the group reverts: unit is re-enabled and started.
|
||
# <unit> may omit the .service suffix; systemd accepts both forms.
|
||
# Conflicts (unit in both lists): logged as a warning, unit is left untouched.
|
||
|
||
DAEMON_ENABLE_STATE="$STATE_DIR/daemon-enabled"
|
||
DAEMON_DISABLE_STATE="$STATE_DIR/daemon-disabled"
|
||
[[ -f "$DAEMON_ENABLE_STATE" ]] || touch "$DAEMON_ENABLE_STATE"
|
||
[[ -f "$DAEMON_DISABLE_STATE" ]] || touch "$DAEMON_DISABLE_STATE"
|
||
|
||
# Append .service only when the name has no unit-type suffix already.
|
||
_svc_unit() { [[ "$1" == *.* ]] && echo "$1" || echo "${1}.service"; }
|
||
|
||
_in_enable_list() { local n="$1"; for s in "${ACTIVE_DAEMON_ENABLE[@]}"; do [[ "$s" == "$n" ]] && return 0; done; return 1; }
|
||
_in_disable_list() { local n="$1"; for s in "${ACTIVE_DAEMON_DISABLE[@]}"; do [[ "$s" == "$n" ]] && return 0; done; return 1; }
|
||
|
||
# Apply enable policies
|
||
for _SVC in "${ACTIVE_DAEMON_ENABLE[@]}"; do
|
||
if _in_disable_list "$_SVC"; then
|
||
warn "Conflict: '$_SVC' is in both daemon-enable and daemon-disable groups — skipped"
|
||
continue
|
||
fi
|
||
_UNIT=$(_svc_unit "$_SVC")
|
||
_EN=$(systemctl is-enabled "$_UNIT" 2>/dev/null || echo "not-found")
|
||
_AC=$(systemctl is-active "$_UNIT" 2>/dev/null || echo "inactive")
|
||
if [[ "$_EN" != "enabled" || "$_AC" != "active" ]]; then
|
||
log "Enabling service: $_UNIT (enabled=$_EN active=$_AC)"
|
||
systemctl enable --now "$_UNIT" 2>/dev/null \
|
||
&& log "Service enabled: $_UNIT" \
|
||
|| warn "Failed to enable $_UNIT — unit may not exist on this host"
|
||
fi
|
||
done
|
||
|
||
# Apply disable policies
|
||
for _SVC in "${ACTIVE_DAEMON_DISABLE[@]}"; do
|
||
if _in_enable_list "$_SVC"; then
|
||
continue # conflict already warned above
|
||
fi
|
||
_UNIT=$(_svc_unit "$_SVC")
|
||
_EN=$(systemctl is-enabled "$_UNIT" 2>/dev/null || echo "not-found")
|
||
_AC=$(systemctl is-active "$_UNIT" 2>/dev/null || echo "inactive")
|
||
if [[ "$_EN" == "enabled" || "$_AC" == "active" ]]; then
|
||
log "Disabling service: $_UNIT (enabled=$_EN active=$_AC)"
|
||
systemctl disable --now "$_UNIT" 2>/dev/null \
|
||
&& log "Service disabled: $_UNIT" \
|
||
|| warn "Failed to disable $_UNIT — unit may not exist on this host"
|
||
fi
|
||
done
|
||
|
||
# Revert: host left a daemon-enable group → disable and stop the service
|
||
while IFS= read -r _OLD; do
|
||
[[ -z "$_OLD" ]] && continue
|
||
if ! _in_enable_list "$_OLD"; then
|
||
_UNIT=$(_svc_unit "$_OLD")
|
||
log "Reverting enable policy: disabling $_UNIT (host left daemon-enable group)"
|
||
systemctl disable --now "$_UNIT" 2>/dev/null \
|
||
|| warn "Failed to disable (revert) $_UNIT"
|
||
fi
|
||
done < "$DAEMON_ENABLE_STATE"
|
||
|
||
# Revert: host left a daemon-disable group → re-enable and start the service
|
||
while IFS= read -r _OLD; do
|
||
[[ -z "$_OLD" ]] && continue
|
||
if ! _in_disable_list "$_OLD"; then
|
||
_UNIT=$(_svc_unit "$_OLD")
|
||
log "Reverting disable policy: enabling $_UNIT (host left daemon-disable group)"
|
||
systemctl enable --now "$_UNIT" 2>/dev/null \
|
||
|| warn "Failed to enable (revert) $_UNIT"
|
||
fi
|
||
done < "$DAEMON_DISABLE_STATE"
|
||
|
||
# Persist current state
|
||
if [[ ${#ACTIVE_DAEMON_ENABLE[@]} -gt 0 ]]; then
|
||
printf '%s\n' "${ACTIVE_DAEMON_ENABLE[@]}" | sort -u > "$DAEMON_ENABLE_STATE"
|
||
else
|
||
> "$DAEMON_ENABLE_STATE"
|
||
fi
|
||
if [[ ${#ACTIVE_DAEMON_DISABLE[@]} -gt 0 ]]; then
|
||
printf '%s\n' "${ACTIVE_DAEMON_DISABLE[@]}" | sort -u > "$DAEMON_DISABLE_STATE"
|
||
else
|
||
> "$DAEMON_DISABLE_STATE"
|
||
fi
|
||
|
||
# ── Per-device local sudo grants ──────────────────────────────────────────────
|
||
# dev_local-sudo-<username>: write a sudoers drop-in granting <username> full sudo on
|
||
# this specific device. The drop-in is removed when the host leaves the group.
|
||
|
||
LOCAL_SUDO_DIR="/etc/sudoers.d"
|
||
LOCAL_SUDO_STATE="$STATE_DIR/local-sudo-users"
|
||
[[ -f "$LOCAL_SUDO_STATE" ]] || touch "$LOCAL_SUDO_STATE"
|
||
|
||
for _USER in "${ACTIVE_LOCAL_SUDO_USERS[@]}"; do
|
||
_DROPIN="$LOCAL_SUDO_DIR/ansipa-local-sudo-${_USER}"
|
||
if [[ ! -f "$_DROPIN" ]]; then
|
||
log "Granting local sudo to $_USER on this device"
|
||
echo "$_USER ALL=(ALL) ALL" > "$_DROPIN"
|
||
chmod 440 "$_DROPIN"
|
||
fi
|
||
grep -qxF "$_USER" "$LOCAL_SUDO_STATE" 2>/dev/null || echo "$_USER" >> "$LOCAL_SUDO_STATE"
|
||
done
|
||
|
||
# Revoke sudo for users no longer in any active dev_local-sudo-* group.
|
||
while IFS= read -r _OLD_USER; do
|
||
[[ -z "$_OLD_USER" ]] && continue
|
||
_still_active=false
|
||
for _U in "${ACTIVE_LOCAL_SUDO_USERS[@]}"; do
|
||
[[ "$_U" == "$_OLD_USER" ]] && _still_active=true && break
|
||
done
|
||
if [[ "$_still_active" == false ]]; then
|
||
_DROPIN="$LOCAL_SUDO_DIR/ansipa-local-sudo-${_OLD_USER}"
|
||
if [[ -f "$_DROPIN" ]]; then
|
||
rm -f "$_DROPIN"
|
||
log "Revoked local sudo for $_OLD_USER (host left dev_local-sudo-$_OLD_USER group)"
|
||
fi
|
||
fi
|
||
done < "$LOCAL_SUDO_STATE"
|
||
|
||
# Persist current local sudo users.
|
||
if [[ ${#ACTIVE_LOCAL_SUDO_USERS[@]} -gt 0 ]]; then
|
||
printf '%s\n' "${ACTIVE_LOCAL_SUDO_USERS[@]}" | sort -u > "$LOCAL_SUDO_STATE"
|
||
else
|
||
> "$LOCAL_SUDO_STATE"
|
||
fi
|
||
unset _USER _DROPIN _OLD_USER _still_active _U
|
||
|
||
# ── No-local-users policy ──────────────────────────────────────────────────────
|
||
# dev_no-local-users: lock the passwords of root and all local users (UID >= 1000)
|
||
# so that only FreeIPA domain accounts with centrally-managed sudo rules can
|
||
# authenticate and gain elevated privileges.
|
||
# Leaving the group reverts: every account locked by this policy is unlocked.
|
||
|
||
NO_LOCAL_USERS_STATE="$STATE_DIR/no-local-users"
|
||
|
||
_apply_no_local_users() {
|
||
log "Applying no_local_users policy — locking local account passwords"
|
||
[[ -f "$NO_LOCAL_USERS_STATE" ]] || touch "$NO_LOCAL_USERS_STATE"
|
||
|
||
while IFS=: read -r uname _ uid _; do
|
||
[[ "$uid" =~ ^[0-9]+$ ]] || continue
|
||
{ [[ "$uid" == "0" ]] || [[ "$uid" -ge 1000 ]]; } || continue
|
||
|
||
# Skip accounts already tracked (locked on a previous run)
|
||
grep -qxF "$uname" "$NO_LOCAL_USERS_STATE" 2>/dev/null && continue
|
||
|
||
# Lock only accounts that currently have a real (unlocked) password hash
|
||
local hash
|
||
hash=$(getent shadow "$uname" 2>/dev/null | cut -d: -f2 || true)
|
||
[[ -z "$hash" || "$hash" == '!'* || "$hash" == '*'* ]] && continue
|
||
|
||
if passwd -l "$uname" &>/dev/null; then
|
||
echo "$uname" >> "$NO_LOCAL_USERS_STATE"
|
||
log "Locked local account: $uname"
|
||
else
|
||
warn "Failed to lock local account: $uname"
|
||
fi
|
||
done < /etc/passwd
|
||
}
|
||
|
||
_revert_no_local_users() {
|
||
[[ -f "$NO_LOCAL_USERS_STATE" ]] || return 0
|
||
log "Reverting no_local_users policy — unlocking previously locked accounts"
|
||
while IFS= read -r uname; do
|
||
[[ -z "$uname" ]] && continue
|
||
if passwd -u "$uname" &>/dev/null; then
|
||
log "Unlocked local account: $uname"
|
||
else
|
||
warn "Failed to unlock local account: $uname (may have been removed)"
|
||
fi
|
||
done < "$NO_LOCAL_USERS_STATE"
|
||
> "$NO_LOCAL_USERS_STATE"
|
||
}
|
||
|
||
if [[ "$WANT_NO_LOCAL_USERS" == true ]]; then
|
||
_apply_no_local_users
|
||
else
|
||
if [[ -f "$NO_LOCAL_USERS_STATE" ]] && [[ -s "$NO_LOCAL_USERS_STATE" ]]; then
|
||
_revert_no_local_users
|
||
fi
|
||
fi
|
||
|
||
# ── usr_admin: global sudo for FreeIPA admin group ───────────────────────────
|
||
# When the FreeIPA user group usr_admin exists, install a sudoers drop-in that
|
||
# grants full sudo to that group (resolved via SSSD/NSS) on this host.
|
||
# The drop-in is removed when the group is deleted from FreeIPA.
|
||
|
||
USR_ADMIN_DROPIN="/etc/sudoers.d/ansipa-usr-admin"
|
||
USR_ADMIN_STATE="$STATE_DIR/usr-admin"
|
||
|
||
if [[ "$WANT_USR_ADMIN" == true ]]; then
|
||
if [[ ! -f "$USR_ADMIN_DROPIN" ]]; then
|
||
log "Installing usr_admin sudoers drop-in"
|
||
printf '%%usr_admin ALL=(ALL:ALL) ALL\n' > "$USR_ADMIN_DROPIN"
|
||
chmod 440 "$USR_ADMIN_DROPIN"
|
||
fi
|
||
touch "$USR_ADMIN_STATE"
|
||
else
|
||
if [[ -f "$USR_ADMIN_STATE" ]] || [[ -f "$USR_ADMIN_DROPIN" ]]; then
|
||
rm -f "$USR_ADMIN_DROPIN" "$USR_ADMIN_STATE"
|
||
log "Removed usr_admin sudoers drop-in (IPA group no longer exists)"
|
||
fi
|
||
fi
|
||
|
||
# ── usr_prt_<printer>: per-user printer auto-add via FreeIPA groups ───────────
|
||
# For each usr_prt_<printer> group in FreeIPA:
|
||
# - The group description must contain the printer URI
|
||
# (e.g. "ipps://printserver.corp.example.com/printers/hp-office")
|
||
# - When a logged-in user is a member of the group, the printer is added to
|
||
# CUPS on this host and restricted to that user (allow:<user>).
|
||
# - Multiple users in the group can each gain access on the same host.
|
||
# - When a user leaves the group their allow entry is revoked; when no users
|
||
# remain the printer is removed from CUPS on this host entirely.
|
||
# The profile.d snippet /etc/profile.d/ansipa-printers.sh triggers this enforcer
|
||
# at login so printers appear immediately rather than waiting for the 30-min tick.
|
||
#
|
||
# To register a printer in FreeIPA:
|
||
# ipa group-mod usr_prt_hp-office \
|
||
# --desc='ipps://printserver.corp.example.com/printers/hp-office'
|
||
|
||
PRT_STATE_DIR="$STATE_DIR/printer-users"
|
||
PRT_DEFS_DIR="$STATE_DIR/printer-defs"
|
||
PRT_PROFILED="/etc/profile.d/ansipa-printers.sh"
|
||
PRT_SUDO_DROPIN="/etc/sudoers.d/ansipa-printers-trigger"
|
||
mkdir -p "$PRT_STATE_DIR" "$PRT_DEFS_DIR"
|
||
|
||
# Read/write per-printer authorized-user state files (<printer>.users)
|
||
_prt_read_users() {
|
||
local sf="$PRT_STATE_DIR/${1}.users"
|
||
[[ -f "$sf" ]] && grep -v '^$' "$sf" | sort -u || true
|
||
}
|
||
_prt_write_users() {
|
||
local printer="$1"; shift
|
||
if [[ $# -gt 0 ]]; then
|
||
printf '%s\n' "$@" | sort -u > "$PRT_STATE_DIR/${printer}.users"
|
||
else
|
||
> "$PRT_STATE_DIR/${printer}.users"
|
||
fi
|
||
}
|
||
# Apply the full allow list from state (or remove printer if list is empty)
|
||
_prt_apply_acl() {
|
||
local printer="$1"
|
||
local -a users
|
||
mapfile -t users < <(_prt_read_users "$printer")
|
||
if [[ ${#users[@]} -gt 0 ]]; then
|
||
local allow_list
|
||
allow_list=$(IFS=','; echo "${users[*]}")
|
||
lpadmin -p "$printer" -u "allow:${allow_list}" 2>/dev/null \
|
||
|| warn "lpadmin ACL update failed for printer $printer"
|
||
else
|
||
lpadmin -x "$printer" 2>/dev/null \
|
||
&& log "Removed printer $printer (no authorized users remaining)" \
|
||
|| warn "lpadmin -x failed for printer $printer"
|
||
rm -f "$PRT_STATE_DIR/${printer}.users"
|
||
fi
|
||
}
|
||
_prt_is_active() {
|
||
local needle="$1"
|
||
for _ap in "${ACTIVE_PRT_PRINTERS[@]+"${ACTIVE_PRT_PRINTERS[@]}"}"; do
|
||
[[ "$_ap" == "$needle" ]] && return 0
|
||
done
|
||
return 1
|
||
}
|
||
|
||
if command -v lpadmin &>/dev/null; then
|
||
# ── Process each active usr_prt_* group ──────────────────────────────────
|
||
for _i in "${!ACTIVE_PRT_PRINTERS[@]}"; do
|
||
_PRT="${ACTIVE_PRT_PRINTERS[$_i]}"
|
||
_GRP="${ACTIVE_PRT_GROUPS[$_i]}"
|
||
|
||
# Fetch URI from IPA group description (must start with a scheme://)
|
||
_URI=$(ipa group-show "$_GRP" --all 2>/dev/null \
|
||
| awk -F': ' '/Description:/{print $2; exit}' \
|
||
| grep -E '^[a-z][a-z+.-]+://' || true)
|
||
|
||
if [[ -z "$_URI" ]]; then
|
||
warn "Group $_GRP has no printer URI in its description — set it with:"
|
||
warn " ipa group-mod $_GRP --desc='ipps://printserver/printers/$_PRT'"
|
||
continue
|
||
fi
|
||
echo "$_URI" > "$PRT_DEFS_DIR/${_GRP}.uri"
|
||
|
||
mapfile -t _CUR_USERS < <(_prt_read_users "$_PRT")
|
||
_UPDATED=false
|
||
|
||
# Check logged-in users for new members to grant access to
|
||
while IFS= read -r _LUSER; do
|
||
[[ -z "$_LUSER" ]] && continue
|
||
id -Gn "$_LUSER" 2>/dev/null | tr ' ' '\n' | grep -qxF "$_GRP" || continue
|
||
printf '%s\n' "${_CUR_USERS[@]+"${_CUR_USERS[@]}"}" | grep -qxF "$_LUSER" && continue
|
||
|
||
# Add the printer to CUPS if not present yet
|
||
if ! lpstat -p "$_PRT" &>/dev/null; then
|
||
log "Adding printer $_PRT (URI: $_URI)"
|
||
lpadmin -p "$_PRT" -v "$_URI" -m everywhere -E 2>/dev/null \
|
||
|| lpadmin -p "$_PRT" -v "$_URI" -E 2>/dev/null \
|
||
|| { warn "lpadmin failed to add printer $_PRT"; continue; }
|
||
fi
|
||
|
||
log "Granting printer $_PRT access to $_LUSER"
|
||
_CUR_USERS+=("$_LUSER")
|
||
_UPDATED=true
|
||
done < <(who -u 2>/dev/null | awk '{print $1}' | sort -u)
|
||
|
||
# Revoke users who left the group
|
||
_VALID_USERS=()
|
||
for _U in "${_CUR_USERS[@]+"${_CUR_USERS[@]}"}"; do
|
||
if id -Gn "$_U" 2>/dev/null | tr ' ' '\n' | grep -qxF "$_GRP"; then
|
||
_VALID_USERS+=("$_U")
|
||
else
|
||
log "Revoking printer $_PRT access for $_U (no longer in $_GRP)"
|
||
_UPDATED=true
|
||
fi
|
||
done
|
||
|
||
if [[ "$_UPDATED" == true ]]; then
|
||
_prt_write_users "$_PRT" "${_VALID_USERS[@]+"${_VALID_USERS[@]}"}"
|
||
_prt_apply_acl "$_PRT"
|
||
fi
|
||
done
|
||
unset _i _PRT _GRP _URI _LUSER _U _CUR_USERS _VALID_USERS _UPDATED _ap
|
||
|
||
# ── Remove printers whose IPA group was deleted ───────────────────────────
|
||
shopt -s nullglob
|
||
for _sf in "$PRT_STATE_DIR"/*.users; do
|
||
[[ -f "$_sf" ]] || continue
|
||
_PRT=$(basename "$_sf" .users)
|
||
_prt_is_active "$_PRT" && continue
|
||
log "Removing printer $_PRT (IPA group usr_prt_$_PRT deleted)"
|
||
lpadmin -x "$_PRT" 2>/dev/null || true
|
||
rm -f "$_sf"
|
||
done
|
||
shopt -u nullglob
|
||
unset _sf _PRT
|
||
|
||
# ── profile.d login trigger ───────────────────────────────────────────────
|
||
# Installs a profile.d snippet + NOPASSWD sudo rule so that when a user
|
||
# logs in, the enforcer runs immediately and adds their printers without
|
||
# waiting for the next 30-minute timer tick.
|
||
if [[ ${#ACTIVE_PRT_PRINTERS[@]} -gt 0 ]]; then
|
||
if [[ ! -f "$PRT_SUDO_DROPIN" ]]; then
|
||
log "Installing ansipa-printers sudo trigger rule"
|
||
printf 'ALL ALL=(root) NOPASSWD: /usr/bin/systemctl start ansipa-enforce-policies.service\n' \
|
||
> "$PRT_SUDO_DROPIN"
|
||
chmod 440 "$PRT_SUDO_DROPIN"
|
||
fi
|
||
if [[ ! -f "$PRT_PROFILED" ]]; then
|
||
log "Installing /etc/profile.d/ansipa-printers.sh"
|
||
cat > "$PRT_PROFILED" <<'PROFILED'
|
||
# ansipa-printers: trigger policy enforcer at login to auto-add printers.
|
||
# Managed by ansipa-enforce-policies — do not edit manually.
|
||
sudo -n /usr/bin/systemctl start ansipa-enforce-policies.service 2>/dev/null &
|
||
disown 2>/dev/null || true
|
||
PROFILED
|
||
chmod 644 "$PRT_PROFILED"
|
||
fi
|
||
else
|
||
if [[ -f "$PRT_SUDO_DROPIN" ]]; then
|
||
rm -f "$PRT_SUDO_DROPIN"
|
||
log "Removed ansipa-printers sudo trigger (no active printer groups)"
|
||
fi
|
||
if [[ -f "$PRT_PROFILED" ]]; then
|
||
rm -f "$PRT_PROFILED"
|
||
log "Removed /etc/profile.d/ansipa-printers.sh (no active printer groups)"
|
||
fi
|
||
fi
|
||
else
|
||
[[ ${#ACTIVE_PRT_PRINTERS[@]} -gt 0 ]] && \
|
||
warn "lpadmin not found — install cups to enforce printer policies"
|
||
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."
|