feat(freeipa): add policy enforcement for binary blocking, backups, scans, and sudo
Introduces a FreeIPA host-group-driven policy system alongside a sudo rules management playbook: - ansipa-enforce-policies.sh: client-side enforcer (systemd timer, 30 min) - policy-block-binary-<name>: PATH-priority wrapper blocks the binary - policy-timeshift-backup: daily Timeshift snapshot cron (03:00) - policy-security-scan: daily ClamAV/rkhunter/chkrootkit cron (02:00) Policies are reversible — leaving a group removes enforcement on next run. - deploy-ansipa-policies.yml: deploys enforcer + systemd service/timer to clients - manage-sudo-rules.yml: creates FreeIPA sudo rules (allow_sudoers, allow_sudo_nopasswd) that SSSD clients already pick up via --sudo enrollment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
da0a9e7a32
commit
45fd7e5d36
|
|
@ -0,0 +1,185 @@
|
|||
#!/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:
|
||||
# policy-block-binary-<name> Block execution of <name> via a wrapper in /usr/local/bin/
|
||||
# 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.
|
||||
|
||||
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 policy groups ────────────────────────────────────────────────
|
||||
ACTIVE_BLOCK_BINARIES=()
|
||||
WANT_TIMESHIFT_BACKUP=false
|
||||
WANT_SECURITY_SCAN=false
|
||||
|
||||
if [[ -n "$RAW_GROUPS" ]]; then
|
||||
while IFS=',' read -ra GRP_ARRAY; do
|
||||
for g in "${GRP_ARRAY[@]}"; do
|
||||
g="${g// /}"
|
||||
case "$g" in
|
||||
policy-block-binary-*) ACTIVE_BLOCK_BINARIES+=("${g#policy-block-binary-}") ;;
|
||||
policy-timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;;
|
||||
policy-security-scan) WANT_SECURITY_SCAN=true ;;
|
||||
esac
|
||||
done
|
||||
done <<< "$RAW_GROUPS"
|
||||
fi
|
||||
|
||||
log "Active policies — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
|
||||
"| timeshift-backup: $WANT_TIMESHIFT_BACKUP | security-scan: $WANT_SECURITY_SCAN"
|
||||
|
||||
# ── Helper ────────────────────────────────────────────────────────────────────
|
||||
in_active_list() {
|
||||
local needle="$1"
|
||||
for b in "${ACTIVE_BLOCK_BINARIES[@]}"; do
|
||||
[[ "$b" == "$needle" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── 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.
|
||||
|
||||
BLOCK_STATE="$STATE_DIR/blocked-binaries"
|
||||
[[ -f "$BLOCK_STATE" ]] || touch "$BLOCK_STATE"
|
||||
|
||||
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"
|
||||
cat > "$WRAPPER" <<WRAPPER
|
||||
#!/bin/bash
|
||||
# blocked by ansipa policy
|
||||
echo "[$LOG_TAG] '$BIN' is blocked by system policy on this host." >&2
|
||||
exit 1
|
||||
WRAPPER
|
||||
chmod 755 "$WRAPPER"
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove wrappers for binaries no longer in any active policy group.
|
||||
while IFS= read -r OLD_BIN; do
|
||||
[[ -z "$OLD_BIN" ]] && continue
|
||||
if ! in_active_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 block: $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 ansipa-module-timeshift first. Cron will be installed anyway."
|
||||
fi
|
||||
log "Enabling daily Timeshift backups"
|
||||
cat > "$TIMESHIFT_CRON" <<'CRON'
|
||||
# ansipa-policy-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 policy-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.
|
||||
# Managed by ansipa-enforce-policies — do not edit manually.
|
||||
LOG=/var/log/ansipa-security-scan.log
|
||||
{
|
||||
echo "=== ansipa-security-scan: $(date) ==="
|
||||
|
||||
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
|
||||
SCAN
|
||||
chmod 755 "$SCAN_SCRIPT"
|
||||
|
||||
if [[ ! -f "$SCAN_CRON" ]]; then
|
||||
log "Enabling daily security scans (ClamAV / rkhunter / chkrootkit)"
|
||||
cat > "$SCAN_CRON" <<'CRON'
|
||||
# ansipa-policy-security-scan: managed by ansipa-enforce-policies — do not edit manually.
|
||||
# Install scan tools by adding the host to the ansipa-module-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 policy-security-scan group)"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Policy enforcement complete."
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
# deploy-ansipa-policies.yml — deploy the policy enforcement daemon to enrolled clients.
|
||||
#
|
||||
# Installs ansipa-enforce-policies.sh and a systemd timer that runs it every 30 minutes.
|
||||
# Policies are declared by adding hosts to the following FreeIPA host groups:
|
||||
#
|
||||
# policy-block-binary-<name> Block execution of <name> via a PATH-priority wrapper
|
||||
# policy-timeshift-backup Enforce daily Timeshift snapshots (03:00)
|
||||
# policy-security-scan Enforce daily ClamAV + rkhunter + chkrootkit scans (02:00)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Host enrolled in FreeIPA (sssd + ipa CLI available)
|
||||
# - For security-scan: also add host to ansipa-module-anti-malware group
|
||||
# - For timeshift-backup: also add host to ansipa-module-timeshift group and
|
||||
# configure Timeshift (type + target device) on the host
|
||||
#
|
||||
# Usage:
|
||||
# ansible-playbook -i inventory deploy-ansipa-policies.yml
|
||||
|
||||
- name: Deploy FreeIPA policy enforcer
|
||||
hosts: all
|
||||
become: yes
|
||||
|
||||
tasks:
|
||||
|
||||
- name: Deploy policy enforcer script
|
||||
copy:
|
||||
src: ansipa-enforce-policies.sh
|
||||
dest: /usr/local/bin/ansipa-enforce-policies.sh
|
||||
mode: '0755'
|
||||
|
||||
- name: Create policy state directory
|
||||
file:
|
||||
path: /var/lib/ansipa-policies
|
||||
state: directory
|
||||
mode: '0700'
|
||||
|
||||
- name: Install policy enforcer systemd service
|
||||
copy:
|
||||
dest: /etc/systemd/system/ansipa-enforce-policies.service
|
||||
mode: '0644'
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Enforce FreeIPA host-group policies (binary blocks, backups, scans)
|
||||
After=network-online.target sssd.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/ansipa-enforce-policies.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
- name: Install policy enforcer systemd timer
|
||||
copy:
|
||||
dest: /etc/systemd/system/ansipa-enforce-policies.timer
|
||||
mode: '0644'
|
||||
content: |
|
||||
[Unit]
|
||||
Description=Periodic FreeIPA policy enforcement
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitActiveSec=30min
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
|
||||
- name: Reload systemd
|
||||
command: systemctl daemon-reload
|
||||
|
||||
- name: Enable and start policy enforcer timer
|
||||
systemd:
|
||||
name: ansipa-enforce-policies.timer
|
||||
enabled: yes
|
||||
state: started
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
# manage-sudo-rules.yml — create and maintain FreeIPA sudo rules.
|
||||
#
|
||||
# This playbook provisions the sudo rules that enrolled clients pick up via SSSD
|
||||
# (configured by the --sudo flag in freeipa-enroll.sh). Run it once when setting
|
||||
# up the domain, and again whenever you add or change a rule.
|
||||
#
|
||||
# Default rules created:
|
||||
# allow_sudoers Members of the 'sudoers' IPA group get full sudo (password required)
|
||||
# allow_sudo_nopasswd Members of 'sudo-nopasswd' get full sudo (NOPASSWD)
|
||||
#
|
||||
# To grant a user sudo access:
|
||||
# ipa group-add-member sudoers --users=<username>
|
||||
# To grant passwordless sudo:
|
||||
# ipa group-add-member sudo-nopasswd --users=<username>
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Active admin Kerberos ticket on the target host: kinit admin
|
||||
# - ipa CLI available (run on the IPA server or any enrolled admin workstation)
|
||||
#
|
||||
# Usage:
|
||||
# kinit admin
|
||||
# ansible-playbook -i ipa-server.example.com, manage-sudo-rules.yml
|
||||
# # or, if 'ipa_server' is defined in your inventory:
|
||||
# ansible-playbook -i inventory manage-sudo-rules.yml
|
||||
|
||||
- name: Manage FreeIPA sudo rules
|
||||
hosts: "{{ ipa_admin_host | default('ipa_server') }}"
|
||||
become: no
|
||||
|
||||
vars:
|
||||
sudo_rules:
|
||||
- rule_name: allow_sudoers
|
||||
group: sudoers
|
||||
description: "Full sudo access for members of the sudoers group (password required)"
|
||||
nopasswd: false
|
||||
|
||||
- rule_name: allow_sudo_nopasswd
|
||||
group: sudo-nopasswd
|
||||
description: "Full sudo access for members of sudo-nopasswd group (no password)"
|
||||
nopasswd: true
|
||||
|
||||
tasks:
|
||||
|
||||
- name: Verify ipa command is available and authenticated
|
||||
command: ipa ping
|
||||
changed_when: false
|
||||
register: ipa_ping
|
||||
failed_when: ipa_ping.rc != 0
|
||||
|
||||
- name: Ensure IPA user groups exist for each sudo rule
|
||||
shell: >
|
||||
ipa group-show "{{ item.group }}" >/dev/null 2>&1 ||
|
||||
ipa group-add "{{ item.group }}"
|
||||
--desc="{{ item.description }}"
|
||||
register: group_result
|
||||
changed_when: "'Added group' in group_result.stdout"
|
||||
loop: "{{ sudo_rules }}"
|
||||
|
||||
- name: Ensure sudo rules exist
|
||||
shell: >
|
||||
ipa sudorule-show "{{ item.rule_name }}" >/dev/null 2>&1 ||
|
||||
ipa sudorule-add "{{ item.rule_name }}"
|
||||
--desc="{{ item.description }}"
|
||||
--cmdcat=all
|
||||
--hostcat=all
|
||||
register: rule_result
|
||||
changed_when: "'Added Sudo Rule' in rule_result.stdout"
|
||||
loop: "{{ sudo_rules }}"
|
||||
|
||||
- name: Assign groups to their sudo rules
|
||||
shell: >
|
||||
ipa sudorule-show "{{ item.rule_name }}" --all 2>/dev/null |
|
||||
grep -q "{{ item.group }}" ||
|
||||
ipa sudorule-add-user "{{ item.rule_name }}" --groups="{{ item.group }}"
|
||||
register: assign_result
|
||||
changed_when: "'Number of members added' in assign_result.stdout"
|
||||
loop: "{{ sudo_rules }}"
|
||||
|
||||
- name: Set NOPASSWD (sudooption !authenticate) on passwordless rules
|
||||
shell: >
|
||||
ipa sudorule-show "{{ item.rule_name }}" --all 2>/dev/null |
|
||||
grep -q "!authenticate" ||
|
||||
ipa sudorule-add-option "{{ item.rule_name }}" --sudooption "!authenticate"
|
||||
register: nopasswd_result
|
||||
changed_when: "'Added option' in nopasswd_result.stdout"
|
||||
loop: "{{ sudo_rules | selectattr('nopasswd', 'equalto', true) | list }}"
|
||||
|
||||
- name: Show configured sudo rules
|
||||
command: ipa sudorule-find --all
|
||||
changed_when: false
|
||||
register: sudo_summary
|
||||
|
||||
- name: Display sudo rules summary
|
||||
debug:
|
||||
msg: "{{ sudo_summary.stdout_lines }}"
|
||||
Loading…
Reference in New Issue