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