From fb8ca498efd590c534f853b4e3c2faadf86616b4 Mon Sep 17 00:00:00 2001 From: The_miro Date: Wed, 20 May 2026 12:00:55 +0200 Subject: [PATCH] feat(freeipa): add AppArmor deny profiles to binary blocking policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Binary blocking now applies two layers: 1. PATH-priority wrapper in /usr/local/bin/ (existing) 2. Empty AppArmor profile in /etc/apparmor.d/ loaded in enforce mode An empty AppArmor profile denies all access — the blocked binary cannot load shared libraries and exits immediately with a permission error, covering callers that use absolute paths and bypassed the wrapper. AppArmor layer is skipped silently when apparmor_parser is not present, and deferred with a warning if the real binary is not yet installed. Profiles are unloaded and deleted when the host leaves the policy group. Co-Authored-By: Claude Sonnet 4.6 --- .../ansible/ansipa-enforce-policies.sh | 86 +++++++++++++++++-- 1 file changed, 77 insertions(+), 9 deletions(-) diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh index 935bc3f..77e5466 100755 --- a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh @@ -5,13 +5,14 @@ # leaving the group removes it on the next run (every 30 min via systemd timer). # # Host-group naming conventions: -# policy-block-binary- Block execution of via a wrapper in /usr/local/bin/ +# policy-block-binary- Block execution of via two layers: +# 1. PATH-priority wrapper in /usr/local/bin/ (catches $PATH calls) +# 2. AppArmor deny profile in /etc/apparmor.d/ (catches absolute paths) +# AppArmor layer is skipped silently if apparmor_parser is not present. # policy-timeshift-backup Enforce a daily Timeshift snapshot (requires timeshift installed) # policy-security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans # # Notes: -# - Binary blocking uses a PATH-priority wrapper in /usr/local/bin/; callers using -# the full absolute path bypass it. For hard enforcement add AppArmor/SELinux rules. # - Install scan tools first: add the host to ansipa-module-anti-malware. # - Configure Timeshift (type + target device) before enabling policy-timeshift-backup. @@ -20,6 +21,7 @@ set -euo pipefail LOG_TAG="ansipa-policies" STATE_DIR="/var/lib/ansipa-policies" BLOCK_DIR="/usr/local/bin" +APPARMOR_DIR="/etc/apparmor.d" CRON_DIR="/etc/cron.d" log() { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; } @@ -60,7 +62,7 @@ fi log "Active policies — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \ "| timeshift-backup: $WANT_TIMESHIFT_BACKUP | security-scan: $WANT_SECURITY_SCAN" -# ── Helper ──────────────────────────────────────────────────────────────────── +# ── Helpers ─────────────────────────────────────────────────────────────────── in_active_list() { local needle="$1" for b in "${ACTIVE_BLOCK_BINARIES[@]}"; do @@ -69,9 +71,73 @@ in_active_list() { return 1 } +# Find the real installed binary, skipping /usr/local/bin where our wrapper lives. +find_real_binary() { + local name="$1" + for dir in /usr/bin /usr/sbin /bin /sbin /usr/local/sbin /opt/bin; do + [[ -x "$dir/$name" ]] && echo "$dir/$name" && return 0 + done + return 1 +} + +aa_profile_file() { echo "$APPARMOR_DIR/ansipa-block-${1}"; } + +# Load an AppArmor deny profile for a binary path. +# An empty AppArmor profile denies all access: the binary cannot load shared +# libraries or open any files, so it exits immediately with a permission error. +apply_apparmor_block() { + local bin="$1" + command -v apparmor_parser &>/dev/null || return 0 + + local bin_path + bin_path=$(find_real_binary "$bin") || { + warn "AppArmor block: real binary '$bin' not found on disk — profile skipped until it is installed." + return 0 + } + + local profile_file + profile_file=$(aa_profile_file "$bin") + + # Write the profile only if it doesn't exist or points to a different path. + if [[ ! -f "$profile_file" ]] || ! grep -qF "$bin_path" "$profile_file" 2>/dev/null; then + log "Writing AppArmor block profile: $profile_file ($bin_path)" + cat > "$profile_file" < + +# ansipa-block-policy: managed by ansipa-enforce-policies — do not edit manually. +# Deny all access so the binary cannot load libraries or run. +# To unblock manually: apparmor_parser -R $profile_file && rm $profile_file +$bin_path { +} +PROFILE + fi + + # Load (or reload) the profile in enforce mode. + if ! apparmor_parser -r "$profile_file" 2>/dev/null; then + warn "apparmor_parser failed to load $profile_file — AppArmor block not active" + fi +} + +# Remove the AppArmor deny profile for a binary. +remove_apparmor_block() { + local bin="$1" + command -v apparmor_parser &>/dev/null || return 0 + + local profile_file + profile_file=$(aa_profile_file "$bin") + [[ -f "$profile_file" ]] || return 0 + + if grep -q "ansipa-block-policy" "$profile_file" 2>/dev/null; then + apparmor_parser -R "$profile_file" 2>/dev/null || true + rm -f "$profile_file" + log "Removed AppArmor block profile: $bin" + fi +} + # ── Binary blocking ─────────────────────────────────────────────────────────── -# Wrapper scripts are placed in /usr/local/bin/ (higher PATH priority than /usr/bin/). -# The "blocked by ansipa policy" sentinel line lets us identify managed wrappers. +# Layer 1: PATH-priority wrapper in /usr/local/bin/ — blocks $PATH-based calls. +# Layer 2: AppArmor deny profile — blocks absolute-path calls and direct exec(). +# Both layers use the "ansipa policy" sentinel to identify managed files. BLOCK_STATE="$STATE_DIR/blocked-binaries" [[ -f "$BLOCK_STATE" ]] || touch "$BLOCK_STATE" @@ -79,7 +145,7 @@ BLOCK_STATE="$STATE_DIR/blocked-binaries" for BIN in "${ACTIVE_BLOCK_BINARIES[@]}"; do WRAPPER="$BLOCK_DIR/$BIN" if [[ ! -f "$WRAPPER" ]] || ! grep -q "blocked by ansipa policy" "$WRAPPER" 2>/dev/null; then - log "Applying block: $BIN" + log "Applying PATH wrapper block: $BIN" cat > "$WRAPPER" </dev/null; then rm -f "$WRAPPER" - log "Removed block: $OLD_BIN" + log "Removed PATH wrapper block: $OLD_BIN" fi + remove_apparmor_block "$OLD_BIN" fi done < "$BLOCK_STATE"