feat(ansipa): rework binary blocking as per-user policy; add local_sudo device policy
policy-block-binary-<name> is now a FreeIPA *user* group instead of a host group, so restrictions follow the user to every enrolled machine. The PATH wrapper is installed on all hosts and checks group membership at runtime via id(1)/SSSD, passing non-members through transparently. __ in the group name decodes to . so Flatpak app IDs are supported (flatpak run fallback included). AppArmor layer removed since per-user confinement requires a different approach and the wrapper alone is sufficient. Adds local_sudo_<username> host group policy which writes a sudoers drop-in granting that user full sudo on the specific device, reverted on group leave. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
6ad8d0d488
commit
87b62f368b
|
|
@ -265,19 +265,27 @@ Keys on the SMB share are accessible only to `KeyAdmin` group members (see [SMB
|
||||||
|
|
||||||
## Host Group Reference
|
## Host Group Reference
|
||||||
|
|
||||||
|
### Device policies (host groups — applied machine-wide)
|
||||||
|
|
||||||
| Group prefix | Handled by | Effect |
|
| Group prefix | Handled by | Effect |
|
||||||
|--------------|-----------|--------|
|
|--------------|-----------|--------|
|
||||||
| `ansipa-install-<pkg>` | `ansipa-install-packages.sh` | Install/remove native package |
|
| `ansipa-install-<pkg>` | `ansipa-install-packages.sh` | Install/remove native package |
|
||||||
| `ansipa-module-<name>` | `ansipa-install-modules.sh` | Run module script once |
|
| `ansipa-module-<name>` | `ansipa-install-modules.sh` | Run module script once |
|
||||||
| `fp_install-<app>` | `ansipa-install-flatpaks.sh` | Install Flatpak app |
|
| `fp_install-<app>` | `ansipa-install-flatpaks.sh` | Install Flatpak app |
|
||||||
| `BaseUser` | `auto-add-baseuser.sh` | Add user to local `baseusers` group |
|
| `BaseUser` | `auto-add-baseuser.sh` | Add user to local `baseusers` group |
|
||||||
| `policy-block-binary-<name>` | `ansipa-enforce-policies.sh` | Block binary via PATH wrapper + AppArmor |
|
|
||||||
| `policy-daemon-enable-<unit>` | `ansipa-enforce-policies.sh` | `systemctl enable --now <unit>`; reverted on leave |
|
| `policy-daemon-enable-<unit>` | `ansipa-enforce-policies.sh` | `systemctl enable --now <unit>`; reverted on leave |
|
||||||
| `policy-daemon-disable-<unit>` | `ansipa-enforce-policies.sh` | `systemctl disable --now <unit>`; reverted on leave |
|
| `policy-daemon-disable-<unit>` | `ansipa-enforce-policies.sh` | `systemctl disable --now <unit>`; reverted on leave |
|
||||||
| `policy-timeshift-backup` | `ansipa-enforce-policies.sh` | Daily Timeshift snapshot at 03:00 |
|
| `policy-timeshift-backup` | `ansipa-enforce-policies.sh` | Daily Timeshift snapshot at 03:00 |
|
||||||
| `policy-security-scan` | `ansipa-enforce-policies.sh` | Daily ClamAV + rkhunter + chkrootkit scan + SMB upload |
|
| `policy-security-scan` | `ansipa-enforce-policies.sh` | Daily ClamAV + rkhunter + chkrootkit scan + SMB upload |
|
||||||
| `policy-scan-notify` | `ansipa-enforce-policies.sh` | Fetch server alerts, notify user every 10 min until acknowledged |
|
| `policy-scan-notify` | `ansipa-enforce-policies.sh` | Fetch server alerts, notify user every 10 min until acknowledged |
|
||||||
| `no_local_users` | `ansipa-enforce-policies.sh` | Lock passwords for root and all local users (UID ≥ 1000); reverted on leave |
|
| `no_local_users` | `ansipa-enforce-policies.sh` | Lock passwords for root and all local users (UID ≥ 1000); reverted on leave |
|
||||||
|
| `local_sudo_<username>` | `ansipa-enforce-policies.sh` | Grant `<username>` full sudo on this specific device; reverted on leave |
|
||||||
|
|
||||||
|
### User policies (user groups — follow the user across all enrolled devices)
|
||||||
|
|
||||||
|
| Group prefix | Handled by | Effect |
|
||||||
|
|--------------|-----------|--------|
|
||||||
|
| `policy-block-binary-<name>` | `ansipa-enforce-policies.sh` | Prevent group members from running `<name>` on any enrolled host; use `__` for `.` in Flatpak app IDs |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -285,14 +293,17 @@ Keys on the SMB share are accessible only to `KeyAdmin` group members (see [SMB
|
||||||
|
|
||||||
`ansipa-enforce-policies.sh` runs every 30 minutes on each enrolled client (deployed by `deploy-ansipa-policies.yml`). All policies are idempotent and reversible — leaving a host group undoes the policy on the next run.
|
`ansipa-enforce-policies.sh` runs every 30 minutes on each enrolled client (deployed by `deploy-ansipa-policies.yml`). All policies are idempotent and reversible — leaving a host group undoes the policy on the next run.
|
||||||
|
|
||||||
### Binary Blocking
|
### Binary Blocking (per user)
|
||||||
|
|
||||||
Adding a host to `policy-block-binary-<name>` applies two layers:
|
`policy-block-binary-<name>` is a FreeIPA **user group**, not a host group. Membership follows the user to every enrolled machine: a blocked user cannot run `<name>` regardless of which device they log into.
|
||||||
|
|
||||||
1. **PATH wrapper** — a script in `/usr/local/bin/<name>` that prints a policy message and exits 1. Takes priority over the real binary for `$PATH`-based calls.
|
The enforcer queries all `policy-block-binary-*` user groups from FreeIPA on every run and installs a **PATH-priority wrapper** in `/usr/local/bin/<name>` for each one. The wrapper checks the caller's group membership at runtime via `id(1)` / SSSD and:
|
||||||
2. **AppArmor deny profile** — `/etc/apparmor.d/ansipa-block-<name>` with an empty profile, denying all file access. Blocks absolute-path calls and direct `exec()`. Skipped silently if `apparmor_parser` is not present.
|
- **blocks** the command if the caller is a group member (exits 1 with a policy message);
|
||||||
|
- **passes through** to the real binary for all other users (searches native PATH dirs, then falls back to `flatpak run <name>`).
|
||||||
|
|
||||||
Leaving the group removes both layers on the next enforcer run.
|
**Flatpak support:** use `__` in place of `.` in the group name. For example, `policy-block-binary-org__gimp__Gimp` blocks the Flatpak `org.gimp.Gimp` for group members while transparently invoking `flatpak run org.gimp.Gimp` for everyone else.
|
||||||
|
|
||||||
|
Deleting the IPA user group causes the wrapper to be removed on the next enforcer run. State is tracked in `/var/lib/ansipa-policies/blocked-binaries`.
|
||||||
|
|
||||||
### Daemon Enable / Disable
|
### Daemon Enable / Disable
|
||||||
|
|
||||||
|
|
@ -343,6 +354,17 @@ State is persisted in `/var/lib/ansipa-policies/no-local-users` (one username pe
|
||||||
|
|
||||||
**Interaction with FreeIPA sudo:** Domain accounts in the `sudoers` or `sudo-nopasswd` FreeIPA groups retain full sudo access via SSSD — local password lockdown does not affect them. Ensure at least one domain admin has sudo before adding a host to this group.
|
**Interaction with FreeIPA sudo:** Domain accounts in the `sudoers` or `sudo-nopasswd` FreeIPA groups retain full sudo access via SSSD — local password lockdown does not affect them. Ensure at least one domain admin has sudo before adding a host to this group.
|
||||||
|
|
||||||
|
### Per-device sudo grants
|
||||||
|
|
||||||
|
`local_sudo_<username>` is a **host group** that grants a specific user full sudo on that particular machine, independently of FreeIPA-wide sudo rules. This is useful for giving a user admin rights on their own workstation while keeping them unprivileged on shared servers.
|
||||||
|
|
||||||
|
| Action | Effect |
|
||||||
|
|--------|--------|
|
||||||
|
| Join `local_sudo_alice` | Creates `/etc/sudoers.d/ansipa-local-sudo-alice` with `alice ALL=(ALL) ALL` |
|
||||||
|
| Leave `local_sudo_alice` | Removes the drop-in on the next enforcer run |
|
||||||
|
|
||||||
|
State is tracked in `/var/lib/ansipa-policies/local-sudo-users`. Drop-ins are mode `0440` and validated by `visudo` syntax rules automatically.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## LUKS Key Flow
|
## LUKS Key Flow
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@
|
||||||
# Policies are idempotent and reversible: joining a group applies the policy;
|
# 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).
|
# leaving the group removes it on the next run (every 30 min via systemd timer).
|
||||||
#
|
#
|
||||||
# Host-group naming conventions:
|
# Host-group naming conventions (device policies — applied to the whole machine):
|
||||||
# policy-block-binary-<name> Block execution of <name> via two layers:
|
|
||||||
# 1. PATH-priority wrapper in /usr/local/bin/ (catches $PATH calls)
|
|
||||||
# 2. AppArmor deny profile in /etc/apparmor.d/ (catches absolute paths)
|
|
||||||
# AppArmor layer is skipped silently if apparmor_parser is not present.
|
|
||||||
# policy-daemon-enable-<unit> Ensure <unit> is enabled and running (systemctl enable --now).
|
# policy-daemon-enable-<unit> Ensure <unit> is enabled and running (systemctl enable --now).
|
||||||
# Leaving the group reverts: service is disabled and stopped.
|
# Leaving the group reverts: service is disabled and stopped.
|
||||||
# policy-daemon-disable-<unit> Ensure <unit> is disabled and stopped (systemctl disable --now).
|
# policy-daemon-disable-<unit> Ensure <unit> is disabled and stopped (systemctl disable --now).
|
||||||
|
|
@ -22,6 +18,17 @@
|
||||||
# rules can authenticate and gain elevated privileges.
|
# rules can authenticate and gain elevated privileges.
|
||||||
# Leaving the group reverts: every account locked by this policy
|
# Leaving the group reverts: every account locked by this policy
|
||||||
# is unlocked on the next run.
|
# is unlocked on the next run.
|
||||||
|
# local_sudo_<username> Grant <username> full sudo on this specific device by adding
|
||||||
|
# them to the local sudoers drop-in. Leaving the group removes
|
||||||
|
# the drop-in on the next run.
|
||||||
|
#
|
||||||
|
# User-group naming conventions (per-user policies — follow the user across devices):
|
||||||
|
# policy-block-binary-<name> Prevent members of this FreeIPA user group from running <name>
|
||||||
|
# on any enrolled host. Use __ in place of . to support Flatpak
|
||||||
|
# app IDs (e.g. policy-block-binary-org__gimp__Gimp blocks the
|
||||||
|
# Flatpak org.gimp.Gimp). Enforced via a PATH-priority wrapper
|
||||||
|
# that checks group membership at runtime via SSSD/id(1).
|
||||||
|
# Removing the user group from FreeIPA reverts the wrapper.
|
||||||
#
|
#
|
||||||
# Notes:
|
# Notes:
|
||||||
# - Install scan tools first: add the host to ansipa-module-anti-malware.
|
# - Install scan tools first: add the host to ansipa-module-anti-malware.
|
||||||
|
|
@ -32,7 +39,6 @@ set -euo pipefail
|
||||||
LOG_TAG="ansipa-policies"
|
LOG_TAG="ansipa-policies"
|
||||||
STATE_DIR="/var/lib/ansipa-policies"
|
STATE_DIR="/var/lib/ansipa-policies"
|
||||||
BLOCK_DIR="/usr/local/bin"
|
BLOCK_DIR="/usr/local/bin"
|
||||||
APPARMOR_DIR="/etc/apparmor.d"
|
|
||||||
CRON_DIR="/etc/cron.d"
|
CRON_DIR="/etc/cron.d"
|
||||||
|
|
||||||
log() { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; }
|
log() { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; }
|
||||||
|
|
@ -52,10 +58,10 @@ mkdir -p "$STATE_DIR"
|
||||||
RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \
|
RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \
|
||||||
| grep -i "Member of host-groups:" | sed 's/.*: //' || true)
|
| grep -i "Member of host-groups:" | sed 's/.*: //' || true)
|
||||||
|
|
||||||
# ── Parse active policy groups ────────────────────────────────────────────────
|
# ── Parse active host-group (device) policies ─────────────────────────────────
|
||||||
ACTIVE_BLOCK_BINARIES=()
|
|
||||||
ACTIVE_DAEMON_ENABLE=()
|
ACTIVE_DAEMON_ENABLE=()
|
||||||
ACTIVE_DAEMON_DISABLE=()
|
ACTIVE_DAEMON_DISABLE=()
|
||||||
|
ACTIVE_LOCAL_SUDO_USERS=()
|
||||||
WANT_TIMESHIFT_BACKUP=false
|
WANT_TIMESHIFT_BACKUP=false
|
||||||
WANT_SECURITY_SCAN=false
|
WANT_SECURITY_SCAN=false
|
||||||
WANT_SCAN_NOTIFY=false
|
WANT_SCAN_NOTIFY=false
|
||||||
|
|
@ -66,26 +72,45 @@ if [[ -n "$RAW_GROUPS" ]]; then
|
||||||
for g in "${GRP_ARRAY[@]}"; do
|
for g in "${GRP_ARRAY[@]}"; do
|
||||||
g="${g// /}"
|
g="${g// /}"
|
||||||
case "$g" in
|
case "$g" in
|
||||||
policy-block-binary-*) ACTIVE_BLOCK_BINARIES+=("${g#policy-block-binary-}") ;;
|
|
||||||
policy-daemon-enable-*) ACTIVE_DAEMON_ENABLE+=("${g#policy-daemon-enable-}") ;;
|
policy-daemon-enable-*) ACTIVE_DAEMON_ENABLE+=("${g#policy-daemon-enable-}") ;;
|
||||||
policy-daemon-disable-*) ACTIVE_DAEMON_DISABLE+=("${g#policy-daemon-disable-}") ;;
|
policy-daemon-disable-*) ACTIVE_DAEMON_DISABLE+=("${g#policy-daemon-disable-}") ;;
|
||||||
policy-timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;;
|
policy-timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;;
|
||||||
policy-security-scan) WANT_SECURITY_SCAN=true ;;
|
policy-security-scan) WANT_SECURITY_SCAN=true ;;
|
||||||
policy-scan-notify) WANT_SCAN_NOTIFY=true ;;
|
policy-scan-notify) WANT_SCAN_NOTIFY=true ;;
|
||||||
no_local_users) WANT_NO_LOCAL_USERS=true ;;
|
no_local_users) WANT_NO_LOCAL_USERS=true ;;
|
||||||
|
local_sudo_*) ACTIVE_LOCAL_SUDO_USERS+=("${g#local_sudo_}") ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
done <<< "$RAW_GROUPS"
|
done <<< "$RAW_GROUPS"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "Active policies — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
|
# ── Fetch user-group-based binary block policies from FreeIPA ─────────────────
|
||||||
"| daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none} | daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \
|
# policy-block-binary-<name> groups are FreeIPA *user* groups — membership follows
|
||||||
"| timeshift-backup: $WANT_TIMESHIFT_BACKUP" \
|
# the user to every enrolled host rather than being tied to a device.
|
||||||
"| security-scan: $WANT_SECURITY_SCAN | scan-notify: $WANT_SCAN_NOTIFY" \
|
ACTIVE_BLOCK_BINARIES=()
|
||||||
"| no-local-users: $WANT_NO_LOCAL_USERS"
|
ACTIVE_BLOCK_IPA_GROUPS=()
|
||||||
|
|
||||||
|
_BLOCK_LIST=$(ipa group-find --pkey-only 2>/dev/null \
|
||||||
|
| awk '/Group name:/ {print $NF}' \
|
||||||
|
| grep "^policy-block-binary-" | sort -u || true)
|
||||||
|
|
||||||
|
while IFS= read -r _grp; do
|
||||||
|
[[ -z "$_grp" ]] && continue
|
||||||
|
_raw="${_grp#policy-block-binary-}"
|
||||||
|
ACTIVE_BLOCK_BINARIES+=("${_raw//__/.}")
|
||||||
|
ACTIVE_BLOCK_IPA_GROUPS+=("$_grp")
|
||||||
|
done <<< "$_BLOCK_LIST"
|
||||||
|
unset _BLOCK_LIST _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" \
|
||||||
|
"| scan-notify: $WANT_SCAN_NOTIFY | no-local-users: $WANT_NO_LOCAL_USERS" \
|
||||||
|
"| local-sudo: ${ACTIVE_LOCAL_SUDO_USERS[*]:-none}"
|
||||||
|
log "User policies — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}"
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
in_active_list() {
|
_in_block_list() {
|
||||||
local needle="$1"
|
local needle="$1"
|
||||||
for b in "${ACTIVE_BLOCK_BINARIES[@]}"; do
|
for b in "${ACTIVE_BLOCK_BINARIES[@]}"; do
|
||||||
[[ "$b" == "$needle" ]] && return 0
|
[[ "$b" == "$needle" ]] && return 0
|
||||||
|
|
@ -93,102 +118,53 @@ in_active_list() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find the real installed binary, skipping /usr/local/bin where our wrapper lives.
|
# ── Binary blocking (user-based) ──────────────────────────────────────────────
|
||||||
find_real_binary() {
|
# A PATH-priority wrapper is installed in /usr/local/bin/ for every binary named
|
||||||
local name="$1"
|
# by a policy-block-binary-* FreeIPA *user* group. The wrapper checks the
|
||||||
for dir in /usr/bin /usr/sbin /bin /sbin /usr/local/sbin /opt/bin; do
|
# caller's group membership at runtime (via id + SSSD) and only blocks members;
|
||||||
[[ -x "$dir/$name" ]] && echo "$dir/$name" && return 0
|
# non-members are transparently passed through to the real binary.
|
||||||
done
|
# __ in the group suffix decodes to . so Flatpak app IDs are fully supported.
|
||||||
return 1
|
# Removing the IPA user group causes the wrapper to be cleaned up on the next run.
|
||||||
}
|
|
||||||
|
|
||||||
aa_profile_file() { echo "$APPARMOR_DIR/ansipa-block-${1}"; }
|
|
||||||
|
|
||||||
# Load an AppArmor deny profile for a binary path.
|
|
||||||
# An empty AppArmor profile denies all access: the binary cannot load shared
|
|
||||||
# libraries or open any files, so it exits immediately with a permission error.
|
|
||||||
apply_apparmor_block() {
|
|
||||||
local bin="$1"
|
|
||||||
command -v apparmor_parser &>/dev/null || return 0
|
|
||||||
|
|
||||||
local bin_path
|
|
||||||
bin_path=$(find_real_binary "$bin") || {
|
|
||||||
warn "AppArmor block: real binary '$bin' not found on disk — profile skipped until it is installed."
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
local profile_file
|
|
||||||
profile_file=$(aa_profile_file "$bin")
|
|
||||||
|
|
||||||
# Write the profile only if it doesn't exist or points to a different path.
|
|
||||||
if [[ ! -f "$profile_file" ]] || ! grep -qF "$bin_path" "$profile_file" 2>/dev/null; then
|
|
||||||
log "Writing AppArmor block profile: $profile_file ($bin_path)"
|
|
||||||
cat > "$profile_file" <<PROFILE
|
|
||||||
#include <tunables/global>
|
|
||||||
|
|
||||||
# ansipa-block-policy: managed by ansipa-enforce-policies — do not edit manually.
|
|
||||||
# Deny all access so the binary cannot load libraries or run.
|
|
||||||
# To unblock manually: apparmor_parser -R $profile_file && rm $profile_file
|
|
||||||
$bin_path {
|
|
||||||
}
|
|
||||||
PROFILE
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Load (or reload) the profile in enforce mode.
|
|
||||||
if ! apparmor_parser -r "$profile_file" 2>/dev/null; then
|
|
||||||
warn "apparmor_parser failed to load $profile_file — AppArmor block not active"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove the AppArmor deny profile for a binary.
|
|
||||||
remove_apparmor_block() {
|
|
||||||
local bin="$1"
|
|
||||||
command -v apparmor_parser &>/dev/null || return 0
|
|
||||||
|
|
||||||
local profile_file
|
|
||||||
profile_file=$(aa_profile_file "$bin")
|
|
||||||
[[ -f "$profile_file" ]] || return 0
|
|
||||||
|
|
||||||
if grep -q "ansipa-block-policy" "$profile_file" 2>/dev/null; then
|
|
||||||
apparmor_parser -R "$profile_file" 2>/dev/null || true
|
|
||||||
rm -f "$profile_file"
|
|
||||||
log "Removed AppArmor block profile: $bin"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Binary blocking ───────────────────────────────────────────────────────────
|
|
||||||
# Layer 1: PATH-priority wrapper in /usr/local/bin/ — blocks $PATH-based calls.
|
|
||||||
# Layer 2: AppArmor deny profile — blocks absolute-path calls and direct exec().
|
|
||||||
# Both layers use the "ansipa policy" sentinel to identify managed files.
|
|
||||||
|
|
||||||
BLOCK_STATE="$STATE_DIR/blocked-binaries"
|
BLOCK_STATE="$STATE_DIR/blocked-binaries"
|
||||||
[[ -f "$BLOCK_STATE" ]] || touch "$BLOCK_STATE"
|
[[ -f "$BLOCK_STATE" ]] || touch "$BLOCK_STATE"
|
||||||
|
|
||||||
for BIN in "${ACTIVE_BLOCK_BINARIES[@]}"; do
|
for _idx in "${!ACTIVE_BLOCK_BINARIES[@]}"; do
|
||||||
|
BIN="${ACTIVE_BLOCK_BINARIES[$_idx]}"
|
||||||
|
IPA_GRP="${ACTIVE_BLOCK_IPA_GROUPS[$_idx]}"
|
||||||
WRAPPER="$BLOCK_DIR/$BIN"
|
WRAPPER="$BLOCK_DIR/$BIN"
|
||||||
if [[ ! -f "$WRAPPER" ]] || ! grep -q "blocked by ansipa policy" "$WRAPPER" 2>/dev/null; then
|
# Write (or refresh) the wrapper when it is absent, not ours, or the group name changed.
|
||||||
log "Applying PATH wrapper block: $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
|
cat > "$WRAPPER" <<WRAPPER
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# blocked by ansipa policy
|
# blocked by ansipa policy (user-based)
|
||||||
echo "[$LOG_TAG] '$BIN' is blocked by system policy on this host." >&2
|
if id -Gn 2>/dev/null | tr ' ' '\n' | grep -qxF "${IPA_GRP}"; then
|
||||||
exit 1
|
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
|
WRAPPER
|
||||||
chmod 755 "$WRAPPER"
|
chmod 755 "$WRAPPER"
|
||||||
fi
|
fi
|
||||||
apply_apparmor_block "$BIN"
|
|
||||||
done
|
done
|
||||||
|
unset _idx
|
||||||
|
|
||||||
# Remove blocks for binaries no longer in any active policy group.
|
# Remove wrappers whose IPA user group no longer exists.
|
||||||
while IFS= read -r OLD_BIN; do
|
while IFS= read -r OLD_BIN; do
|
||||||
[[ -z "$OLD_BIN" ]] && continue
|
[[ -z "$OLD_BIN" ]] && continue
|
||||||
if ! in_active_list "$OLD_BIN"; then
|
if ! _in_block_list "$OLD_BIN"; then
|
||||||
WRAPPER="$BLOCK_DIR/$OLD_BIN"
|
WRAPPER="$BLOCK_DIR/$OLD_BIN"
|
||||||
if [[ -f "$WRAPPER" ]] && grep -q "blocked by ansipa policy" "$WRAPPER" 2>/dev/null; then
|
if [[ -f "$WRAPPER" ]] && grep -q "blocked by ansipa policy" "$WRAPPER" 2>/dev/null; then
|
||||||
rm -f "$WRAPPER"
|
rm -f "$WRAPPER"
|
||||||
log "Removed PATH wrapper block: $OLD_BIN"
|
log "Removed binary block wrapper: $OLD_BIN"
|
||||||
fi
|
fi
|
||||||
remove_apparmor_block "$OLD_BIN"
|
|
||||||
fi
|
fi
|
||||||
done < "$BLOCK_STATE"
|
done < "$BLOCK_STATE"
|
||||||
|
|
||||||
|
|
@ -452,6 +428,48 @@ else
|
||||||
> "$DAEMON_DISABLE_STATE"
|
> "$DAEMON_DISABLE_STATE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Per-device local sudo grants ──────────────────────────────────────────────
|
||||||
|
# local_sudo_<username>: write a sudoers drop-in granting <username> full sudo on
|
||||||
|
# this specific device. The drop-in is removed when the host leaves the 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[@]}"; 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 local_sudo_* group.
|
||||||
|
while IFS= read -r _OLD_USER; do
|
||||||
|
[[ -z "$_OLD_USER" ]] && continue
|
||||||
|
_still_active=false
|
||||||
|
for _U in "${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 local_sudo_$_OLD_USER group)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$LOCAL_SUDO_STATE"
|
||||||
|
|
||||||
|
# Persist current local sudo users.
|
||||||
|
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
|
||||||
|
|
||||||
# ── No-local-users policy ──────────────────────────────────────────────────────
|
# ── No-local-users policy ──────────────────────────────────────────────────────
|
||||||
# no_local_users: lock the passwords of root and all local users (UID >= 1000)
|
# no_local_users: lock the passwords of root and all local users (UID >= 1000)
|
||||||
# so that only FreeIPA domain accounts with centrally-managed sudo rules can
|
# so that only FreeIPA domain accounts with centrally-managed sudo rules can
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue