From b21f415cded1d5fc3ba0b037bc01fee2b75d5dc2 Mon Sep 17 00:00:00 2001 From: The_miro Date: Thu, 2 Jul 2026 12:25:03 +0200 Subject: [PATCH] fix(ansipa): hostname-binary-free FQDN + push-mode orchestration (NAT path) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found while running the policy matrix and NAT/WAN scenario in VMs: - ansipa-enforce-policies.sh and the dev_security-scan generated script called `hostname -f`, but the hostname binary (inetutils) is not installed on a minimal Arch system, so the enforcer aborted with "hostname: command not found" on every run. Add an ansipa_fqdn() helper (hostnamectl / getent / /etc/hostname / kernel-hostname fallback chain) and use it; inline the same fallback in the standalone scan script. - Fix two stacked bugs that meant CheckMK push mode — the only monitoring transport that works when a client is behind NAT — never functioned: * ansipa-checkmk-setup.sh referenced IPA_ADMIN_PASS in the DONE_FLAG early-exit path, but that variable was assigned only afterwards, so every rerun kinit'd with an empty password, failed silently, and skipped the push-mode request writer. Resolve config before the early exit. * _write_push_requests parsed `ipa hostgroup-show dev_mon_base` members with awk that did `next` on the "Member hosts:" line — but ipa prints members on that same line, so the only line with hostnames was discarded and no push request was ever written. Rewrite the awk to capture the label line's value plus wrapped continuation lines. Verified end-to-end: requests are written and CheckMK flips the host to cmk_agent_connection=push-agent. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01MUhrcFU8J1Hnf7vNqNxZNi --- .../ansible/ansipa-enforce-policies.sh | 2 +- .../ansible/lib/ansipa-policy.sh | 22 ++++++++++ .../ansible/policies.d/dev_security-scan.sh | 5 ++- .../image/ansipa-checkmk-setup.sh | 40 ++++++++++++++----- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh index 41e515a..41c2fd1 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh @@ -103,7 +103,7 @@ set -euo pipefail # shellcheck source=lib/ansipa-policy.sh source /usr/local/lib/ansipa/policy.sh -HOST_FQDN=$(hostname -f 2>/dev/null || hostname) +HOST_FQDN=$(ansipa_fqdn) command -v ipa &>/dev/null || { warn "ipa not found — host not enrolled in FreeIPA. Exiting."; exit 0; } diff --git a/setup/modules/FreeipaAnsible/ansible/lib/ansipa-policy.sh b/setup/modules/FreeipaAnsible/ansible/lib/ansipa-policy.sh index fa19d20..3acd550 100644 --- a/setup/modules/FreeipaAnsible/ansible/lib/ansipa-policy.sh +++ b/setup/modules/FreeipaAnsible/ansible/lib/ansipa-policy.sh @@ -27,6 +27,28 @@ POLICY_DIR="/usr/local/lib/ansipa/policies.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; } +# ── Hostname resolution ─────────────────────────────────────────────────────── +# The `hostname` binary (from inetutils) is NOT installed on a minimal Arch +# system, so `hostname -f` aborts the enforcer with "command not found". Resolve +# the FQDN without depending on it: prefer hostnamectl, then the FQDN via getent +# on the static hostname, then /etc/hostname, then the kernel hostname. Every +# enrolled host has an FQDN (ipa-client-install sets it), so this resolves. +ansipa_fqdn() { + local h="" + if command -v hostname &>/dev/null; then + h=$(hostname -f 2>/dev/null) || h="" + fi + [[ -z "$h" ]] && h=$(hostnamectl --static 2>/dev/null || true) + [[ -z "$h" ]] && h=$(cat /etc/hostname 2>/dev/null || true) + [[ -z "$h" ]] && h=$(cat /proc/sys/kernel/hostname 2>/dev/null || true) + # Upgrade a short name to the FQDN via NSS if resolvable. + if [[ "$h" != *.* ]] && command -v getent &>/dev/null; then + local fqdn; fqdn=$(getent hosts "$h" 2>/dev/null | awk '{for(i=2;i<=NF;i++) if($i ~ /\./){print $i; exit}}') + [[ -n "$fqdn" ]] && h="$fqdn" + fi + printf '%s' "$h" +} + # ── Group discovery ─────────────────────────────────────────────────────────── # Called once by the orchestrator. Sets all policy-state variables needed by # every policy file, using exactly two IPA queries (one host-show, one group-find). diff --git a/setup/modules/FreeipaAnsible/ansible/policies.d/dev_security-scan.sh b/setup/modules/FreeipaAnsible/ansible/policies.d/dev_security-scan.sh index cd1934d..18285f2 100644 --- a/setup/modules/FreeipaAnsible/ansible/policies.d/dev_security-scan.sh +++ b/setup/modules/FreeipaAnsible/ansible/policies.d/dev_security-scan.sh @@ -12,7 +12,10 @@ if [[ "$WANT_SECURITY_SCAN" == true ]]; then # 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) +# `hostname` (inetutils) may be absent on minimal Arch; resolve without it. +HOSTNAME=$( { command -v hostname >/dev/null 2>&1 && hostname -f 2>/dev/null; } \ + || hostnamectl --static 2>/dev/null || cat /etc/hostname 2>/dev/null \ + || cat /proc/sys/kernel/hostname 2>/dev/null ) DATE=$(date +%Y-%m-%d) { echo "=== ansipa-security-scan: $DATE $HOSTNAME ===" diff --git a/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh b/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh index a7f76a8..1a5b65c 100644 --- a/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh +++ b/setup/modules/FreeipaAnsible/image/ansipa-checkmk-setup.sh @@ -27,8 +27,20 @@ warn() { echo "[$LOG_TAG][WARN] $*" >&2; } _write_push_requests() { [[ -d /cmk-creds ]] || return 0 local _hosts _h + # `ipa hostgroup-show` prints members on the SAME line as the label + # (" Member hosts: a.example, b.example") and wraps long lists onto + # indented continuation lines. Capture the label line's own value AND any + # continuation lines, stopping at the next " Key:" field. (The previous + # awk did `next` on the label line, discarding the very line that holds the + # hostnames, so no host was ever extracted and no push request written.) _hosts=$(ipa hostgroup-show dev_mon_base --all 2>/dev/null \ - | awk '/Member hosts:/{p=1; next} /^$/{p=0} p{print}' \ + | awk ' + /^ [A-Za-z][A-Za-z0-9 _-]*:/ { + if ($0 ~ /Member hosts:/) { sub(/.*Member hosts:[[:space:]]*/, ""); p=1; print; next } + else { 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 @@ -39,17 +51,12 @@ _write_push_requests() { 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 ────────────────────────────────────────────────────── +# Must run BEFORE the DONE_FLAG early-exit: that path calls _write_push_requests, +# which needs IPA_REALM + IPA_ADMIN_PASS to kinit. Previously these were resolved +# only after the early exit, so every rerun kinit'd with an empty password, failed +# silently, and never wrote push-mode requests — leaving NAT'd clients stuck in +# pull mode (unreachable) forever. # docker-env.service writes container env to /etc/container.env; source it. [[ -f /etc/container.env ]] && source /etc/container.env || true @@ -59,6 +66,17 @@ CMK_API="${CMK_URL}/${CMK_SITE}/check_mk/api/1.0" IPA_REALM="${IPA_REALM:-}" IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-}" +# Skip re-initialization but still run push-mode requests if already set up +if [[ -f "$DONE_FLAG" ]]; then + if [[ -f /etc/ipa/default.conf && -n "$IPA_ADMIN_PASS" && -n "$IPA_REALM" ]]; then + echo "$IPA_ADMIN_PASS" | kinit "admin@${IPA_REALM}" &>/dev/null \ + && _write_push_requests 2>/dev/null + kdestroy &>/dev/null || true + fi + log "Already configured (delete $DONE_FLAG to force re-initialization)" + exit 0 +fi + if [[ -z "$IPA_REALM" || -z "$IPA_ADMIN_PASS" ]]; then warn "IPA_REALM or IPA_ADMIN_PASSWORD not set — cannot configure IPA side" exit 1