diff --git a/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh new file mode 100755 index 0000000..935bc3f --- /dev/null +++ b/setup/modules/FreeipaAnsible/ansible/ansipa-enforce-policies.sh @@ -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- Block execution of 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" <&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." diff --git a/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml new file mode 100644 index 0000000..e9d632e --- /dev/null +++ b/setup/modules/FreeipaAnsible/ansible/deploy-ansipa-policies.yml @@ -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- Block execution of 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 diff --git a/setup/modules/FreeipaAnsible/ansible/manage-sudo-rules.yml b/setup/modules/FreeipaAnsible/ansible/manage-sudo-rules.yml new file mode 100644 index 0000000..e02bf4f --- /dev/null +++ b/setup/modules/FreeipaAnsible/ansible/manage-sudo-rules.yml @@ -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= +# To grant passwordless sudo: +# ipa group-add-member sudo-nopasswd --users= +# +# 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 }}"