refactor(ansipa): split enforce-policies monolith into modular policies.d/ system
The 1550-line ansipa-enforce-policies.sh is replaced by a slim orchestrator
that sources policy files from policies.d/ — one file per policy type — via
a shared lib/ansipa-policy.sh that consolidates all IPA group discovery into
two queries (one host-show, one group-find) instead of five or more.
New files:
- ansible/lib/ansipa-policy.sh shared constants, log/warn, _ansipa_discover(),
_smb_parse_cred(), _ansipa_sync_policystore()
- ansible/policies.d/dev_daemon.sh
- ansible/policies.d/dev_timeshift-backup.sh
- ansible/policies.d/dev_security-scan.sh
- ansible/policies.d/dev_no-local-users.sh
- ansible/policies.d/dev_local-sudo.sh
- ansible/policies.d/dev_ssh.sh
- ansible/policies.d/dev_mon.sh includes usr_mon_logins (same CMK dep)
- ansible/policies.d/usr_admin.sh
- ansible/policies.d/usr_block-binary.sh
- ansible/policies.d/usr_prt.sh
- ansible/policies.d/usr_scan-notify.sh
- ansible/policies.d/usr_smb.sh
- ansible/policies.d/usr_policystore.sh NEW: auto-mount ~/policystore for
usr_smb_adm_policystore members
The policies.d/ folder on the FreeIPA container is exposed as an SMB share
(ansipa-policystore) so admins can live-edit policy files without Ansible.
The enforcer syncs from the share before each run; cached local files are
used as fallback when SMB is unreachable.
- ansipa-smb-setup.sh: adds ansipa-policystore share + smb-policystore system
user + usr_smb_adm_policystore IPA group with auto-generated credential
- deploy-ansipa-policies.yml: deploys lib/ and policies.d/ to enrolled clients;
adds optional seed_policystore task for bootstrap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat/astal-menu
parent
b4ae7ae4ce
commit
815c0e57a8
File diff suppressed because it is too large
Load Diff
|
|
@ -2,6 +2,10 @@
|
||||||
# deploy-ansipa-policies.yml — deploy the policy enforcement daemon to enrolled clients.
|
# 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.
|
# Installs ansipa-enforce-policies.sh and a systemd timer that runs it every 30 minutes.
|
||||||
|
# Policy logic lives in /usr/local/lib/ansipa/policies.d/ (one .sh file per policy type).
|
||||||
|
# At runtime the enforcer also syncs those files from the ansipa-policystore SMB share
|
||||||
|
# on the FreeIPA container, so live edits via the share take effect without re-running Ansible.
|
||||||
|
#
|
||||||
# Device policies (FreeIPA host groups — applied to the whole machine):
|
# Device policies (FreeIPA host groups — applied to the whole machine):
|
||||||
# dev_daemon-enable-<unit> Ensure <unit> is enabled and running; reverted when host leaves group
|
# dev_daemon-enable-<unit> Ensure <unit> is enabled and running; reverted when host leaves group
|
||||||
# dev_daemon-disable-<unit> Ensure <unit> is disabled and stopped; reverted when host leaves group
|
# dev_daemon-disable-<unit> Ensure <unit> is disabled and stopped; reverted when host leaves group
|
||||||
|
|
@ -21,6 +25,8 @@
|
||||||
# usr_scan-notify Fetch alerts from server, notify user every 10 min until acknowledged
|
# usr_scan-notify Fetch alerts from server, notify user every 10 min until acknowledged
|
||||||
# usr_smb_r_<name> Mount read-only Samba share ~/‹name› for members; credentials in IPA group description
|
# usr_smb_r_<name> Mount read-only Samba share ~/‹name› for members; credentials in IPA group description
|
||||||
# usr_smb_rw_<name> Mount read-write Samba share ~/‹name› for members (rw beats r if both)
|
# usr_smb_rw_<name> Mount read-write Samba share ~/‹name› for members (rw beats r if both)
|
||||||
|
# usr_smb_adm_policystore Mount the policy store at ~/policystore (rw) for admin members;
|
||||||
|
# credential in IPA group description (set by ansipa-smb-setup.sh)
|
||||||
#
|
#
|
||||||
# CheckMK monitoring policies (device host-groups, unless noted):
|
# CheckMK monitoring policies (device host-groups, unless noted):
|
||||||
# dev_mon_base Install CheckMK agent; register host in CMK; check: installed packages.
|
# dev_mon_base Install CheckMK agent; register host in CMK; check: installed packages.
|
||||||
|
|
@ -43,12 +49,17 @@
|
||||||
# - For timeshift-backup: also add host to dev_mod_timeshift group
|
# - For timeshift-backup: also add host to dev_mod_timeshift group
|
||||||
# - For dev_mon_*: CheckMK CE must be up; run ansipa-checkmk-setup.sh on the FreeIPA
|
# - For dev_mon_*: CheckMK CE must be up; run ansipa-checkmk-setup.sh on the FreeIPA
|
||||||
# container first (seeds credentials + groups in IPA)
|
# container first (seeds credentials + groups in IPA)
|
||||||
# - For usr_smb_r_*/usr_smb_rw_*: cifs-utils installed (handled below); Samba service
|
# - For usr_smb_r_*/usr_smb_rw_*/usr_smb_adm_policystore: cifs-utils installed (handled below);
|
||||||
# running on the IPA container (ansipa-smb.service auto-starts)
|
# Samba service running on the IPA container (ansipa-smb.service auto-starts)
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ansible-playbook -i inventory deploy-ansipa-policies.yml \
|
# ansible-playbook -i inventory deploy-ansipa-policies.yml \
|
||||||
# -e smb_scan_password=<password> # or use --vault-password-file
|
# -e smb_scan_password=<password> # or use --vault-password-file
|
||||||
|
#
|
||||||
|
# To seed the policystore on the FreeIPA container for the first time (bootstrap only;
|
||||||
|
# thereafter edit files directly via the SMB share at ~/policystore):
|
||||||
|
# ansible-playbook -i inventory deploy-ansipa-policies.yml \
|
||||||
|
# -e smb_scan_password=<password> -e seed_policystore=true -e ipa_server_host=<ipa-host>
|
||||||
|
|
||||||
- name: Deploy FreeIPA policy enforcer
|
- name: Deploy FreeIPA policy enforcer
|
||||||
hosts: all
|
hosts: all
|
||||||
|
|
@ -59,11 +70,27 @@
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|
||||||
- name: Create local check scripts directory (CheckMK agent)
|
- name: Create ansipa library and policy directories
|
||||||
file:
|
file:
|
||||||
path: /usr/lib/check_mk_agent/local
|
path: "{{ item }}"
|
||||||
state: directory
|
state: directory
|
||||||
mode: '0755'
|
mode: '0755'
|
||||||
|
loop:
|
||||||
|
- /usr/local/lib/ansipa
|
||||||
|
- /usr/local/lib/ansipa/policies.d
|
||||||
|
- /usr/lib/check_mk_agent/local
|
||||||
|
|
||||||
|
- name: Deploy shared policy library
|
||||||
|
copy:
|
||||||
|
src: lib/ansipa-policy.sh
|
||||||
|
dest: /usr/local/lib/ansipa/policy.sh
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
|
- name: Deploy policy modules
|
||||||
|
copy:
|
||||||
|
src: policies.d/
|
||||||
|
dest: /usr/local/lib/ansipa/policies.d/
|
||||||
|
mode: '0644'
|
||||||
|
|
||||||
- name: Install required packages
|
- name: Install required packages
|
||||||
package:
|
package:
|
||||||
|
|
@ -151,3 +178,16 @@
|
||||||
name: ansipa-enforce-policies.timer
|
name: ansipa-enforce-policies.timer
|
||||||
enabled: yes
|
enabled: yes
|
||||||
state: started
|
state: started
|
||||||
|
|
||||||
|
# ── Bootstrap: seed policy files onto the policystore SMB share ──────────
|
||||||
|
# Run once with -e seed_policystore=true -e ipa_server_host=<host>.
|
||||||
|
# After seeding, edit policies directly via ~/policystore (usr_smb_adm_policystore).
|
||||||
|
- name: Seed policystore on IPA server (bootstrap — run once)
|
||||||
|
copy:
|
||||||
|
src: "policies.d/"
|
||||||
|
dest: /data/policy-store/policies.d/
|
||||||
|
mode: '0664'
|
||||||
|
directory_mode: '02775'
|
||||||
|
delegate_to: "{{ ipa_server_host }}"
|
||||||
|
run_once: true
|
||||||
|
when: seed_policystore | default(false) | bool
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
#!/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_SECURITY_SCAN WANT_NO_LOCAL_USERS (booleans)
|
||||||
|
# ACTIVE_LOCAL_SUDO_USERS ACTIVE_SSH_USERS ACTIVE_MON_DEV (arrays)
|
||||||
|
# WANT_USR_ADMIN WANT_SCAN_NOTIFY (booleans)
|
||||||
|
# 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_DIR="/usr/local/lib/ansipa/policies.d"
|
||||||
|
|
||||||
|
# ── 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; }
|
||||||
|
|
||||||
|
# ── 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_SECURITY_SCAN=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_security-scan) WANT_SECURITY_SCAN=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_*) 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
|
||||||
|
WANT_SCAN_NOTIFY=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_scan-notify) WANT_SCAN_NOTIFY=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 | security-scan: $WANT_SECURITY_SCAN" \
|
||||||
|
"| 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}" \
|
||||||
|
"| scan-notify: $WANT_SCAN_NOTIFY | 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]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Policystore sync ──────────────────────────────────────────────────────────
|
||||||
|
# Called by the orchestrator after discovery. Reads the SMB credential from the
|
||||||
|
# usr_smb_adm_policystore IPA group description, then syncs *.sh files from the
|
||||||
|
# ansipa-policystore SMB share into $POLICY_DIR.
|
||||||
|
#
|
||||||
|
# On SMB failure: warns and returns 0; the orchestrator continues with whatever
|
||||||
|
# files are already in $POLICY_DIR (Ansible-deployed or previously synced).
|
||||||
|
_ansipa_sync_policystore() {
|
||||||
|
command -v smbclient &>/dev/null || return 0
|
||||||
|
|
||||||
|
local _cred_line _host _suser _spass
|
||||||
|
_cred_line=$(_smb_parse_cred "usr_smb_adm_policystore" 2>/dev/null || true)
|
||||||
|
if [[ -z "$_cred_line" ]]; then
|
||||||
|
return 0 # group absent or no credential yet — use local files
|
||||||
|
fi
|
||||||
|
read -r _host _suser _spass <<< "$_cred_line"
|
||||||
|
|
||||||
|
local _tmp
|
||||||
|
_tmp=$(mktemp -d)
|
||||||
|
|
||||||
|
# Build a temporary smbclient credentials file (avoids shell-quoting issues)
|
||||||
|
local _creds_tmp
|
||||||
|
_creds_tmp=$(mktemp)
|
||||||
|
printf 'username=%s\npassword=%s\ndomain=WORKGROUP\n' "$_suser" "$_spass" \
|
||||||
|
> "$_creds_tmp"
|
||||||
|
chmod 600 "$_creds_tmp"
|
||||||
|
|
||||||
|
if smbclient "//${_host}/ansipa-policystore" -A "$_creds_tmp" \
|
||||||
|
-c "cd policies.d; mask *.sh; recurse ON; prompt OFF; lcd ${_tmp}; mget *" \
|
||||||
|
&>/dev/null
|
||||||
|
then
|
||||||
|
# Atomic swap: remove stale files and replace with synced set.
|
||||||
|
rm -f "$POLICY_DIR"/*.sh 2>/dev/null || true
|
||||||
|
cp "$_tmp"/*.sh "$POLICY_DIR"/ 2>/dev/null \
|
||||||
|
&& log "Synced policy store from //${_host}/ansipa-policystore/policies.d" \
|
||||||
|
|| warn "Policystore sync: no .sh files on share — using local files"
|
||||||
|
else
|
||||||
|
warn "Policystore sync failed (SMB unreachable?) — using cached local files"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$_tmp"
|
||||||
|
rm -f "$_creds_tmp"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: dev_daemon — enable or disable systemd units via FreeIPA host-groups.
|
||||||
|
# dev_daemon-enable-<unit> ensure the unit is enabled and running
|
||||||
|
# dev_daemon-disable-<unit> ensure the unit is disabled and stopped
|
||||||
|
# Conflicts (same unit in both lists) are skipped with a warning.
|
||||||
|
|
||||||
|
DAEMON_ENABLE_STATE="$STATE_DIR/daemon-enabled"
|
||||||
|
DAEMON_DISABLE_STATE="$STATE_DIR/daemon-disabled"
|
||||||
|
[[ -f "$DAEMON_ENABLE_STATE" ]] || touch "$DAEMON_ENABLE_STATE"
|
||||||
|
[[ -f "$DAEMON_DISABLE_STATE" ]] || touch "$DAEMON_DISABLE_STATE"
|
||||||
|
|
||||||
|
_svc_unit() { [[ "$1" == *.* ]] && echo "$1" || echo "${1}.service"; }
|
||||||
|
_in_enable_list() { local n="$1"; for s in "${ACTIVE_DAEMON_ENABLE[@]}"; do [[ "$s" == "$n" ]] && return 0; done; return 1; }
|
||||||
|
_in_disable_list() { local n="$1"; for s in "${ACTIVE_DAEMON_DISABLE[@]}"; do [[ "$s" == "$n" ]] && return 0; done; return 1; }
|
||||||
|
|
||||||
|
# Apply enable policies
|
||||||
|
for _SVC in "${ACTIVE_DAEMON_ENABLE[@]+"${ACTIVE_DAEMON_ENABLE[@]}"}"; do
|
||||||
|
if _in_disable_list "$_SVC"; then
|
||||||
|
warn "Conflict: '$_SVC' is in both daemon-enable and daemon-disable groups — skipped"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
_UNIT=$(_svc_unit "$_SVC")
|
||||||
|
_EN=$(systemctl is-enabled "$_UNIT" 2>/dev/null || echo "not-found")
|
||||||
|
_AC=$(systemctl is-active "$_UNIT" 2>/dev/null || echo "inactive")
|
||||||
|
if [[ "$_EN" != "enabled" || "$_AC" != "active" ]]; then
|
||||||
|
log "Enabling service: $_UNIT (enabled=$_EN active=$_AC)"
|
||||||
|
systemctl enable --now "$_UNIT" 2>/dev/null \
|
||||||
|
&& log "Service enabled: $_UNIT" \
|
||||||
|
|| warn "Failed to enable $_UNIT — unit may not exist on this host"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Apply disable policies
|
||||||
|
for _SVC in "${ACTIVE_DAEMON_DISABLE[@]+"${ACTIVE_DAEMON_DISABLE[@]}"}"; do
|
||||||
|
if _in_enable_list "$_SVC"; then
|
||||||
|
continue # conflict already warned above
|
||||||
|
fi
|
||||||
|
_UNIT=$(_svc_unit "$_SVC")
|
||||||
|
_EN=$(systemctl is-enabled "$_UNIT" 2>/dev/null || echo "not-found")
|
||||||
|
_AC=$(systemctl is-active "$_UNIT" 2>/dev/null || echo "inactive")
|
||||||
|
if [[ "$_EN" == "enabled" || "$_AC" == "active" ]]; then
|
||||||
|
log "Disabling service: $_UNIT (enabled=$_EN active=$_AC)"
|
||||||
|
systemctl disable --now "$_UNIT" 2>/dev/null \
|
||||||
|
&& log "Service disabled: $_UNIT" \
|
||||||
|
|| warn "Failed to disable $_UNIT — unit may not exist on this host"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Revert: host left a daemon-enable group → disable and stop the service
|
||||||
|
while IFS= read -r _OLD; do
|
||||||
|
[[ -z "$_OLD" ]] && continue
|
||||||
|
if ! _in_enable_list "$_OLD"; then
|
||||||
|
_UNIT=$(_svc_unit "$_OLD")
|
||||||
|
log "Reverting enable policy: disabling $_UNIT (host left daemon-enable group)"
|
||||||
|
systemctl disable --now "$_UNIT" 2>/dev/null \
|
||||||
|
|| warn "Failed to disable (revert) $_UNIT"
|
||||||
|
fi
|
||||||
|
done < "$DAEMON_ENABLE_STATE"
|
||||||
|
|
||||||
|
# Revert: host left a daemon-disable group → re-enable and start the service
|
||||||
|
while IFS= read -r _OLD; do
|
||||||
|
[[ -z "$_OLD" ]] && continue
|
||||||
|
if ! _in_disable_list "$_OLD"; then
|
||||||
|
_UNIT=$(_svc_unit "$_OLD")
|
||||||
|
log "Reverting disable policy: enabling $_UNIT (host left daemon-disable group)"
|
||||||
|
systemctl enable --now "$_UNIT" 2>/dev/null \
|
||||||
|
|| warn "Failed to enable (revert) $_UNIT"
|
||||||
|
fi
|
||||||
|
done < "$DAEMON_DISABLE_STATE"
|
||||||
|
|
||||||
|
# Persist current state
|
||||||
|
if [[ ${#ACTIVE_DAEMON_ENABLE[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${ACTIVE_DAEMON_ENABLE[@]}" | sort -u > "$DAEMON_ENABLE_STATE"
|
||||||
|
else
|
||||||
|
> "$DAEMON_ENABLE_STATE"
|
||||||
|
fi
|
||||||
|
if [[ ${#ACTIVE_DAEMON_DISABLE[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${ACTIVE_DAEMON_DISABLE[@]}" | sort -u > "$DAEMON_DISABLE_STATE"
|
||||||
|
else
|
||||||
|
> "$DAEMON_DISABLE_STATE"
|
||||||
|
fi
|
||||||
|
unset _SVC _UNIT _EN _AC _OLD
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: dev_local-sudo — per-device sudo grant for named local users.
|
||||||
|
# dev_local-sudo-<username>: adds a sudoers drop-in; removed when host leaves group.
|
||||||
|
|
||||||
|
LOCAL_SUDO_DIR="/etc/sudoers.d"
|
||||||
|
LOCAL_SUDO_STATE="$STATE_DIR/local-sudo-users"
|
||||||
|
[[ -f "$LOCAL_SUDO_STATE" ]] || touch "$LOCAL_SUDO_STATE"
|
||||||
|
|
||||||
|
for _USER in "${ACTIVE_LOCAL_SUDO_USERS[@]+"${ACTIVE_LOCAL_SUDO_USERS[@]}"}"; do
|
||||||
|
_DROPIN="$LOCAL_SUDO_DIR/ansipa-local-sudo-${_USER}"
|
||||||
|
if [[ ! -f "$_DROPIN" ]]; then
|
||||||
|
log "Granting local sudo to $_USER on this device"
|
||||||
|
echo "$_USER ALL=(ALL) ALL" > "$_DROPIN"
|
||||||
|
chmod 440 "$_DROPIN"
|
||||||
|
fi
|
||||||
|
grep -qxF "$_USER" "$LOCAL_SUDO_STATE" 2>/dev/null || echo "$_USER" >> "$LOCAL_SUDO_STATE"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Revoke sudo for users no longer in any active dev_local-sudo-* group.
|
||||||
|
while IFS= read -r _OLD_USER; do
|
||||||
|
[[ -z "$_OLD_USER" ]] && continue
|
||||||
|
_still_active=false
|
||||||
|
for _U in "${ACTIVE_LOCAL_SUDO_USERS[@]+"${ACTIVE_LOCAL_SUDO_USERS[@]}"}"; do
|
||||||
|
[[ "$_U" == "$_OLD_USER" ]] && _still_active=true && break
|
||||||
|
done
|
||||||
|
if [[ "$_still_active" == false ]]; then
|
||||||
|
_DROPIN="$LOCAL_SUDO_DIR/ansipa-local-sudo-${_OLD_USER}"
|
||||||
|
if [[ -f "$_DROPIN" ]]; then
|
||||||
|
rm -f "$_DROPIN"
|
||||||
|
log "Revoked local sudo for $_OLD_USER (host left dev_local-sudo-$_OLD_USER group)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$LOCAL_SUDO_STATE"
|
||||||
|
|
||||||
|
if [[ ${#ACTIVE_LOCAL_SUDO_USERS[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${ACTIVE_LOCAL_SUDO_USERS[@]}" | sort -u > "$LOCAL_SUDO_STATE"
|
||||||
|
else
|
||||||
|
> "$LOCAL_SUDO_STATE"
|
||||||
|
fi
|
||||||
|
unset _USER _DROPIN _OLD_USER _still_active _U
|
||||||
|
|
@ -0,0 +1,396 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: dev_mon — CheckMK monitoring agent + local checks.
|
||||||
|
# dev_mon_base install agent, register host, installed-packages check
|
||||||
|
# dev_mon_malware ClamAV scan result check
|
||||||
|
# dev_mon_timeshift Timeshift snapshot age check
|
||||||
|
# dev_mon_power CPU package power via RAPL / lm-sensors
|
||||||
|
# usr_mon_logins SSH login audit check (user group, but requires dev_mon_base)
|
||||||
|
# Revert: deregisters the host and removes the agent socket when dev_mon_base leaves.
|
||||||
|
|
||||||
|
MON_LOCAL_DIR="/usr/lib/check_mk_agent/local"
|
||||||
|
MON_STATE="$STATE_DIR/mon-registered"
|
||||||
|
MON_CHECKS_STATE="$STATE_DIR/mon-checks"
|
||||||
|
MON_CMK_CREDS="$STATE_DIR/mon-cmk-creds"
|
||||||
|
[[ -f "$MON_CHECKS_STATE" ]] || touch "$MON_CHECKS_STATE"
|
||||||
|
|
||||||
|
_mon_dev_active() { local n="$1"; for _m in "${ACTIVE_MON_DEV[@]+"${ACTIVE_MON_DEV[@]}"}"; do [[ "$_m" == "$n" ]] && return 0; done; return 1; }
|
||||||
|
_mon_usr_active() { local n="$1"; for _m in "${ACTIVE_MON_USR[@]+"${ACTIVE_MON_USR[@]}"}"; do [[ "$_m" == "$n" ]] && return 0; done; return 1; }
|
||||||
|
|
||||||
|
CMK_URL="" CMK_SITE="" CMK_USER="" CMK_SECRET="" CMK_API="" CMK_HOST_PORT=""
|
||||||
|
_cmk_parse_creds() {
|
||||||
|
local _desc
|
||||||
|
_desc=$(ipa hostgroup-show dev_mon_base --all 2>/dev/null \
|
||||||
|
| awk -F': ' '/Description:/{print $2; exit}' || true)
|
||||||
|
if [[ "$_desc" =~ ^cmk://([^/]+)(/[^:]+):([^:]+):(.+)$ ]]; then
|
||||||
|
CMK_HOST_PORT="${BASH_REMATCH[1]}"
|
||||||
|
CMK_SITE="${BASH_REMATCH[2]#/}"
|
||||||
|
CMK_USER="${BASH_REMATCH[3]}"
|
||||||
|
CMK_SECRET="${BASH_REMATCH[4]}"
|
||||||
|
CMK_URL="http://${CMK_HOST_PORT}"
|
||||||
|
CMK_API="${CMK_URL}/${CMK_SITE}/check_mk/api/1.0"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_WANT_CHECKS=()
|
||||||
|
_cmk_write_check() {
|
||||||
|
local _name="$1" _body="$2"
|
||||||
|
mkdir -p "$MON_LOCAL_DIR"
|
||||||
|
printf '%s\n' "$_body" > "$MON_LOCAL_DIR/$_name"
|
||||||
|
chmod 755 "$MON_LOCAL_DIR/$_name"
|
||||||
|
_WANT_CHECKS+=("$_name")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _mon_dev_active "base"; then
|
||||||
|
_cmk_parse_creds
|
||||||
|
|
||||||
|
if [[ -z "$CMK_SECRET" ]]; then
|
||||||
|
warn "dev_mon_base: no CheckMK credentials in IPA dev_mon_base description"
|
||||||
|
warn " — run ansipa-checkmk-setup on the FreeIPA container first"
|
||||||
|
else
|
||||||
|
# ── Install CheckMK agent ─────────────────────────────────────────────
|
||||||
|
_AGENT_INSTALLED=false
|
||||||
|
if rpm -q check-mk-agent &>/dev/null || command -v check_mk_agent &>/dev/null; then
|
||||||
|
_AGENT_INSTALLED=true
|
||||||
|
else
|
||||||
|
log "dev_mon_base: downloading CheckMK agent from ${CMK_URL}"
|
||||||
|
_RPM=$(curl -sf -u "${CMK_USER}:${CMK_SECRET}" \
|
||||||
|
"${CMK_URL}/${CMK_SITE}/check_mk/agents/" 2>/dev/null \
|
||||||
|
| grep -oE 'check-mk-agent-[0-9][^"]*\.noarch\.rpm' | head -1 || true)
|
||||||
|
if [[ -n "$_RPM" ]]; then
|
||||||
|
curl -sf -u "${CMK_USER}:${CMK_SECRET}" \
|
||||||
|
"${CMK_URL}/${CMK_SITE}/check_mk/agents/${_RPM}" \
|
||||||
|
-o /tmp/cmk-agent.rpm 2>/dev/null \
|
||||||
|
&& (rpm -ivh --nodeps /tmp/cmk-agent.rpm 2>/dev/null \
|
||||||
|
|| dnf install -y /tmp/cmk-agent.rpm 2>/dev/null) \
|
||||||
|
&& _AGENT_INSTALLED=true \
|
||||||
|
|| warn "dev_mon_base: CheckMK RPM install failed"
|
||||||
|
rm -f /tmp/cmk-agent.rpm
|
||||||
|
fi
|
||||||
|
if [[ "$_AGENT_INSTALLED" == false ]]; then
|
||||||
|
curl -sf -u "${CMK_USER}:${CMK_SECRET}" \
|
||||||
|
"${CMK_URL}/${CMK_SITE}/check_mk/agents/check_mk_agent.linux" \
|
||||||
|
-o /usr/local/bin/check_mk_agent 2>/dev/null \
|
||||||
|
&& chmod 755 /usr/local/bin/check_mk_agent \
|
||||||
|
&& _AGENT_INSTALLED=true \
|
||||||
|
|| warn "dev_mon_base: shell agent download failed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$_AGENT_INSTALLED" == true ]] && [[ ! -f /etc/systemd/system/check_mk.socket ]]; then
|
||||||
|
cat > /etc/systemd/system/check_mk.socket <<'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=Check_MK agent socket (ansipa-managed)
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=6556
|
||||||
|
Accept=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sockets.target
|
||||||
|
UNIT
|
||||||
|
cat > /etc/systemd/system/check_mk@.service <<'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=Check_MK per-connection agent (ansipa-managed)
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/check_mk_agent
|
||||||
|
StandardInput=socket
|
||||||
|
StandardOutput=socket
|
||||||
|
StandardError=null
|
||||||
|
UNIT
|
||||||
|
systemctl daemon-reload
|
||||||
|
fi
|
||||||
|
if [[ "$_AGENT_INSTALLED" == true ]]; then
|
||||||
|
systemctl enable --now check_mk.socket 2>/dev/null \
|
||||||
|
&& log "dev_mon_base: check_mk.socket enabled (port 6556)" \
|
||||||
|
|| warn "dev_mon_base: could not enable check_mk.socket"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Register host in CheckMK ──────────────────────────────────────────
|
||||||
|
_HOST_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "")
|
||||||
|
_CMK_REG_URL="${CMK_API}/objects/host_config/${HOST_FQDN}"
|
||||||
|
_ALREADY_REG=false
|
||||||
|
curl -sf -o /dev/null \
|
||||||
|
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
|
||||||
|
"$_CMK_REG_URL" 2>/dev/null && _ALREADY_REG=true || true
|
||||||
|
|
||||||
|
if [[ "$_ALREADY_REG" == false ]]; then
|
||||||
|
_reg_body="{\"host_name\":\"${HOST_FQDN}\",\"folder\":\"/ansipa\""
|
||||||
|
[[ -n "$_HOST_IP" ]] && _reg_body+=",\"attributes\":{\"ipaddress\":\"${_HOST_IP}\"}"
|
||||||
|
_reg_body+="}"
|
||||||
|
_reg_http=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-X POST "${CMK_API}/domain-types/host_config/collections/all" \
|
||||||
|
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-d "$_reg_body" 2>/dev/null || echo "000")
|
||||||
|
if [[ "$_reg_http" =~ ^2 ]]; then
|
||||||
|
log "dev_mon_base: host ${HOST_FQDN} registered in CheckMK"
|
||||||
|
curl -sf \
|
||||||
|
-X POST "${CMK_API}/domain-types/service_discovery_run/actions/start/invoke" \
|
||||||
|
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-d "{\"host_name\":\"${HOST_FQDN}\",\"mode\":\"refresh\"}" \
|
||||||
|
>/dev/null 2>&1 || true
|
||||||
|
curl -sf \
|
||||||
|
-X POST "${CMK_API}/domain-types/activation_run/actions/activate-changes/invoke" \
|
||||||
|
-H "Authorization: Bearer ${CMK_USER} ${CMK_SECRET}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-d '{"redirect":false,"sites":[],"force_foreign_changes":true}' \
|
||||||
|
>/dev/null 2>&1 || true
|
||||||
|
printf '%s\n' "${CMK_API}" > "$MON_STATE"
|
||||||
|
else
|
||||||
|
warn "dev_mon_base: host registration failed (HTTP ${_reg_http})"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "dev_mon_base: ${HOST_FQDN} already registered in CheckMK"
|
||||||
|
printf '%s\n' "${CMK_API}" > "$MON_STATE"
|
||||||
|
fi
|
||||||
|
unset _HOST_IP _CMK_REG_URL _ALREADY_REG _reg_body _reg_http
|
||||||
|
|
||||||
|
# ── cmk-agent-ctl: register for TLS/push transport ───────────────────
|
||||||
|
if command -v cmk-agent-ctl &>/dev/null; then
|
||||||
|
_CMK_RECV_HOST=$(echo "$CMK_URL" | sed 's|http://||;s|:.*||')
|
||||||
|
_CMK_CTL_STATUS_FILE="$STATE_DIR/mon-cmk-ctl-mode"
|
||||||
|
_ALREADY_CTL=$(cmk-agent-ctl status 2>/dev/null | grep -c "${_CMK_RECV_HOST}/${CMK_SITE}" || true)
|
||||||
|
if [[ "${_ALREADY_CTL:-0}" -eq 0 ]]; then
|
||||||
|
cmk-agent-ctl register \
|
||||||
|
--server "${_CMK_RECV_HOST}:8000" \
|
||||||
|
--site "${CMK_SITE}" \
|
||||||
|
--user "${CMK_USER}" \
|
||||||
|
--password "${CMK_SECRET}" \
|
||||||
|
--hostname "${HOST_FQDN}" \
|
||||||
|
--trust-cert &>/dev/null \
|
||||||
|
&& log "dev_mon_base: cmk-agent-ctl registered with ${_CMK_RECV_HOST}:8000" \
|
||||||
|
|| warn "dev_mon_base: cmk-agent-ctl registration failed (non-fatal)"
|
||||||
|
fi
|
||||||
|
_CTL_MODE=$(cmk-agent-ctl status 2>/dev/null | awk '/Connection mode:/{print $NF; exit}')
|
||||||
|
echo "$_CTL_MODE" > "$_CMK_CTL_STATUS_FILE" 2>/dev/null || true
|
||||||
|
if [[ "$_CTL_MODE" == "push-agent" ]]; then
|
||||||
|
log "dev_mon_base: push mode active — setting up push timer"
|
||||||
|
cmk-agent-ctl push 2>/dev/null || true
|
||||||
|
if [[ ! -f /etc/systemd/system/cmk-agent-push.timer ]]; then
|
||||||
|
cat > /etc/systemd/system/cmk-agent-push.service <<'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=CheckMK agent push (ansipa-managed)
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/bin/cmk-agent-ctl push
|
||||||
|
UNIT
|
||||||
|
cat > /etc/systemd/system/cmk-agent-push.timer <<'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=CheckMK agent push timer (ansipa-managed)
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=30s
|
||||||
|
OnUnitActiveSec=60s
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
UNIT
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now cmk-agent-push.timer 2>/dev/null \
|
||||||
|
&& log "dev_mon_base: cmk-agent-push.timer enabled (push every 60s)" \
|
||||||
|
|| warn "dev_mon_base: could not enable push timer"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
unset _CMK_RECV_HOST _ALREADY_CTL _CTL_MODE _CMK_CTL_STATUS_FILE
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Persist CMK credentials for push scripts ──────────────────────────
|
||||||
|
printf 'CMK_API=%q\nCMK_USER=%q\nCMK_SECRET=%q\n' \
|
||||||
|
"$CMK_API" "$CMK_USER" "$CMK_SECRET" > "$MON_CMK_CREDS"
|
||||||
|
chmod 600 "$MON_CMK_CREDS"
|
||||||
|
|
||||||
|
# ── dev_mon_base: installed-packages local check ──────────────────────
|
||||||
|
_cmk_write_check "ansipa_packages" '#!/bin/bash
|
||||||
|
PKG_COUNT=0
|
||||||
|
RECENT="n/a"
|
||||||
|
if command -v rpm &>/dev/null; then
|
||||||
|
PKG_COUNT=$(rpm -qa --qf "%{NAME}\n" 2>/dev/null | wc -l)
|
||||||
|
RECENT=$(rpm -qa --qf "%{INSTALLTIME} %{NAME}\n" 2>/dev/null | sort -rn | head -5 | awk "{print \$2}" | tr "\n" "," | sed "s/,\$//")
|
||||||
|
elif command -v dpkg &>/dev/null; then
|
||||||
|
PKG_COUNT=$(dpkg --list 2>/dev/null | grep -c "^ii")
|
||||||
|
RECENT=$(grep " install " /var/log/dpkg.log 2>/dev/null | tail -5 | awk "{print \$4}" | tr "\n" "," | sed "s/,\$//")
|
||||||
|
elif command -v pacman &>/dev/null; then
|
||||||
|
PKG_COUNT=$(pacman -Qq 2>/dev/null | wc -l)
|
||||||
|
RECENT=$(grep "\[ALPM\] installed" /var/log/pacman.log 2>/dev/null | tail -5 | awk "{print \$4}" | tr "\n" "," | sed "s/,\$//")
|
||||||
|
fi
|
||||||
|
echo "0 Ansipa_Packages packages=${PKG_COUNT} ${PKG_COUNT} packages installed. Recent: ${RECENT}"'
|
||||||
|
|
||||||
|
# ── dev_mon_malware: ClamAV scan result check ─────────────────────────
|
||||||
|
if _mon_dev_active "malware"; then
|
||||||
|
_cmk_write_check "ansipa_clamav" '#!/bin/bash
|
||||||
|
LOG=/var/log/ansipa-security-scan.log
|
||||||
|
CLAMDB=/var/lib/clamav/main.cvd
|
||||||
|
[[ -f /var/lib/clamav/main.cld ]] && CLAMDB=/var/lib/clamav/main.cld
|
||||||
|
if [[ ! -f "$LOG" ]]; then
|
||||||
|
echo "3 Ansipa_ClamAV - No scan log found. Add host to dev_security-scan first."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
LAST_DATE=$(grep "^=== ansipa-security-scan:" "$LOG" 2>/dev/null | tail -1 \
|
||||||
|
| grep -oE "[0-9]{4}-[0-9]{2}-[0-9]{2}" || echo "")
|
||||||
|
INFECTED=$(grep -c "FOUND$" "$LOG" 2>/dev/null) || INFECTED=0
|
||||||
|
DB_DAYS=999
|
||||||
|
if [[ -f "$CLAMDB" ]]; then
|
||||||
|
DB_DAYS=$(( ( $(date +%s) - $(stat -c %Y "$CLAMDB" 2>/dev/null || echo 0) ) / 86400 ))
|
||||||
|
fi
|
||||||
|
PERF="infected=${INFECTED};0;0 db_age_days=${DB_DAYS};7;30"
|
||||||
|
if [[ -z "$LAST_DATE" ]]; then
|
||||||
|
echo "1 Ansipa_ClamAV ${PERF} No scan found yet"
|
||||||
|
elif [[ "$INFECTED" -gt 0 ]]; then
|
||||||
|
echo "2 Ansipa_ClamAV ${PERF} INFECTED: ${INFECTED} threat(s) found — last scan: ${LAST_DATE}"
|
||||||
|
elif [[ "$DB_DAYS" -gt 7 ]]; then
|
||||||
|
echo "1 Ansipa_ClamAV ${PERF} DB stale (${DB_DAYS}d). Last scan: ${LAST_DATE}"
|
||||||
|
else
|
||||||
|
echo "0 Ansipa_ClamAV ${PERF} Clean — last scan: ${LAST_DATE}, DB age: ${DB_DAYS}d"
|
||||||
|
fi'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── dev_mon_timeshift: snapshot age check ─────────────────────────────
|
||||||
|
if _mon_dev_active "timeshift"; then
|
||||||
|
_cmk_write_check "ansipa_timeshift" '#!/bin/bash
|
||||||
|
if ! command -v timeshift &>/dev/null; then
|
||||||
|
echo "3 Ansipa_Timeshift - Timeshift not installed on this host"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
LAST=$(timeshift --list 2>/dev/null | grep -E "^\s*[0-9]+" \
|
||||||
|
| awk "{print \$3, \$4}" | tail -1 || echo "")
|
||||||
|
if [[ -z "$LAST" ]]; then
|
||||||
|
echo "2 Ansipa_Timeshift last_backup_days=9999;5;10 No Timeshift snapshots found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
LAST_TS=$(date -d "$LAST" +%s 2>/dev/null || echo 0)
|
||||||
|
DAYS=$(( ( $(date +%s) - LAST_TS ) / 86400 ))
|
||||||
|
PERF="last_backup_days=${DAYS};5;10"
|
||||||
|
if [[ "$DAYS" -ge 10 ]]; then
|
||||||
|
echo "2 Ansipa_Timeshift ${PERF} Last backup: ${DAYS}d ago (${LAST}) — CRIT"
|
||||||
|
elif [[ "$DAYS" -ge 5 ]]; then
|
||||||
|
echo "1 Ansipa_Timeshift ${PERF} Last backup: ${DAYS}d ago (${LAST}) — WARN"
|
||||||
|
else
|
||||||
|
echo "0 Ansipa_Timeshift ${PERF} Last backup: ${DAYS}d ago (${LAST})"
|
||||||
|
fi'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── dev_mon_power: CPU package power via RAPL / lm-sensors ──────────
|
||||||
|
if _mon_dev_active "power"; then
|
||||||
|
_cmk_write_check "ansipa_power" '#!/bin/bash
|
||||||
|
RAPL=/sys/class/powercap/intel-rapl/intel-rapl:0/energy_uj
|
||||||
|
POWER_W=0; SOURCE=""
|
||||||
|
if [[ -r "$RAPL" ]]; then
|
||||||
|
E1=$(cat "$RAPL" 2>/dev/null || echo 0)
|
||||||
|
sleep 1
|
||||||
|
E2=$(cat "$RAPL" 2>/dev/null || echo 0)
|
||||||
|
if [[ "$E2" -gt "$E1" ]]; then
|
||||||
|
POWER_W=$(( (E2 - E1) / 1000000 ))
|
||||||
|
SOURCE="RAPL"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$SOURCE" ]] && command -v sensors &>/dev/null; then
|
||||||
|
W=$(sensors 2>/dev/null | grep -iE "package.*power|cpu.*power" \
|
||||||
|
| grep -oP "[0-9]+\.[0-9]+" | head -1 | cut -d. -f1 || echo 0)
|
||||||
|
[[ "${W:-0}" -gt 0 ]] && POWER_W="$W" && SOURCE="lm-sensors"
|
||||||
|
fi
|
||||||
|
if [[ -z "$SOURCE" ]]; then
|
||||||
|
echo "3 Ansipa_Power power_w=0 Power monitoring unavailable (needs Intel RAPL or lm-sensors)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
PERF="power_w=${POWER_W};65;95"
|
||||||
|
if [[ "$POWER_W" -ge 95 ]]; then
|
||||||
|
echo "2 Ansipa_Power ${PERF} CPU power: ${POWER_W}W via ${SOURCE} — CRIT"
|
||||||
|
elif [[ "$POWER_W" -ge 65 ]]; then
|
||||||
|
echo "1 Ansipa_Power ${PERF} CPU power: ${POWER_W}W via ${SOURCE} — WARN"
|
||||||
|
else
|
||||||
|
echo "0 Ansipa_Power ${PERF} CPU power: ${POWER_W}W via ${SOURCE}"
|
||||||
|
fi'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── usr_mon_logins: SSH login audit (requires dev_mon_base agent) ─────
|
||||||
|
if _mon_usr_active "logins"; then
|
||||||
|
_cmk_write_check "ansipa_logins" '#!/bin/bash
|
||||||
|
count_journal() {
|
||||||
|
local pat="$1"
|
||||||
|
journalctl -u sshd --since "24 hours ago" --no-pager -q 2>/dev/null \
|
||||||
|
| grep -c "$pat" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
count_secure() {
|
||||||
|
local pat="$1"
|
||||||
|
{ grep "$(date "+%b %e")" /var/log/secure 2>/dev/null
|
||||||
|
grep "$(date -d yesterday "+%b %e" 2>/dev/null || true)" /var/log/secure 2>/dev/null; } \
|
||||||
|
| grep -c "$pat" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
if command -v journalctl &>/dev/null && journalctl -u sshd --since "24 hours ago" -q &>/dev/null; then
|
||||||
|
ACCEPTED=$(count_journal "Accepted ")
|
||||||
|
FAILED=$(count_journal "Failed ")
|
||||||
|
INVALID=$(count_journal "Invalid user ")
|
||||||
|
else
|
||||||
|
ACCEPTED=$(count_secure "sshd.*Accepted")
|
||||||
|
FAILED=$(count_secure "sshd.*Failed")
|
||||||
|
INVALID=$(count_secure "sshd.*Invalid user")
|
||||||
|
fi
|
||||||
|
PERF="successful=${ACCEPTED};; failed=${FAILED};10;50 invalid=${INVALID};5;20"
|
||||||
|
MSG="Logins 24h: ${ACCEPTED} ok / ${FAILED} failed / ${INVALID} invalid user"
|
||||||
|
if [[ "$FAILED" -ge 50 ]] || [[ "$INVALID" -ge 20 ]]; then
|
||||||
|
echo "2 Ansipa_Logins ${PERF} HIGH ACTIVITY — ${MSG}"
|
||||||
|
elif [[ "$FAILED" -ge 10 ]] || [[ "$INVALID" -ge 5 ]]; then
|
||||||
|
echo "1 Ansipa_Logins ${PERF} ELEVATED — ${MSG}"
|
||||||
|
else
|
||||||
|
echo "0 Ansipa_Logins ${PERF} ${MSG}"
|
||||||
|
fi'
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi # CMK_SECRET non-empty
|
||||||
|
|
||||||
|
else
|
||||||
|
# ── Revert: host left dev_mon_base → deregister and clean up ─────────────
|
||||||
|
if [[ -f "$MON_STATE" ]]; then
|
||||||
|
_OLD_CMK_API=$(cat "$MON_STATE" 2>/dev/null || true)
|
||||||
|
if [[ -n "$_OLD_CMK_API" ]] && [[ -f "$MON_CMK_CREDS" ]]; then
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$MON_CMK_CREDS" 2>/dev/null || true
|
||||||
|
_del_http=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-X DELETE "${_OLD_CMK_API}/objects/host_config/${HOST_FQDN}" \
|
||||||
|
-H "Authorization: Bearer ${CMK_USER:-} ${CMK_SECRET:-}" \
|
||||||
|
-H "Accept: application/json" 2>/dev/null || echo "000")
|
||||||
|
if [[ "$_del_http" =~ ^(2|404) ]]; then
|
||||||
|
log "Deregistered ${HOST_FQDN} from CheckMK (HTTP ${_del_http})"
|
||||||
|
curl -sf \
|
||||||
|
-X POST "${_OLD_CMK_API}/domain-types/activation_run/actions/activate-changes/invoke" \
|
||||||
|
-H "Authorization: Bearer ${CMK_USER:-} ${CMK_SECRET:-}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json" \
|
||||||
|
-d '{"redirect":false,"sites":[],"force_foreign_changes":true}' \
|
||||||
|
>/dev/null 2>&1 || true
|
||||||
|
else
|
||||||
|
warn "CheckMK deregistration returned HTTP ${_del_http} — may need manual cleanup"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -f "$MON_STATE" "$MON_CMK_CREDS"
|
||||||
|
systemctl disable --now check_mk.socket 2>/dev/null || true
|
||||||
|
systemctl disable --now cmk-agent-push.timer 2>/dev/null || true
|
||||||
|
command -v cmk-agent-ctl &>/dev/null && cmk-agent-ctl delete-all 2>/dev/null || true
|
||||||
|
log "dev_mon_base reverted: check_mk.socket disabled, host deregistered"
|
||||||
|
unset _OLD_CMK_API _del_http
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Cleanup: remove local check scripts for groups no longer active ───────────
|
||||||
|
while IFS= read -r _old_check; do
|
||||||
|
[[ -z "$_old_check" ]] && continue
|
||||||
|
_still_wanted=false
|
||||||
|
for _wc in "${_WANT_CHECKS[@]+"${_WANT_CHECKS[@]}"}"; do
|
||||||
|
[[ "$_wc" == "$_old_check" ]] && _still_wanted=true && break
|
||||||
|
done
|
||||||
|
if [[ "$_still_wanted" == false ]] && [[ -f "$MON_LOCAL_DIR/$_old_check" ]]; then
|
||||||
|
rm -f "$MON_LOCAL_DIR/$_old_check"
|
||||||
|
log "Removed CheckMK local check: $_old_check (group left)"
|
||||||
|
fi
|
||||||
|
done < "$MON_CHECKS_STATE"
|
||||||
|
unset _old_check _still_wanted _wc
|
||||||
|
|
||||||
|
if [[ ${#_WANT_CHECKS[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${_WANT_CHECKS[@]+"${_WANT_CHECKS[@]}"}" | sort -u > "$MON_CHECKS_STATE"
|
||||||
|
else
|
||||||
|
> "$MON_CHECKS_STATE"
|
||||||
|
fi
|
||||||
|
unset _WANT_CHECKS _AGENT_INSTALLED
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: dev_no-local-users — lock local account passwords so only FreeIPA accounts can auth.
|
||||||
|
# Applied when host is member of dev_no-local-users host-group.
|
||||||
|
# Revert: unlocks every account that was locked by this policy.
|
||||||
|
|
||||||
|
NO_LOCAL_USERS_STATE="$STATE_DIR/no-local-users"
|
||||||
|
|
||||||
|
_apply_no_local_users() {
|
||||||
|
log "Applying no_local_users policy — locking local account passwords"
|
||||||
|
[[ -f "$NO_LOCAL_USERS_STATE" ]] || touch "$NO_LOCAL_USERS_STATE"
|
||||||
|
|
||||||
|
while IFS=: read -r uname _ uid _; do
|
||||||
|
[[ "$uid" =~ ^[0-9]+$ ]] || continue
|
||||||
|
{ [[ "$uid" == "0" ]] || [[ "$uid" -ge 1000 ]]; } || continue
|
||||||
|
|
||||||
|
grep -qxF "$uname" "$NO_LOCAL_USERS_STATE" 2>/dev/null && continue
|
||||||
|
|
||||||
|
local hash
|
||||||
|
hash=$(getent shadow "$uname" 2>/dev/null | cut -d: -f2 || true)
|
||||||
|
[[ -z "$hash" || "$hash" == '!'* || "$hash" == '*'* ]] && continue
|
||||||
|
|
||||||
|
if passwd -l "$uname" &>/dev/null; then
|
||||||
|
echo "$uname" >> "$NO_LOCAL_USERS_STATE"
|
||||||
|
log "Locked local account: $uname"
|
||||||
|
else
|
||||||
|
warn "Failed to lock local account: $uname"
|
||||||
|
fi
|
||||||
|
done < /etc/passwd
|
||||||
|
}
|
||||||
|
|
||||||
|
_revert_no_local_users() {
|
||||||
|
[[ -f "$NO_LOCAL_USERS_STATE" ]] || return 0
|
||||||
|
log "Reverting no_local_users policy — unlocking previously locked accounts"
|
||||||
|
while IFS= read -r uname; do
|
||||||
|
[[ -z "$uname" ]] && continue
|
||||||
|
if passwd -u "$uname" &>/dev/null; then
|
||||||
|
log "Unlocked local account: $uname"
|
||||||
|
else
|
||||||
|
warn "Failed to unlock local account: $uname (may have been removed)"
|
||||||
|
fi
|
||||||
|
done < "$NO_LOCAL_USERS_STATE"
|
||||||
|
> "$NO_LOCAL_USERS_STATE"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$WANT_NO_LOCAL_USERS" == true ]]; then
|
||||||
|
_apply_no_local_users
|
||||||
|
else
|
||||||
|
if [[ -f "$NO_LOCAL_USERS_STATE" ]] && [[ -s "$NO_LOCAL_USERS_STATE" ]]; then
|
||||||
|
_revert_no_local_users
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: dev_security-scan — daily ClamAV + rkhunter + chkrootkit scans + SMB upload at 02:00.
|
||||||
|
# Applied when host is member of dev_security-scan host-group.
|
||||||
|
|
||||||
|
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 + SMB upload.
|
||||||
|
# Managed by ansipa-enforce-policies — do not edit manually.
|
||||||
|
LOG=/var/log/ansipa-security-scan.log
|
||||||
|
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
{
|
||||||
|
echo "=== ansipa-security-scan: $DATE $HOSTNAME ==="
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# ── Upload to server SMB share ────────────────────────────────────────────────
|
||||||
|
IPA_SERVER=$(awk '/^server[[:space:]]*=/{print $3}' /etc/ipa/default.conf 2>/dev/null || echo "")
|
||||||
|
if [[ -n "$IPA_SERVER" ]] && [[ -f /etc/ansipa-smb.creds ]] && command -v smbclient &>/dev/null; then
|
||||||
|
# Create host archive dir (mkdir is idempotent; errors suppressed).
|
||||||
|
smbclient "//$IPA_SERVER/ansipa-scans" -A /etc/ansipa-smb.creds \
|
||||||
|
-c "mkdir archive; mkdir archive\\$HOSTNAME; put $LOG archive\\$HOSTNAME\\$DATE.log" \
|
||||||
|
>> "$LOG" 2>&1 \
|
||||||
|
&& echo "[ansipa] Scan results uploaded to $IPA_SERVER/ansipa-scans/archive/$HOSTNAME/$DATE.log" >> "$LOG" \
|
||||||
|
|| echo "[ansipa][WARN] SMB upload failed — results remain local at $LOG" >> "$LOG"
|
||||||
|
else
|
||||||
|
echo "[ansipa] SMB upload skipped (no credentials or smbclient not found)." >> "$LOG"
|
||||||
|
fi
|
||||||
|
SCAN
|
||||||
|
chmod 755 "$SCAN_SCRIPT"
|
||||||
|
|
||||||
|
if [[ ! -f "$SCAN_CRON" ]]; then
|
||||||
|
log "Enabling daily security scans (ClamAV / rkhunter / chkrootkit)"
|
||||||
|
cat > "$SCAN_CRON" <<'CRON'
|
||||||
|
# ansipa-dev_security-scan: managed by ansipa-enforce-policies — do not edit manually.
|
||||||
|
# Install scan tools by adding the host to the dev_mod_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 dev_security-scan group)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: dev_ssh — distribute IPA user SSH public keys to authorized_keys.
|
||||||
|
# dev_ssh_<userid>: writes the IPA user's keys in an ansipa-managed section.
|
||||||
|
# Leaving the host-group removes only the ansipa section; manual keys are preserved.
|
||||||
|
|
||||||
|
SSH_KEY_STATE="$STATE_DIR/ssh-keys"
|
||||||
|
[[ -f "$SSH_KEY_STATE" ]] || touch "$SSH_KEY_STATE"
|
||||||
|
|
||||||
|
_ssh_write_keys() {
|
||||||
|
local _u="$1" _h="$2"; shift 2
|
||||||
|
local _auth="$_h/.ssh/authorized_keys"
|
||||||
|
mkdir -p "$_h/.ssh"
|
||||||
|
chmod 700 "$_h/.ssh"
|
||||||
|
[[ -f "$_auth" ]] || touch "$_auth"
|
||||||
|
local _rest
|
||||||
|
_rest=$(awk '
|
||||||
|
/^# BEGIN ansipa-ssh-managed$/ { skip=1; next }
|
||||||
|
skip && /^# END ansipa-ssh-managed$/ { skip=0; next }
|
||||||
|
!skip
|
||||||
|
' "$_auth" 2>/dev/null || true)
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
{ [[ -n "$_rest" ]] && printf '%s\n' "$_rest"; \
|
||||||
|
echo "# BEGIN ansipa-ssh-managed"; \
|
||||||
|
printf '%s\n' "$@"; \
|
||||||
|
echo "# END ansipa-ssh-managed"; } > "$_auth"
|
||||||
|
else
|
||||||
|
[[ -n "$_rest" ]] && printf '%s\n' "$_rest" > "$_auth" || > "$_auth"
|
||||||
|
fi
|
||||||
|
chmod 600 "$_auth"
|
||||||
|
chown "$_u:$_u" "$_h/.ssh" "$_auth" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
declare -A _SSH_APPLIED
|
||||||
|
|
||||||
|
for _SSH_UID in "${ACTIVE_SSH_USERS[@]+"${ACTIVE_SSH_USERS[@]}"}"; do
|
||||||
|
ipa user-show "$_SSH_UID" &>/dev/null || {
|
||||||
|
warn "dev_ssh_${_SSH_UID}: IPA user '$_SSH_UID' not found — skipping"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_SSH_KEYS=()
|
||||||
|
mapfile -t _SSH_KEYS < <(
|
||||||
|
ipa user-show "$_SSH_UID" --all 2>/dev/null \
|
||||||
|
| grep "^ SSH public key:" | grep -v "fingerprint" \
|
||||||
|
| sed 's/^ SSH public key: //' || true
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ ${#_SSH_KEYS[@]} -eq 0 ]]; then
|
||||||
|
warn "dev_ssh_${_SSH_UID}: '$_SSH_UID' has no SSH keys in IPA — add via:"
|
||||||
|
warn " ipa user-mod $_SSH_UID --sshpubkey='ssh-ed25519 AAAA...'"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
_SSH_HOME=$(getent passwd "$_SSH_UID" 2>/dev/null | cut -d: -f6 || true)
|
||||||
|
if [[ -z "$_SSH_HOME" ]]; then
|
||||||
|
warn "dev_ssh_${_SSH_UID}: '$_SSH_UID' not resolvable via getent — is SSSD running?"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$_SSH_HOME" ]]; then
|
||||||
|
log "Creating home dir for $_SSH_UID: $_SSH_HOME"
|
||||||
|
mkdir -p "$_SSH_HOME"
|
||||||
|
chown "$_SSH_UID:$_SSH_UID" "$_SSH_HOME" 2>/dev/null || true
|
||||||
|
chmod 700 "$_SSH_HOME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_ssh_write_keys "$_SSH_UID" "$_SSH_HOME" "${_SSH_KEYS[@]}"
|
||||||
|
log "SSH keys for $_SSH_UID: ${#_SSH_KEYS[@]} key(s) written to $_SSH_HOME/.ssh/authorized_keys"
|
||||||
|
_SSH_APPLIED["$_SSH_UID"]=1
|
||||||
|
grep -qxF "$_SSH_UID" "$SSH_KEY_STATE" 2>/dev/null || echo "$_SSH_UID" >> "$SSH_KEY_STATE"
|
||||||
|
done
|
||||||
|
unset _SSH_UID _SSH_KEYS _SSH_HOME
|
||||||
|
|
||||||
|
# Revert: remove ansipa keys for users whose dev_ssh_* group was removed from this host.
|
||||||
|
_NEW_SSH_STATE=()
|
||||||
|
while IFS= read -r _OLD_SSH_UID; do
|
||||||
|
[[ -z "$_OLD_SSH_UID" ]] && continue
|
||||||
|
if [[ -n "${_SSH_APPLIED[$_OLD_SSH_UID]+x}" ]]; then
|
||||||
|
_NEW_SSH_STATE+=("$_OLD_SSH_UID")
|
||||||
|
else
|
||||||
|
_OLD_SSH_HOME=$(getent passwd "$_OLD_SSH_UID" 2>/dev/null | cut -d: -f6 || true)
|
||||||
|
if [[ -n "$_OLD_SSH_HOME" ]]; then
|
||||||
|
_ssh_write_keys "$_OLD_SSH_UID" "$_OLD_SSH_HOME"
|
||||||
|
log "Removed ansipa SSH keys for $_OLD_SSH_UID (host left dev_ssh_${_OLD_SSH_UID})"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$SSH_KEY_STATE"
|
||||||
|
unset _OLD_SSH_UID _OLD_SSH_HOME
|
||||||
|
|
||||||
|
if [[ ${#_NEW_SSH_STATE[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${_NEW_SSH_STATE[@]}" | sort -u > "$SSH_KEY_STATE"
|
||||||
|
else
|
||||||
|
> "$SSH_KEY_STATE"
|
||||||
|
fi
|
||||||
|
unset _SSH_APPLIED _NEW_SSH_STATE
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: dev_timeshift-backup — daily Timeshift snapshot at 03:00.
|
||||||
|
# Applied when host is member of dev_timeshift-backup host-group.
|
||||||
|
|
||||||
|
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 dev_mod_timeshift first. Cron will be installed anyway."
|
||||||
|
fi
|
||||||
|
log "Enabling daily Timeshift backups"
|
||||||
|
cat > "$TIMESHIFT_CRON" <<'CRON'
|
||||||
|
# ansipa-dev_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 dev_timeshift-backup group)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: usr_admin — global sudo grant for FreeIPA usr_admin user group.
|
||||||
|
# When the IPA group exists, a sudoers drop-in grants full sudo (SSSD-resolved).
|
||||||
|
# The drop-in is removed when the group is deleted from FreeIPA.
|
||||||
|
|
||||||
|
USR_ADMIN_DROPIN="/etc/sudoers.d/ansipa-usr-admin"
|
||||||
|
USR_ADMIN_STATE="$STATE_DIR/usr-admin"
|
||||||
|
|
||||||
|
if [[ "$WANT_USR_ADMIN" == true ]]; then
|
||||||
|
if [[ ! -f "$USR_ADMIN_DROPIN" ]]; then
|
||||||
|
log "Installing usr_admin sudoers drop-in"
|
||||||
|
printf '%%usr_admin ALL=(ALL:ALL) ALL\n' > "$USR_ADMIN_DROPIN"
|
||||||
|
chmod 440 "$USR_ADMIN_DROPIN"
|
||||||
|
fi
|
||||||
|
touch "$USR_ADMIN_STATE"
|
||||||
|
else
|
||||||
|
if [[ -f "$USR_ADMIN_STATE" ]] || [[ -f "$USR_ADMIN_DROPIN" ]]; then
|
||||||
|
rm -f "$USR_ADMIN_DROPIN" "$USR_ADMIN_STATE"
|
||||||
|
log "Removed usr_admin sudoers drop-in (IPA group no longer exists)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: usr_block-binary — prevent IPA user group members from running named binaries.
|
||||||
|
# usr_block-binary-<name>: installs a PATH-priority wrapper that checks group membership
|
||||||
|
# at runtime via SSSD/id. __ in the suffix decodes to . (Flatpak app ID support).
|
||||||
|
# Removing the IPA group causes the wrapper to be cleaned up on the next run.
|
||||||
|
|
||||||
|
BLOCK_STATE="$STATE_DIR/blocked-binaries"
|
||||||
|
[[ -f "$BLOCK_STATE" ]] || touch "$BLOCK_STATE"
|
||||||
|
|
||||||
|
_in_block_list() {
|
||||||
|
local needle="$1"
|
||||||
|
for b in "${ACTIVE_BLOCK_BINARIES[@]+"${ACTIVE_BLOCK_BINARIES[@]}"}"; do
|
||||||
|
[[ "$b" == "$needle" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for _idx in "${!ACTIVE_BLOCK_BINARIES[@]}"; do
|
||||||
|
BIN="${ACTIVE_BLOCK_BINARIES[$_idx]}"
|
||||||
|
IPA_GRP="${ACTIVE_BLOCK_IPA_GROUPS[$_idx]}"
|
||||||
|
WRAPPER="$BLOCK_DIR/$BIN"
|
||||||
|
if [[ ! -f "$WRAPPER" ]] \
|
||||||
|
|| ! grep -q "blocked by ansipa policy" "$WRAPPER" 2>/dev/null \
|
||||||
|
|| ! grep -qF "$IPA_GRP" "$WRAPPER" 2>/dev/null; then
|
||||||
|
log "Installing user-aware block wrapper: $BIN (group: $IPA_GRP)"
|
||||||
|
cat > "$WRAPPER" <<WRAPPER
|
||||||
|
#!/bin/bash
|
||||||
|
# blocked by ansipa policy (user-based)
|
||||||
|
if id -Gn 2>/dev/null | tr ' ' '\n' | grep -qxF "${IPA_GRP}"; then
|
||||||
|
echo "[ansipa-policies] '${BIN}' is blocked by system policy for your account." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
_real=\$(PATH="/usr/bin:/usr/sbin:/bin:/sbin:/usr/local/sbin:/opt/bin:/var/lib/flatpak/exports/bin:/usr/share/flatpak/exports/bin" command -v "${BIN}" 2>/dev/null)
|
||||||
|
[[ -n "\$_real" ]] && exec "\$_real" "\$@"
|
||||||
|
command -v flatpak &>/dev/null && exec flatpak run "${BIN}" "\$@" 2>/dev/null
|
||||||
|
echo "${BIN}: command not found" >&2
|
||||||
|
exit 127
|
||||||
|
WRAPPER
|
||||||
|
chmod 755 "$WRAPPER"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
unset _idx
|
||||||
|
|
||||||
|
# Remove wrappers whose IPA user group no longer exists.
|
||||||
|
while IFS= read -r OLD_BIN; do
|
||||||
|
[[ -z "$OLD_BIN" ]] && continue
|
||||||
|
if ! _in_block_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 binary block wrapper: $OLD_BIN"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$BLOCK_STATE"
|
||||||
|
|
||||||
|
if [[ ${#ACTIVE_BLOCK_BINARIES[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${ACTIVE_BLOCK_BINARIES[@]}" | sort -u > "$BLOCK_STATE"
|
||||||
|
else
|
||||||
|
> "$BLOCK_STATE"
|
||||||
|
fi
|
||||||
|
unset BIN IPA_GRP WRAPPER OLD_BIN
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: usr_policystore — auto-mount the ansipa policy store for admins.
|
||||||
|
# When the IPA user group usr_smb_adm_policystore exists, members who are
|
||||||
|
# logged in get the ansipa-policystore SMB share mounted at ~/policystore (rw).
|
||||||
|
# The mount is tracked in state and removed when the group is deleted or the
|
||||||
|
# user is no longer a member.
|
||||||
|
# Credential: cifs://<host>:smb-policystore:<pass> in the IPA group description,
|
||||||
|
# written by ansipa-smb-setup.sh.
|
||||||
|
|
||||||
|
PS_MOUNT_STATE="$STATE_DIR/policystore-mounts"
|
||||||
|
PS_CREDS_DIR="$STATE_DIR/smb-creds"
|
||||||
|
PS_CREDS_FILE="$PS_CREDS_DIR/policystore.creds"
|
||||||
|
[[ -f "$PS_MOUNT_STATE" ]] || touch "$PS_MOUNT_STATE"
|
||||||
|
mkdir -p "$PS_CREDS_DIR"
|
||||||
|
chmod 700 "$PS_CREDS_DIR"
|
||||||
|
|
||||||
|
_ps_grp_exists=false
|
||||||
|
grep -qxF "usr_smb_adm_policystore" <<< "$_ALL_USER_GROUPS" && _ps_grp_exists=true
|
||||||
|
|
||||||
|
_ps_member() { id -nG "$1" 2>/dev/null | tr ' ' '\n' | grep -qxF "usr_smb_adm_policystore"; }
|
||||||
|
|
||||||
|
if [[ "$_ps_grp_exists" == true ]] && command -v mount.cifs &>/dev/null; then
|
||||||
|
_PS_CRED=$(_smb_parse_cred "usr_smb_adm_policystore")
|
||||||
|
if [[ -z "$_PS_CRED" ]]; then
|
||||||
|
warn "usr_smb_adm_policystore: no cifs:// credential in IPA group description"
|
||||||
|
warn " — run ansipa-smb.service on the FreeIPA container first"
|
||||||
|
else
|
||||||
|
read -r _PS_HOST _PS_SUSER _PS_SPASS <<< "$_PS_CRED"
|
||||||
|
printf 'username=%s\npassword=%s\ndomain=WORKGROUP\n' "$_PS_SUSER" "$_PS_SPASS" \
|
||||||
|
> "$PS_CREDS_FILE"
|
||||||
|
chmod 600 "$PS_CREDS_FILE"
|
||||||
|
|
||||||
|
# Collect logged-in users.
|
||||||
|
declare -A _PS_SESS
|
||||||
|
_ps_add_sess() {
|
||||||
|
local _u
|
||||||
|
[[ "$2" == "loginctl" ]] && _u=$(awk '{print $3}' <<< "$1") \
|
||||||
|
|| _u=$(awk '{print $1}' <<< "$1")
|
||||||
|
[[ -z "$_u" || "$_u" == "root" || "$_u" == "USER" ]] && return 0
|
||||||
|
local _h; _h=$(getent passwd "$_u" | cut -d: -f6 2>/dev/null) || return 0
|
||||||
|
[[ -d "$_h" ]] && _PS_SESS["$_u"]="$_h"
|
||||||
|
}
|
||||||
|
if loginctl list-sessions --no-legend &>/dev/null; then
|
||||||
|
while IFS= read -r _L; do _ps_add_sess "$_L" "loginctl"
|
||||||
|
done < <(loginctl list-sessions --no-legend 2>/dev/null)
|
||||||
|
else
|
||||||
|
while IFS= read -r _L; do _ps_add_sess "$_L" "who"
|
||||||
|
done < <(who 2>/dev/null)
|
||||||
|
fi
|
||||||
|
unset _L
|
||||||
|
|
||||||
|
# Mount for logged-in members.
|
||||||
|
for _PS_U in "${!_PS_SESS[@]}"; do
|
||||||
|
_ps_member "$_PS_U" || continue
|
||||||
|
_PS_H="${_PS_SESS[$_PS_U]}"
|
||||||
|
_PS_MP="$_PS_H/policystore"
|
||||||
|
|
||||||
|
if mountpoint -q "$_PS_MP" 2>/dev/null; then
|
||||||
|
grep -qxF "$_PS_U" "$PS_MOUNT_STATE" 2>/dev/null || \
|
||||||
|
echo "$_PS_U" >> "$PS_MOUNT_STATE"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$_PS_MP"
|
||||||
|
chown "$_PS_U" "$_PS_MP" 2>/dev/null || true
|
||||||
|
|
||||||
|
if mount -t cifs "//${_PS_HOST}/ansipa-policystore" "$_PS_MP" \
|
||||||
|
-o "credentials=${PS_CREDS_FILE},uid=${_PS_U},gid=${_PS_U},file_mode=0664,dir_mode=02775,nounix,noserverino" \
|
||||||
|
2>/dev/null; then
|
||||||
|
log "Mounted //${_PS_HOST}/ansipa-policystore → $_PS_MP for $_PS_U"
|
||||||
|
grep -qxF "$_PS_U" "$PS_MOUNT_STATE" 2>/dev/null || \
|
||||||
|
echo "$_PS_U" >> "$PS_MOUNT_STATE"
|
||||||
|
else
|
||||||
|
warn "Failed to mount policystore for $_PS_U"
|
||||||
|
rmdir "$_PS_MP" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
unset _PS_U _PS_H _PS_MP
|
||||||
|
|
||||||
|
# Revert: unmount for users no longer in the group or group deleted.
|
||||||
|
_NEW_PS_MOUNTS=()
|
||||||
|
while IFS= read -r _OLD_PS_U; do
|
||||||
|
[[ -z "$_OLD_PS_U" ]] && continue
|
||||||
|
_OLD_PS_HOME=$(getent passwd "$_OLD_PS_U" | cut -d: -f6 2>/dev/null) || continue
|
||||||
|
_OLD_PS_MP="$_OLD_PS_HOME/policystore"
|
||||||
|
if _ps_member "$_OLD_PS_U"; then
|
||||||
|
_NEW_PS_MOUNTS+=("$_OLD_PS_U")
|
||||||
|
else
|
||||||
|
umount -l "$_OLD_PS_MP" 2>/dev/null || true
|
||||||
|
rmdir "$_OLD_PS_MP" 2>/dev/null || true
|
||||||
|
log "Unmounted policystore for $_OLD_PS_U (no longer in usr_smb_adm_policystore)"
|
||||||
|
fi
|
||||||
|
done < "$PS_MOUNT_STATE"
|
||||||
|
unset _OLD_PS_U _OLD_PS_HOME _OLD_PS_MP
|
||||||
|
|
||||||
|
if [[ ${#_NEW_PS_MOUNTS[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${_NEW_PS_MOUNTS[@]}" | sort -u > "$PS_MOUNT_STATE"
|
||||||
|
else
|
||||||
|
> "$PS_MOUNT_STATE"
|
||||||
|
fi
|
||||||
|
unset _NEW_PS_MOUNTS _PS_SESS _PS_HOST _PS_SUSER _PS_SPASS _PS_CRED
|
||||||
|
|
||||||
|
rm -f "$PS_CREDS_FILE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Group deleted — unmount for any previously-tracked users.
|
||||||
|
if [[ -s "$PS_MOUNT_STATE" ]]; then
|
||||||
|
while IFS= read -r _OLD_PS_U; do
|
||||||
|
[[ -z "$_OLD_PS_U" ]] && continue
|
||||||
|
_OLD_PS_HOME=$(getent passwd "$_OLD_PS_U" | cut -d: -f6 2>/dev/null) || continue
|
||||||
|
umount -l "$_OLD_PS_HOME/policystore" 2>/dev/null || true
|
||||||
|
rmdir "$_OLD_PS_HOME/policystore" 2>/dev/null || true
|
||||||
|
log "Unmounted policystore for $_OLD_PS_U (usr_smb_adm_policystore group deleted)"
|
||||||
|
done < "$PS_MOUNT_STATE"
|
||||||
|
> "$PS_MOUNT_STATE"
|
||||||
|
unset _OLD_PS_U _OLD_PS_HOME
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
unset _ps_grp_exists
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: usr_prt — per-user printer auto-add via FreeIPA usr_prt_<printer> groups.
|
||||||
|
# Printer URI stored in IPA group description (ipps://... or similar).
|
||||||
|
# Logged-in members get the printer added to CUPS; leaving the group revokes access.
|
||||||
|
|
||||||
|
PRT_STATE_DIR="$STATE_DIR/printer-users"
|
||||||
|
PRT_DEFS_DIR="$STATE_DIR/printer-defs"
|
||||||
|
PRT_PROFILED="/etc/profile.d/ansipa-printers.sh"
|
||||||
|
PRT_SUDO_DROPIN="/etc/sudoers.d/ansipa-printers-trigger"
|
||||||
|
mkdir -p "$PRT_STATE_DIR" "$PRT_DEFS_DIR"
|
||||||
|
|
||||||
|
_prt_read_users() {
|
||||||
|
local sf="$PRT_STATE_DIR/${1}.users"
|
||||||
|
[[ -f "$sf" ]] && grep -v '^$' "$sf" | sort -u || true
|
||||||
|
}
|
||||||
|
_prt_write_users() {
|
||||||
|
local printer="$1"; shift
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
printf '%s\n' "$@" | sort -u > "$PRT_STATE_DIR/${printer}.users"
|
||||||
|
else
|
||||||
|
> "$PRT_STATE_DIR/${printer}.users"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
_prt_apply_acl() {
|
||||||
|
local printer="$1"
|
||||||
|
local -a users
|
||||||
|
mapfile -t users < <(_prt_read_users "$printer")
|
||||||
|
if [[ ${#users[@]} -gt 0 ]]; then
|
||||||
|
local allow_list
|
||||||
|
allow_list=$(IFS=','; echo "${users[*]}")
|
||||||
|
lpadmin -p "$printer" -u "allow:${allow_list}" 2>/dev/null \
|
||||||
|
|| warn "lpadmin ACL update failed for printer $printer"
|
||||||
|
else
|
||||||
|
lpadmin -x "$printer" 2>/dev/null \
|
||||||
|
&& log "Removed printer $printer (no authorized users remaining)" \
|
||||||
|
|| warn "lpadmin -x failed for printer $printer"
|
||||||
|
rm -f "$PRT_STATE_DIR/${printer}.users"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
_prt_is_active() {
|
||||||
|
local needle="$1"
|
||||||
|
for _ap in "${ACTIVE_PRT_PRINTERS[@]+"${ACTIVE_PRT_PRINTERS[@]}"}"; do
|
||||||
|
[[ "$_ap" == "$needle" ]] && return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if command -v lpadmin &>/dev/null; then
|
||||||
|
for _i in "${!ACTIVE_PRT_PRINTERS[@]}"; do
|
||||||
|
_PRT="${ACTIVE_PRT_PRINTERS[$_i]}"
|
||||||
|
_GRP="${ACTIVE_PRT_GROUPS[$_i]}"
|
||||||
|
|
||||||
|
_URI=$(ipa group-show "$_GRP" --all 2>/dev/null \
|
||||||
|
| awk -F': ' '/Description:/{print $2; exit}' \
|
||||||
|
| grep -E '^[a-z][a-z+.-]+://' || true)
|
||||||
|
|
||||||
|
if [[ -z "$_URI" ]]; then
|
||||||
|
warn "Group $_GRP has no printer URI in its description — set it with:"
|
||||||
|
warn " ipa group-mod $_GRP --desc='ipps://printserver/printers/$_PRT'"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "$_URI" > "$PRT_DEFS_DIR/${_GRP}.uri"
|
||||||
|
|
||||||
|
mapfile -t _CUR_USERS < <(_prt_read_users "$_PRT")
|
||||||
|
_UPDATED=false
|
||||||
|
|
||||||
|
while IFS= read -r _LUSER; do
|
||||||
|
[[ -z "$_LUSER" ]] && continue
|
||||||
|
id -Gn "$_LUSER" 2>/dev/null | tr ' ' '\n' | grep -qxF "$_GRP" || continue
|
||||||
|
printf '%s\n' "${_CUR_USERS[@]+"${_CUR_USERS[@]}"}" | grep -qxF "$_LUSER" && continue
|
||||||
|
|
||||||
|
if ! lpstat -p "$_PRT" &>/dev/null; then
|
||||||
|
log "Adding printer $_PRT (URI: $_URI)"
|
||||||
|
lpadmin -p "$_PRT" -v "$_URI" -m everywhere -E 2>/dev/null \
|
||||||
|
|| lpadmin -p "$_PRT" -v "$_URI" -E 2>/dev/null \
|
||||||
|
|| { warn "lpadmin failed to add printer $_PRT"; continue; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Granting printer $_PRT access to $_LUSER"
|
||||||
|
_CUR_USERS+=("$_LUSER")
|
||||||
|
_UPDATED=true
|
||||||
|
done < <(who -u 2>/dev/null | awk '{print $1}' | sort -u)
|
||||||
|
|
||||||
|
_VALID_USERS=()
|
||||||
|
for _U in "${_CUR_USERS[@]+"${_CUR_USERS[@]}"}"; do
|
||||||
|
if id -Gn "$_U" 2>/dev/null | tr ' ' '\n' | grep -qxF "$_GRP"; then
|
||||||
|
_VALID_USERS+=("$_U")
|
||||||
|
else
|
||||||
|
log "Revoking printer $_PRT access for $_U (no longer in $_GRP)"
|
||||||
|
_UPDATED=true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$_UPDATED" == true ]]; then
|
||||||
|
_prt_write_users "$_PRT" "${_VALID_USERS[@]+"${_VALID_USERS[@]}"}"
|
||||||
|
_prt_apply_acl "$_PRT"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
unset _i _PRT _GRP _URI _LUSER _U _CUR_USERS _VALID_USERS _UPDATED _ap
|
||||||
|
|
||||||
|
# Remove printers whose IPA group was deleted.
|
||||||
|
shopt -s nullglob
|
||||||
|
for _sf in "$PRT_STATE_DIR"/*.users; do
|
||||||
|
[[ -f "$_sf" ]] || continue
|
||||||
|
_PRT=$(basename "$_sf" .users)
|
||||||
|
_prt_is_active "$_PRT" && continue
|
||||||
|
log "Removing printer $_PRT (IPA group usr_prt_$_PRT deleted)"
|
||||||
|
lpadmin -x "$_PRT" 2>/dev/null || true
|
||||||
|
rm -f "$_sf"
|
||||||
|
done
|
||||||
|
shopt -u nullglob
|
||||||
|
unset _sf _PRT
|
||||||
|
|
||||||
|
if [[ ${#ACTIVE_PRT_PRINTERS[@]} -gt 0 ]]; then
|
||||||
|
if [[ ! -f "$PRT_SUDO_DROPIN" ]]; then
|
||||||
|
log "Installing ansipa-printers sudo trigger rule"
|
||||||
|
printf 'ALL ALL=(root) NOPASSWD: /usr/bin/systemctl start ansipa-enforce-policies.service\n' \
|
||||||
|
> "$PRT_SUDO_DROPIN"
|
||||||
|
chmod 440 "$PRT_SUDO_DROPIN"
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$PRT_PROFILED" ]]; then
|
||||||
|
log "Installing /etc/profile.d/ansipa-printers.sh"
|
||||||
|
cat > "$PRT_PROFILED" <<'PROFILED'
|
||||||
|
# ansipa-printers: trigger policy enforcer at login to auto-add printers.
|
||||||
|
# Managed by ansipa-enforce-policies — do not edit manually.
|
||||||
|
sudo -n /usr/bin/systemctl start ansipa-enforce-policies.service 2>/dev/null &
|
||||||
|
disown 2>/dev/null || true
|
||||||
|
PROFILED
|
||||||
|
chmod 644 "$PRT_PROFILED"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ -f "$PRT_SUDO_DROPIN" ]]; then
|
||||||
|
rm -f "$PRT_SUDO_DROPIN"
|
||||||
|
log "Removed ansipa-printers sudo trigger (no active printer groups)"
|
||||||
|
fi
|
||||||
|
if [[ -f "$PRT_PROFILED" ]]; then
|
||||||
|
rm -f "$PRT_PROFILED"
|
||||||
|
log "Removed /etc/profile.d/ansipa-printers.sh (no active printer groups)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
[[ ${#ACTIVE_PRT_PRINTERS[@]} -gt 0 ]] && \
|
||||||
|
warn "lpadmin not found — install cups to enforce printer policies"
|
||||||
|
fi
|
||||||
|
unset _ap
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: usr_scan-notify — scan alert notification daemon for IPA group members.
|
||||||
|
# When the usr_scan-notify user group exists: installs a systemd fetch-alerts timer
|
||||||
|
# (fleet-wide) and a profile.d snippet that starts the notification daemon on login
|
||||||
|
# for group members (checked at login time via id/SSSD).
|
||||||
|
|
||||||
|
FETCH_SVC="/etc/systemd/system/ansipa-fetch-alerts.service"
|
||||||
|
FETCH_TIMER="/etc/systemd/system/ansipa-fetch-alerts.timer"
|
||||||
|
NOTIFY_PROFILED="/etc/profile.d/ansipa-notify.sh"
|
||||||
|
|
||||||
|
if [[ "$WANT_SCAN_NOTIFY" == true ]]; then
|
||||||
|
if [[ ! -x /usr/local/bin/ansipa-fetch-alerts.sh ]]; then
|
||||||
|
warn "ansipa-fetch-alerts.sh not found — run deploy-ansipa-policies.yml first."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$FETCH_SVC" ]]; then
|
||||||
|
log "Installing ansipa-fetch-alerts systemd service + timer"
|
||||||
|
cat > "$FETCH_SVC" <<'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=Fetch Ansipa security alerts from the server SMB share
|
||||||
|
After=network-online.target sssd.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/bin/ansipa-fetch-alerts.sh
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
UNIT
|
||||||
|
|
||||||
|
cat > "$FETCH_TIMER" <<'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=Periodic ansipa security alert fetch
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=2min
|
||||||
|
OnUnitActiveSec=10min
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
UNIT
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now ansipa-fetch-alerts.timer
|
||||||
|
log "ansipa-fetch-alerts.timer enabled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$NOTIFY_PROFILED" ]]; then
|
||||||
|
log "Installing /etc/profile.d/ansipa-notify.sh"
|
||||||
|
cat > "$NOTIFY_PROFILED" <<'PROFILED'
|
||||||
|
# ansipa-notify: launch the scan alert notification daemon on login for
|
||||||
|
# members of the usr_scan-notify FreeIPA user group.
|
||||||
|
# Managed by ansipa-enforce-policies — do not edit manually.
|
||||||
|
_NOTIFY_DAEMON=/usr/local/bin/ansipa-scan-notify.sh
|
||||||
|
if [[ -x "$_NOTIFY_DAEMON" ]] && \
|
||||||
|
id -nG 2>/dev/null | grep -qw "usr_scan-notify" && \
|
||||||
|
! pgrep -u "$(id -u)" -f "ansipa-scan-notify" >/dev/null 2>&1; then
|
||||||
|
"$_NOTIFY_DAEMON" &
|
||||||
|
disown
|
||||||
|
fi
|
||||||
|
unset _NOTIFY_DAEMON
|
||||||
|
PROFILED
|
||||||
|
chmod 644 "$NOTIFY_PROFILED"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ -f "$FETCH_TIMER" ]]; then
|
||||||
|
systemctl disable --now ansipa-fetch-alerts.timer 2>/dev/null || true
|
||||||
|
rm -f "$FETCH_SVC" "$FETCH_TIMER"
|
||||||
|
systemctl daemon-reload
|
||||||
|
log "Removed ansipa-fetch-alerts timer (usr_scan-notify user group no longer exists)"
|
||||||
|
fi
|
||||||
|
if [[ -f "$NOTIFY_PROFILED" ]]; then
|
||||||
|
rm -f "$NOTIFY_PROFILED"
|
||||||
|
log "Removed /etc/profile.d/ansipa-notify.sh"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# policy: usr_smb — auto-mount network shares in user home directories.
|
||||||
|
# usr_smb_r_<name> mount read-only share ~/‹name› for logged-in members
|
||||||
|
# usr_smb_rw_<name> mount read-write share ~/‹name› for logged-in members
|
||||||
|
# rw membership takes precedence over r for the same share name.
|
||||||
|
# Credential (server, Samba user, password) stored in IPA group description by
|
||||||
|
# ansipa-smb-setup.sh: cifs://<ipa-host>:<samba-user>:<password>
|
||||||
|
# Requires: cifs-utils installed on the client.
|
||||||
|
|
||||||
|
SMB_MOUNT_STATE="$STATE_DIR/smb-mounts"
|
||||||
|
[[ -f "$SMB_MOUNT_STATE" ]] || touch "$SMB_MOUNT_STATE"
|
||||||
|
SMB_CREDS_DIR="$STATE_DIR/smb-creds"
|
||||||
|
mkdir -p "$SMB_CREDS_DIR"
|
||||||
|
chmod 700 "$SMB_CREDS_DIR"
|
||||||
|
|
||||||
|
if command -v mount.cifs &>/dev/null; then
|
||||||
|
|
||||||
|
# ── Discover usr_smb_r_* / usr_smb_rw_* from the already-fetched group list
|
||||||
|
declare -A _SMB_R_GRP _SMB_RW_GRP
|
||||||
|
|
||||||
|
while IFS= read -r _g; do
|
||||||
|
[[ -z "$_g" ]] && continue
|
||||||
|
if [[ "$_g" =~ ^usr_smb_r_(.+)$ ]]; then _SMB_R_GRP["${BASH_REMATCH[1]}"]="$_g"
|
||||||
|
elif [[ "$_g" =~ ^usr_smb_rw_(.+)$ ]]; then _SMB_RW_GRP["${BASH_REMATCH[1]}"]="$_g"
|
||||||
|
fi
|
||||||
|
done <<< "$_ALL_USER_GROUPS"
|
||||||
|
unset _g
|
||||||
|
|
||||||
|
declare -A _SMB_NAMES
|
||||||
|
for _n in "${!_SMB_R_GRP[@]}" "${!_SMB_RW_GRP[@]}"; do _SMB_NAMES["$_n"]=1; done
|
||||||
|
unset _n
|
||||||
|
|
||||||
|
_smb_member() { id -nG "$1" 2>/dev/null | tr ' ' '\n' | grep -qxF "$2"; }
|
||||||
|
|
||||||
|
declare -A _SMB_SESS
|
||||||
|
_smb_add_sess() {
|
||||||
|
local _u
|
||||||
|
[[ "$2" == "loginctl" ]] && _u=$(awk '{print $3}' <<< "$1") \
|
||||||
|
|| _u=$(awk '{print $1}' <<< "$1")
|
||||||
|
[[ -z "$_u" || "$_u" == "root" || "$_u" == "USER" ]] && return 0
|
||||||
|
local _h; _h=$(getent passwd "$_u" | cut -d: -f6 2>/dev/null) || return 0
|
||||||
|
[[ -d "$_h" ]] && _SMB_SESS["$_u"]="$_h"
|
||||||
|
}
|
||||||
|
if loginctl list-sessions --no-legend &>/dev/null; then
|
||||||
|
while IFS= read -r _L; do _smb_add_sess "$_L" "loginctl"
|
||||||
|
done < <(loginctl list-sessions --no-legend 2>/dev/null)
|
||||||
|
else
|
||||||
|
while IFS= read -r _L; do _smb_add_sess "$_L" "who"
|
||||||
|
done < <(who 2>/dev/null)
|
||||||
|
fi
|
||||||
|
unset _L
|
||||||
|
|
||||||
|
for _SMB_NAME in "${!_SMB_NAMES[@]}"; do
|
||||||
|
_R_GRP="${_SMB_R_GRP[$_SMB_NAME]:-}"
|
||||||
|
_RW_GRP="${_SMB_RW_GRP[$_SMB_NAME]:-}"
|
||||||
|
|
||||||
|
_CRED_LINE=""
|
||||||
|
[[ -n "$_RW_GRP" ]] && _CRED_LINE=$(_smb_parse_cred "$_RW_GRP")
|
||||||
|
[[ -z "$_CRED_LINE" && -n "$_R_GRP" ]] && _CRED_LINE=$(_smb_parse_cred "$_R_GRP")
|
||||||
|
|
||||||
|
if [[ -z "$_CRED_LINE" ]]; then
|
||||||
|
warn "No credential in IPA description for share $_SMB_NAME — skipped (run ansipa-smb.service on FreeIPA container)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
read -r _SMB_HOST _SMB_SUSER _SMB_SPASS <<< "$_CRED_LINE"
|
||||||
|
|
||||||
|
_CREDS_FILE="$SMB_CREDS_DIR/${_SMB_NAME}.creds"
|
||||||
|
printf 'username=%s\npassword=%s\ndomain=WORKGROUP\n' "$_SMB_SUSER" "$_SMB_SPASS" \
|
||||||
|
> "$_CREDS_FILE"
|
||||||
|
chmod 600 "$_CREDS_FILE"
|
||||||
|
|
||||||
|
for _SMB_U in "${!_SMB_SESS[@]}"; do
|
||||||
|
_SMB_LVL=""
|
||||||
|
[[ -n "$_RW_GRP" ]] && _smb_member "$_SMB_U" "$_RW_GRP" && _SMB_LVL="rw"
|
||||||
|
[[ -z "$_SMB_LVL" && -n "$_R_GRP" ]] && _smb_member "$_SMB_U" "$_R_GRP" && _SMB_LVL="r"
|
||||||
|
[[ -z "$_SMB_LVL" ]] && continue
|
||||||
|
|
||||||
|
_SMB_H="${_SMB_SESS[$_SMB_U]}"
|
||||||
|
_SMB_MP="$_SMB_H/$_SMB_NAME"
|
||||||
|
_SMB_SHARE="usr-${_SMB_NAME}-${_SMB_LVL}"
|
||||||
|
|
||||||
|
if mountpoint -q "$_SMB_MP" 2>/dev/null; then
|
||||||
|
grep -qxF "${_SMB_U}:${_SMB_NAME}" "$SMB_MOUNT_STATE" 2>/dev/null || \
|
||||||
|
echo "${_SMB_U}:${_SMB_NAME}" >> "$SMB_MOUNT_STATE"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$_SMB_MP"
|
||||||
|
chown "$_SMB_U" "$_SMB_MP" 2>/dev/null || true
|
||||||
|
|
||||||
|
if mount -t cifs "//${_SMB_HOST}/${_SMB_SHARE}" "$_SMB_MP" \
|
||||||
|
-o "credentials=${_CREDS_FILE},uid=${_SMB_U},gid=${_SMB_U},file_mode=0644,dir_mode=0755,nounix,noserverino" \
|
||||||
|
2>/dev/null; then
|
||||||
|
log "Mounted //${_SMB_HOST}/${_SMB_SHARE} → $_SMB_MP for $_SMB_U"
|
||||||
|
grep -qxF "${_SMB_U}:${_SMB_NAME}" "$SMB_MOUNT_STATE" 2>/dev/null || \
|
||||||
|
echo "${_SMB_U}:${_SMB_NAME}" >> "$SMB_MOUNT_STATE"
|
||||||
|
else
|
||||||
|
warn "Failed to mount //${_SMB_HOST}/${_SMB_SHARE} for $_SMB_U"
|
||||||
|
rmdir "$_SMB_MP" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
unset _SMB_NAME _R_GRP _RW_GRP _CRED_LINE _SMB_HOST _SMB_SUSER _SMB_SPASS \
|
||||||
|
_CREDS_FILE _SMB_U _SMB_H _SMB_MP _SMB_SHARE _SMB_LVL
|
||||||
|
|
||||||
|
_NEW_SMB_MOUNTS=()
|
||||||
|
while IFS=: read -r _OLD_USER _OLD_NAME; do
|
||||||
|
[[ -z "$_OLD_USER" || -z "$_OLD_NAME" ]] && continue
|
||||||
|
_OLD_HOME=$(getent passwd "$_OLD_USER" | cut -d: -f6 2>/dev/null) || continue
|
||||||
|
_OLD_MPT="$_OLD_HOME/$_OLD_NAME"
|
||||||
|
|
||||||
|
_STILL_MEMBER=false
|
||||||
|
if [[ -n "${_SMB_R_GRP[$_OLD_NAME]+x}" ]] && \
|
||||||
|
_smb_member "$_OLD_USER" "${_SMB_R_GRP[$_OLD_NAME]}"; then
|
||||||
|
_STILL_MEMBER=true
|
||||||
|
elif [[ -n "${_SMB_RW_GRP[$_OLD_NAME]+x}" ]] && \
|
||||||
|
_smb_member "$_OLD_USER" "${_SMB_RW_GRP[$_OLD_NAME]}"; then
|
||||||
|
_STILL_MEMBER=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$_STILL_MEMBER" == true ]]; then
|
||||||
|
_NEW_SMB_MOUNTS+=("${_OLD_USER}:${_OLD_NAME}")
|
||||||
|
else
|
||||||
|
umount -l "$_OLD_MPT" 2>/dev/null || true
|
||||||
|
rmdir "$_OLD_MPT" 2>/dev/null || true
|
||||||
|
log "Unmounted ${_OLD_MPT} for ${_OLD_USER} (left usr_smb_*_${_OLD_NAME})"
|
||||||
|
fi
|
||||||
|
done < "$SMB_MOUNT_STATE"
|
||||||
|
unset _OLD_USER _OLD_NAME _OLD_HOME _OLD_MPT _STILL_MEMBER
|
||||||
|
|
||||||
|
if [[ ${#_NEW_SMB_MOUNTS[@]} -gt 0 ]]; then
|
||||||
|
printf '%s\n' "${_NEW_SMB_MOUNTS[@]}" | sort -u > "$SMB_MOUNT_STATE"
|
||||||
|
else
|
||||||
|
> "$SMB_MOUNT_STATE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$SMB_CREDS_DIR"/*.creds 2>/dev/null || true
|
||||||
|
|
||||||
|
unset _SMB_R_GRP _SMB_RW_GRP _SMB_NAMES _SMB_SESS _NEW_SMB_MOUNTS
|
||||||
|
|
||||||
|
else
|
||||||
|
warn "mount.cifs not found — install cifs-utils to enable usr_smb_* policies"
|
||||||
|
[[ -s "$SMB_MOUNT_STATE" ]] && \
|
||||||
|
warn "smb-mounts state is non-empty — cannot revert existing mounts without cifs-utils" || true
|
||||||
|
fi
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ansipa-smb-setup.sh — configure the Samba scan-results and LUKS-key shares on the IPA container.
|
# ansipa-smb-setup.sh — configure Samba shares on the IPA container.
|
||||||
#
|
#
|
||||||
# Runs on every container start via ansipa-smb.service so that smb.conf and
|
# Runs on every container start via ansipa-smb.service so that smb.conf and
|
||||||
# Samba users are always in place after container restarts (ephemeral rootfs).
|
# Samba users are always in place after container restarts (ephemeral rootfs).
|
||||||
|
|
@ -9,12 +9,17 @@
|
||||||
# 2. /data/samba/ansipa-smb.env (persisted from first boot)
|
# 2. /data/samba/ansipa-smb.env (persisted from first boot)
|
||||||
#
|
#
|
||||||
# Shares:
|
# Shares:
|
||||||
# ansipa-scans — write-only for 'scanupload'; clients push scan results here.
|
# ansipa-scans — write-only for 'scanupload'; clients push scan results here.
|
||||||
# ansipa-luks-keys — write-only for 'luks-upload' (Ansible controller);
|
# ansipa-luks-keys — write-only for 'luks-upload' (Ansible controller);
|
||||||
# read for members of the 'KeyAdmin' Linux group.
|
# read for members of the 'KeyAdmin' Linux group.
|
||||||
# Add a Samba user to KeyAdmin to grant key-read access:
|
# Add a Samba user to KeyAdmin to grant key-read access:
|
||||||
# useradd -r -G KeyAdmin <user>
|
# useradd -r -G KeyAdmin <user>
|
||||||
# smbpasswd -a <user>
|
# smbpasswd -a <user>
|
||||||
|
# ansipa-policystore — read-write for 'smb-policystore'; admins in the IPA group
|
||||||
|
# usr_smb_adm_policystore mount this to edit policy files live.
|
||||||
|
# Password auto-generated and stored in the IPA group description:
|
||||||
|
# cifs://<host>:smb-policystore:<password>
|
||||||
|
# The enforcer on clients reads this credential to sync policy files.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
@ -22,9 +27,11 @@ LOG_TAG="ansipa-smb-setup"
|
||||||
SCAN_BASE="/data/scan-results"
|
SCAN_BASE="/data/scan-results"
|
||||||
LUKS_BASE="/data/luks-keys"
|
LUKS_BASE="/data/luks-keys"
|
||||||
SHARES_BASE="/data/smb-shares"
|
SHARES_BASE="/data/smb-shares"
|
||||||
|
POLICY_STORE_BASE="/data/policy-store"
|
||||||
SMB_CONF="/etc/samba/smb.conf"
|
SMB_CONF="/etc/samba/smb.conf"
|
||||||
SMB_USER="scanupload"
|
SMB_USER="scanupload"
|
||||||
LUKS_UPLOAD_USER="luks-upload"
|
LUKS_UPLOAD_USER="luks-upload"
|
||||||
|
POLICY_STORE_USER="smb-policystore"
|
||||||
KEYADMIN_GROUP="KeyAdmin"
|
KEYADMIN_GROUP="KeyAdmin"
|
||||||
ENV_FILE="/data/samba/ansipa-smb.env"
|
ENV_FILE="/data/samba/ansipa-smb.env"
|
||||||
|
|
||||||
|
|
@ -61,6 +68,7 @@ chmod 600 "$ENV_FILE"
|
||||||
mkdir -p "$SCAN_BASE/archive" "$SCAN_BASE/alerts"
|
mkdir -p "$SCAN_BASE/archive" "$SCAN_BASE/alerts"
|
||||||
mkdir -p "$LUKS_BASE"
|
mkdir -p "$LUKS_BASE"
|
||||||
mkdir -p "$SHARES_BASE"
|
mkdir -p "$SHARES_BASE"
|
||||||
|
mkdir -p "$POLICY_STORE_BASE/policies.d"
|
||||||
|
|
||||||
# ── KeyAdmin group ────────────────────────────────────────────────────────────
|
# ── KeyAdmin group ────────────────────────────────────────────────────────────
|
||||||
if ! getent group "$KEYADMIN_GROUP" &>/dev/null; then
|
if ! getent group "$KEYADMIN_GROUP" &>/dev/null; then
|
||||||
|
|
@ -74,6 +82,11 @@ if ! id "$SMB_USER" &>/dev/null; then
|
||||||
log "Created system user: $SMB_USER"
|
log "Created system user: $SMB_USER"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! id "$POLICY_STORE_USER" &>/dev/null; then
|
||||||
|
useradd -r -s /sbin/nologin -d "$POLICY_STORE_BASE" -M "$POLICY_STORE_USER"
|
||||||
|
log "Created system user: $POLICY_STORE_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
if ! id "$LUKS_UPLOAD_USER" &>/dev/null; then
|
if ! id "$LUKS_UPLOAD_USER" &>/dev/null; then
|
||||||
useradd -r -s /sbin/nologin -d "$LUKS_BASE" -M -G "$KEYADMIN_GROUP" "$LUKS_UPLOAD_USER"
|
useradd -r -s /sbin/nologin -d "$LUKS_BASE" -M -G "$KEYADMIN_GROUP" "$LUKS_UPLOAD_USER"
|
||||||
log "Created system user: $LUKS_UPLOAD_USER (member of $KEYADMIN_GROUP)"
|
log "Created system user: $LUKS_UPLOAD_USER (member of $KEYADMIN_GROUP)"
|
||||||
|
|
@ -85,6 +98,8 @@ fi
|
||||||
chown -R "$SMB_USER:$SMB_USER" "$SCAN_BASE"
|
chown -R "$SMB_USER:$SMB_USER" "$SCAN_BASE"
|
||||||
chown -R "root:$KEYADMIN_GROUP" "$LUKS_BASE"
|
chown -R "root:$KEYADMIN_GROUP" "$LUKS_BASE"
|
||||||
chmod 2750 "$LUKS_BASE" # setgid so new files inherit KeyAdmin group
|
chmod 2750 "$LUKS_BASE" # setgid so new files inherit KeyAdmin group
|
||||||
|
chown -R "$POLICY_STORE_USER:$POLICY_STORE_USER" "$POLICY_STORE_BASE"
|
||||||
|
chmod 2770 "$POLICY_STORE_BASE"
|
||||||
|
|
||||||
# ── smb.conf (base only; _setup_user_shares appends stanzas afterwards) ───────
|
# ── smb.conf (base only; _setup_user_shares appends stanzas afterwards) ───────
|
||||||
_write_smb_conf() {
|
_write_smb_conf() {
|
||||||
|
|
@ -124,6 +139,17 @@ _write_smb_conf() {
|
||||||
browseable = no
|
browseable = no
|
||||||
create mask = 0640
|
create mask = 0640
|
||||||
directory mask = 0750
|
directory mask = 0750
|
||||||
|
|
||||||
|
[ansipa-policystore]
|
||||||
|
comment = Ansipa policy store — admin rw; enforcer syncs policy files from here
|
||||||
|
path = $POLICY_STORE_BASE
|
||||||
|
valid users = $POLICY_STORE_USER
|
||||||
|
read only = no
|
||||||
|
browseable = no
|
||||||
|
force user = $POLICY_STORE_USER
|
||||||
|
force group = $POLICY_STORE_USER
|
||||||
|
create mask = 0664
|
||||||
|
directory mask = 02775
|
||||||
CONF
|
CONF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,13 +309,69 @@ STANZA
|
||||||
kdestroy &>/dev/null || true
|
kdestroy &>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Write smb.conf, set passwords, configure user shares ─────────────────────
|
# ── Policystore share management (usr_smb_adm_policystore IPA user group) ────
|
||||||
|
# Creates the ansipa-policystore Samba share with auto-generated credentials.
|
||||||
|
# Credential is stored in the IPA group description so clients can discover it:
|
||||||
|
# cifs://<host>:smb-policystore:<password>
|
||||||
|
# Uses the same kinit/kdestroy pattern as _setup_user_shares.
|
||||||
|
_setup_policystore() {
|
||||||
|
if [[ ! -f /etc/ipa/default.conf ]]; then
|
||||||
|
warn "IPA not yet configured — skipping policystore setup (restart after ipa-first-boot)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [[ -z "$IPA_ADMIN_PASS" ]]; then
|
||||||
|
warn "IPA_ADMIN_PASSWORD not set — skipping policystore setup"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local realm ipa_host
|
||||||
|
realm=$(awk -F'[[:space:]]*=[[:space:]]*' '/^realm[[:space:]]*=/{print $2; exit}' \
|
||||||
|
/etc/ipa/default.conf 2>/dev/null || echo "")
|
||||||
|
ipa_host=$(hostname -f 2>/dev/null || echo "ipa.test.local")
|
||||||
|
|
||||||
|
echo "$IPA_ADMIN_PASS" | kinit "admin@${realm}" &>/dev/null || {
|
||||||
|
warn "kinit admin@${realm} failed — skipping policystore setup"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Retrieve existing password from the IPA group description or generate one.
|
||||||
|
local PASS=""
|
||||||
|
local _desc
|
||||||
|
if ipa group-show usr_smb_adm_policystore &>/dev/null; then
|
||||||
|
_desc=$(ipa group-show usr_smb_adm_policystore --all 2>/dev/null \
|
||||||
|
| awk -F': ' '/Description:/{print $2; exit}' || true)
|
||||||
|
if [[ "$_desc" =~ ^cifs://[^:]*:smb-policystore:(.+)$ ]]; then
|
||||||
|
PASS="${BASH_REMATCH[1]}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [[ -z "$PASS" ]]; then
|
||||||
|
PASS=$(tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 32 \
|
||||||
|
|| openssl rand -base64 24 | tr -d '/+=\n')
|
||||||
|
log "Generated new password for $POLICY_STORE_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_smb_set_pass "$POLICY_STORE_USER" "$PASS"
|
||||||
|
|
||||||
|
# Create the IPA group if absent, then store/update the credential in its description.
|
||||||
|
ipa group-add usr_smb_adm_policystore \
|
||||||
|
--desc="cifs://${ipa_host}:${POLICY_STORE_USER}:${PASS}" 2>/dev/null \
|
||||||
|
|| ipa group-mod usr_smb_adm_policystore \
|
||||||
|
--desc="cifs://${ipa_host}:${POLICY_STORE_USER}:${PASS}" 2>/dev/null \
|
||||||
|
|| true
|
||||||
|
log "Stored policystore credential in IPA group usr_smb_adm_policystore"
|
||||||
|
|
||||||
|
unset PASS _desc
|
||||||
|
kdestroy &>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Write smb.conf, set passwords, configure shares ──────────────────────────
|
||||||
_smb_set_pass "$SMB_USER" "$SMB_PASS"
|
_smb_set_pass "$SMB_USER" "$SMB_PASS"
|
||||||
_smb_set_pass "$LUKS_UPLOAD_USER" "$LUKS_PASS"
|
_smb_set_pass "$LUKS_UPLOAD_USER" "$LUKS_PASS"
|
||||||
|
|
||||||
# Write base smb.conf first, then have _setup_user_shares append to it.
|
# Write base smb.conf first, then append dynamic share stanzas.
|
||||||
_write_smb_conf
|
_write_smb_conf
|
||||||
_setup_user_shares
|
_setup_user_shares
|
||||||
|
_setup_policystore
|
||||||
|
|
||||||
# Reload smbd if it is already running (picks up new user shares without restart).
|
# Reload smbd if it is already running (picks up new user shares without restart).
|
||||||
smbcontrol smbd reload-config 2>/dev/null || true
|
smbcontrol smbd reload-config 2>/dev/null || true
|
||||||
|
|
@ -304,4 +386,4 @@ CRON
|
||||||
chmod 644 /etc/cron.d/ansipa-check-scans
|
chmod 644 /etc/cron.d/ansipa-check-scans
|
||||||
log "Installed hourly scan-checker cron"
|
log "Installed hourly scan-checker cron"
|
||||||
|
|
||||||
log "Samba setup complete. Share: //localhost/ansipa-scans user: $SMB_USER"
|
log "Samba setup complete. Shares: ansipa-scans ($SMB_USER), ansipa-policystore ($POLICY_STORE_USER)"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue