#!/usr/bin/env bash # ansipa-policy.sh — shared library for ansipa policy enforcement. # # Sourced by ansipa-enforce-policies.sh before any policy file is loaded. # Provides: constants, logging, group discovery, policystore sync, # and the _smb_parse_cred() helper (shared across usr_smb and usr_policystore). # # After _ansipa_discover() the following variables are exported for policy files: # RAW_GROUPS (comma-sep host-group string from ipa host-show) # _ALL_USER_GROUPS (newline-sep full list from one ipa group-find call) # ACTIVE_DAEMON_ENABLE ACTIVE_DAEMON_DISABLE (arrays, unit name suffixes) # WANT_TIMESHIFT_BACKUP WANT_CLAMSCAN WANT_NO_LOCAL_USERS (booleans) # ACTIVE_LOCAL_SUDO_USERS ACTIVE_SSH_USERS ACTIVE_MON_DEV (arrays) # WANT_USR_ADMIN (boolean) # ACTIVE_BLOCK_BINARIES ACTIVE_BLOCK_IPA_GROUPS (parallel arrays) # ACTIVE_PRT_GROUPS ACTIVE_PRT_PRINTERS (parallel arrays) # ACTIVE_MON_USR (array, usr_mon_* suffixes) # ── Constants ───────────────────────────────────────────────────────────────── STATE_DIR="/var/lib/ansipa-policies" BLOCK_DIR="/usr/local/bin" CRON_DIR="/etc/cron.d" LOG_TAG="ansipa-policies" # Policy source resolution: /run/ansipa/current is a tmpfs workdir populated # fresh on every ansipa-pull-apply.sh run with content whose commit has ALREADY # been GPG-signature-verified and fast-forward-checked (see ansipa-pull-apply.sh # and deploy-ansipa-git-pull.yml) — it is the trusted, live source once the git # pull system is bootstrapped. Before that first successful pull (or if a node # is never enrolled in it), fall back to the Ansible-deployed baseline at # /usr/local/lib/ansipa/policies.d. There is no third, unverified source — # the old SMB-policystore auto-sync (which sourced whatever unsigned .sh files # sat on the share, with no integrity check) has been removed. if [[ -d /run/ansipa/current/policies.d ]]; then POLICY_DIR="/run/ansipa/current/policies.d" else POLICY_DIR="/usr/local/lib/ansipa/policies.d" fi # ── Logging ─────────────────────────────────────────────────────────────────── 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). _ansipa_discover() { # ── Host groups (device policies) ───────────────────────────────────────── RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \ | grep -i "Member of host-groups:" | sed 's/.*: //' || true) ACTIVE_DAEMON_ENABLE=() ACTIVE_DAEMON_DISABLE=() ACTIVE_LOCAL_SUDO_USERS=() ACTIVE_SSH_USERS=() ACTIVE_MON_DEV=() WANT_TIMESHIFT_BACKUP=false WANT_CLAMSCAN=false WANT_NO_LOCAL_USERS=false 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_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_clamscan runs the scan + emits its own CheckMK check; # match it BEFORE the generic dev_mon_* catch-all so it sets # WANT_CLAMSCAN instead of being treated as a dev_mon.sh check. dev_mon_clamscan) WANT_CLAMSCAN=true ;; dev_mon_*) ACTIVE_MON_DEV+=("${g#dev_mon_}") ;; esac done done <<< "$RAW_GROUPS" fi # ── User groups (one call, all prefixes) ────────────────────────────────── _ALL_USER_GROUPS=$(ipa group-find --pkey-only 2>/dev/null \ | awk '/Group name:/ {print $NF}' | sort -u || true) WANT_USR_ADMIN=false ACTIVE_BLOCK_BINARIES=() ACTIVE_BLOCK_IPA_GROUPS=() ACTIVE_PRT_GROUPS=() ACTIVE_PRT_PRINTERS=() ACTIVE_MON_USR=() while IFS= read -r _grp; do [[ -z "$_grp" ]] && continue case "$_grp" in usr_admin) WANT_USR_ADMIN=true ;; usr_block-binary-*) local _raw="${_grp#usr_block-binary-}" ACTIVE_BLOCK_BINARIES+=("${_raw//__/.}") ACTIVE_BLOCK_IPA_GROUPS+=("$_grp") ;; usr_prt_*) ACTIVE_PRT_GROUPS+=("$_grp") ACTIVE_PRT_PRINTERS+=("${_grp#usr_prt_}") ;; usr_mon_*) ACTIVE_MON_USR+=("${_grp#usr_mon_}") ;; esac done <<< "$_ALL_USER_GROUPS" unset _grp _raw log "Device policies — daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none}" \ "| daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \ "| timeshift-backup: $WANT_TIMESHIFT_BACKUP | clamscan: $WANT_CLAMSCAN" \ "| 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}" \ "| mon-usr: ${ACTIVE_MON_USR[*]:-none}" } # ── SMB credential parser ───────────────────────────────────────────────────── # Used by both usr_smb.sh and usr_policystore.sh. # Outputs "host samba_user password" when the IPA group description contains # a cifs:// credential (written by ansipa-smb-setup.sh). _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]}" } # NOTE: this file previously had an _ansipa_sync_policystore() here, which # synced *.sh files from the ansipa-policystore SMB share directly into # $POLICY_DIR with no integrity check before they were sourced as root. That # blind-trust path has been removed; policy content now only reaches # $POLICY_DIR via ansipa-pull-apply.sh, which requires a valid GPG signature # and a fast-forward history before anything is applied. See # docs/md/freeipa-ansible.md, "Git-based policy distribution".