feat(freeipa-ansible): split login monitoring and add usr_ctl capability grants

Split usr_mon_logins into three focused checks: failed local (console/
tty/greeter) logins, SSH logins (usr_mon_sshlogins), and sudo/SELinux/
AppArmor rights violations (usr_mon_rightsviolations) — plus a new
usr_mon_iploc check that flags public-IP geolocation country changes.

Add usr_ctl_dnshostfile, usr_ctl_netman, and usr_ctl_wifi policies that
grant members scoped, non-root capability (hosts file ACL, NetworkManager
connection permissions) resolved from IPA group membership and
re-applied every enforcer tick.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
main
Amir Alexander Abdelbaki 2026-07-23 09:06:34 +02:00
parent 042103f131
commit cd2f1424c7
9 changed files with 586 additions and 10 deletions

View File

@ -159,10 +159,59 @@ lm-sensors: WARN ≥ 65 W, CRIT ≥ 95 W. Add the host to `dev_mod_*` that insta
`lm_sensors` if RAPL is unavailable.
### `usr_mon_logins` (user group)
`Ansipa_Logins` check reporting SSH login attempts in the last 24 h (successful +
`Ansipa_Logins` check reporting failed **local** login attempts in the last 24 h
— console/tty, graphical greeters (gdm/sddm/greetd/lightdm): anything
authenticating via PAM that isn't sshd or sudo. Reports failed + invalid-user
counts, with thresholds. Installed fleet-wide when the group exists; needs
`dev_mon_base` on the hosts. SSH activity is tracked separately by
`usr_mon_sshlogins`; sudo/access-control denials by `usr_mon_rightsviolations`
— both below.
### `usr_mon_sshlogins` (user group)
`Ansipa_SSHLogins` check reporting SSH login attempts in the last 24 h (successful +
failed + invalid-user), with thresholds. Installed fleet-wide when the group
exists; needs `dev_mon_base` on the hosts.
### `usr_mon_rightsviolations` (user group)
`Ansipa_RightsViolations` check reporting authorization/access-control denials
by an already-authenticated user in the last 24 h — not "who are you" failures
(that's the two checks above), but "you are who you say, but that's not
allowed": sudo denials (`user NOT in sudoers`, `command not allowed`, incorrect
sudo password — works on every distro, sudo always logs these), plus SELinux
AVC denials (Fedora/RHEL, only when `getenforce` reports `Enforcing`) and
AppArmor denials (Debian/Ubuntu, only if loaded). Each source degrades
gracefully when not applicable to the host — this is a best-effort union of
what the system already logs, not a guarantee of catching every possible file
access denial; watching arbitrary paths would need explicit `auditd` watch
rules on specific files, which is a deliberate per-host decision this check
does not make for you. WARN ≥ 3 total, CRIT ≥ 10 total. Installed fleet-wide
when the group exists; needs `dev_mon_base` on the hosts.
### `usr_mon_iploc` (user group)
`Ansipa_IPLoc` check reporting this device's current public IP and its
geolocation (city/region/country/ISP), and **WARNing when the observed country
changes** since the last reading — e.g. a device that's normally in one
country suddenly appearing in another.
- Uses two free, no-API-key third-party services: `api.ipify.org` (what's my
public IP) and `ip-api.com` (IP → geolocation). The device's public IP is
sent to both on every actual lookup — be aware of that before enabling this
fleet-wide.
- Reports the network **egress** location, not necessarily the device's
physical location: a host behind a VPN will show the VPN exit country, and
that will (correctly, if noisily) trigger a "country changed" WARN the
moment the VPN connects or disconnects.
- No hardcoded country blocklist — *any* change from the last-observed
country trips a WARN once, then returns to OK on the next reading if the
country is stable at the new location. This is intentionally an anomaly
("it just moved") signal, not a permanent flag on any particular country.
- CheckMK invokes local checks on its own polling cadence (often every
minute) — far more often than an external API should be hit. This check
self-throttles to one real lookup per 30 minutes and replays its last
result in between; the geolocation call additionally only re-fires when
the public IP itself has changed.
- Installed fleet-wide when the group exists; needs `dev_mon_base` on the hosts.
---
## Software-installer policies (`dev_pkg_*`, `dev_mod_*`, `dev_fp_*`)
@ -241,6 +290,50 @@ the live `policies.d/*.sh` files directly on the share — changes take effect
fleet-wide on the next 30-min enforcer tick, no redeploy needed. Credential in
the group description (auto-set by `ansipa-smb-setup.sh`).
### `usr_ctl_dnshostfile`
Grants members write access to `/etc/hosts` via a POSIX ACL
(`setfacl -m u:<user>:rw /etc/hosts`) on every enrolled host. Unlike the other
`usr_*` grants above, this doesn't rely on runtime SSSD/PAM resolution — a
filesystem ACL can't check group membership dynamically at the point of use,
so the enforcer computes the group's actual member list (`ipa group-show
usr_ctl_dnshostfile --all`) and re-applies it every tick, revoking the ACL
entry for anyone who has left.
- **Scope:** `/etc/hosts` only — deliberately not `/etc/resolv.conf`, which on
systemd-resolved systems is usually a symlink to a tmpfs stub regenerated on
boot/network changes, so a static ACL there would silently vanish.
- **Requires:** the `acl` package (`setfacl`/`getfacl`) on the client.
- **Security note:** this is a real grant, not a cosmetic one — a member can
redirect any hostname to any IP *on that machine*, enough to locally spoof a
login page or update server. Grant deliberately.
### `usr_ctl_netman` / `usr_ctl_wifi`
Grant members the ability to modify this device's NetworkManager connections
via NetworkManager's own per-connection `connection.permissions` mechanism —
NOT a blanket sudo/root grant. NetworkManager already lets a connection's
designated non-root "permitted user" modify/activate that connection without
a password (`org.freedesktop.NetworkManager.settings.modify.own`, allowed for
local users by default on every mainstream distro's polkit rules); these
policies just add the member to that list.
- **`usr_ctl_netman`** — every **non-Wi-Fi** connection (ethernet, bridge,
bond, VLAN, …): IP address, gateway, DNS, routes. Wi-Fi connections are left
completely untouched.
- **`usr_ctl_wifi`** — every **Wi-Fi** connection only: SSID, security/PSK,
etc. Wired/other connections are left completely untouched.
- A user can be in either, both, or neither — the two are kept deliberately
separate rather than one combined "network admin" group.
- Like `usr_ctl_dnshostfile`, membership is resolved to concrete usernames
(`ipa group-show ... --all`) and re-applied every enforcer tick, since
NetworkManager's connection permissions are static profile metadata, not
something checkable dynamically at the point of use.
- **Requires:** NetworkManager (`nmcli`) on the client.
- Applies to connections that exist at enforcement time; a brand-new
connection created afterwards is picked up on the next 30-min tick. Users
can already create/manage their own new connections by default (standard
NetworkManager multi-user behavior) — these policies are specifically about
granting access to existing, previously root-owned connections.
---
## BaseUser sync (`BaseUser` group)

View File

@ -77,10 +77,48 @@
# automatically: commit and push (GPG-signed) to the ansipa-policy
# git server, which ansipa-pull-apply.sh verifies and distributes
# fleet-wide. See docs/md/freeipa-ansible.md.
# usr_mon_logins (CheckMK) Local check for SSH login attempts in the last 24 h:
# usr_ctl_dnshostfile Grants members write access to /etc/hosts via a POSIX ACL
# (setfacl). Unlike the grants above, membership can't be checked
# dynamically at the point of use (a filesystem ACL isn't a
# runtime SSSD/PAM check), so the enforcer resolves the group's
# actual member list every tick and revokes the ACL entry for
# anyone who has left. Requires the 'acl' package.
# usr_ctl_netman Grants members modify access to this host's NON-Wi-Fi
# NetworkManager connections (IP/gateway/DNS/routes) via
# NetworkManager's own connection.permissions mechanism — NOT sudo.
# Wi-Fi connections are left completely untouched (see
# usr_ctl_wifi). Requires NetworkManager (nmcli).
# usr_ctl_wifi Grants members modify access to this host's Wi-Fi
# NetworkManager connections only (SSID/security) — the mirror of
# usr_ctl_netman; wired/other connections are untouched. A user
# can be in either, both, or neither. Requires NetworkManager (nmcli).
# usr_mon_logins (CheckMK) Local check for failed LOCAL login attempts in the
# last 24 h — console/tty, graphical greeters: anything via PAM
# that isn't sshd or sudo (those are usr_mon_sshlogins and
# usr_mon_rightsviolations, below).
# Reports failed + invalid-user counts with thresholds.
# WARN: ≥10 failed or ≥5 invalid; CRIT: ≥50 failed or ≥20 invalid.
# Installed fleet-wide when the group exists; requires dev_mon_base.
# usr_mon_sshlogins (CheckMK) Local check for SSH login attempts in the last 24 h:
# successful + failed + invalid-user counts with thresholds.
# WARN: ≥10 failed or ≥5 invalid; CRIT: ≥50 failed or ≥20 invalid.
# Installed fleet-wide when the group exists; requires dev_mon_base.
# usr_mon_rightsviolations (CheckMK) Local check for authorization/access-control denials
# by an already-authenticated user in the last 24 h: sudo denials
# (every distro), plus SELinux AVC denials (Fedora/RHEL, if
# enforcing) and AppArmor denials (Debian/Ubuntu, if loaded) —
# each source degrades gracefully when not applicable to the host.
# WARN: ≥3 total; CRIT: ≥10 total.
# Installed fleet-wide when the group exists; requires dev_mon_base.
# usr_mon_iploc (CheckMK) Local check reporting this device's current public IP
# and geolocation (via api.ipify.org + ip-api.com — both free,
# no-API-key third-party services); WARNs when the observed
# country changes since the last reading (e.g. a device normally
# in one country suddenly showing up elsewhere). Reports the
# network EGRESS location — a VPN's exit country, not necessarily
# the device's physical location. Self-throttles to one real
# lookup per 30 min regardless of how often CheckMK polls.
# Installed fleet-wide when the group exists; requires dev_mon_base.
#
# Notes:
# - Install scan tools first: add the host to dev_mod_anti-malware.

View File

@ -30,6 +30,14 @@
# 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)
# usr_ctl_dnshostfile Grant write access to /etc/hosts via a POSIX ACL (setfacl).
# Membership resolved to concrete usernames every tick (a
# filesystem ACL can't be checked dynamically). Requires 'acl'.
# usr_ctl_netman Grant modify access to non-Wi-Fi NetworkManager connections
# (IP/gateway/DNS/routes) via connection.permissions — not sudo.
# Wi-Fi is untouched (see usr_ctl_wifi). Requires nmcli.
# usr_ctl_wifi Grant modify access to Wi-Fi NetworkManager connections only
# (SSID/security) — the mirror of usr_ctl_netman. Requires nmcli.
#
# CheckMK monitoring policies (device host-groups, unless noted):
# dev_mon_base Install CheckMK agent; register host in CMK; check: installed packages.
@ -43,8 +51,18 @@
# dev_mon_timeshift Local check for most recent Timeshift snapshot age.
# WARN if >5 days, CRIT if >10 days, CRIT if no snapshots found.
# dev_mon_power Local check for CPU package TDP/power via Intel RAPL or lm-sensors.
# usr_mon_logins (User group) Local check for SSH login attempts in last 24h.
# usr_mon_logins (User group) Local check for failed LOCAL login attempts in
# last 24h (console/tty/greeter — anything via PAM that isn't
# sshd or sudo). Reports: failed + invalid-user counts with thresholds.
# usr_mon_sshlogins (User group) Local check for SSH login attempts in last 24h.
# Reports: successful + failed + invalid-user counts with thresholds.
# usr_mon_rightsviolations (User group) Local check for authorization/access-control denials
# in last 24h: sudo denials (every distro) + SELinux AVC denials
# (if enforcing) + AppArmor denials (if loaded).
# usr_mon_iploc (User group) Local check for this device's public IP + geolocation
# (api.ipify.org + ip-api.com); WARNs on a country change since the
# last reading. Reports network egress location, not necessarily the
# device's physical location. Self-throttled to one lookup/30min.
#
# Prerequisites:
# - Host enrolled in FreeIPA (sssd + ipa CLI available)

View File

@ -152,6 +152,20 @@ _smb_parse_cred() {
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]}"
}
# ── Group member list helper ──────────────────────────────────────────────────
# Returns newline-separated usernames belonging to the given IPA user group.
# Used by capability-grant policies (usr_ctl_*) that apply to concrete user
# accounts rather than relying on runtime SSSD/PAM resolution — filesystem
# ACLs and NetworkManager connection permissions are static profile metadata,
# not something that can be checked dynamically at the point of use the way a
# sudo rule or PATH wrapper can, so the enforcer must compute and re-apply the
# member set on every tick.
_ipa_group_member_users() {
ipa group-show "$1" --all 2>/dev/null \
| awk -F': ' '/Member users:/{print $2; exit}' \
| tr ',' '\n' | sed 's/^ *//; s/ *$//' | grep -v '^$' || true
}
# NOTE: this file previously had an _ansipa_sync_policystore() here, which
# synced *.sh files from the ansipa-policystore SMB share directly into
# $POLICY_DIR with no integrity check before they were sourced as root. That

View File

@ -3,7 +3,13 @@
# dev_mon_base install agent, register host, installed-packages 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)
# usr_mon_logins failed LOCAL login audit (console/tty/greeter — anything
# via PAM that isn't sshd or sudo); user group, requires dev_mon_base
# usr_mon_sshlogins SSH login audit check (user group, but requires dev_mon_base)
# usr_mon_rightsviolations sudo denials + SELinux/AppArmor access-control
# denials (user group, but requires dev_mon_base)
# usr_mon_iploc public IP + geolocation, flags a country change
# (user group, but requires dev_mon_base)
# (ClamAV scan reporting lives in its own policy, dev_mon_clamscan, which runs
# the scan and writes the Ansipa_ClamScan local check the agent here collects.)
# Revert: deregisters the host and removes the agent socket when dev_mon_base leaves.
@ -284,9 +290,40 @@ else
fi'
fi
# ── usr_mon_logins: SSH login audit (requires dev_mon_base agent) ─────
# ── usr_mon_logins: failed LOCAL login audit (requires dev_mon_base agent) ─
# "Local" = anything authenticating via PAM that ISN'T sshd — console/tty
# (login), graphical greeters (gdm/sddm/greetd/lightdm). SSH activity is
# tracked separately by usr_mon_sshlogins; sudo/access-control denials by
# usr_mon_rightsviolations — both excluded here to keep the three clean.
if _mon_usr_active "logins"; then
_cmk_write_check "ansipa_logins" '#!/bin/bash
if command -v journalctl &>/dev/null && journalctl --since "24 hours ago" -q &>/dev/null; then
LINES=$(journalctl --since "24 hours ago" --no-pager -q 2>/dev/null \
| grep -iE "authentication failure|failed login" | grep -viE "sshd|sudo")
else
LINES=$(
{ grep "$(date "+%b %e")" /var/log/secure /var/log/auth.log 2>/dev/null
grep "$(date -d yesterday "+%b %e" 2>/dev/null || true)" /var/log/secure /var/log/auth.log 2>/dev/null
} | grep -iE "authentication failure|failed login" | grep -viE "sshd|sudo"
)
fi
FAILED=0
[[ -n "$LINES" ]] && FAILED=$(grep -c . <<< "$LINES")
INVALID=$(grep -icE "unknown user|user unknown" <<< "$LINES" 2>/dev/null || echo 0)
PERF="failed=${FAILED};10;50 invalid=${INVALID};5;20"
MSG="Local failed logins 24h: ${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
# ── usr_mon_sshlogins: SSH login audit (requires dev_mon_base agent) ──
if _mon_usr_active "sshlogins"; then
_cmk_write_check "ansipa_sshlogins" '#!/bin/bash
count_journal() {
local pat="$1"
journalctl -u sshd --since "24 hours ago" --no-pager -q 2>/dev/null \
@ -308,13 +345,138 @@ else
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"
MSG="SSH 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}"
echo "2 Ansipa_SSHLogins ${PERF} HIGH ACTIVITY — ${MSG}"
elif [[ "$FAILED" -ge 10 ]] || [[ "$INVALID" -ge 5 ]]; then
echo "1 Ansipa_Logins ${PERF} ELEVATED — ${MSG}"
echo "1 Ansipa_SSHLogins ${PERF} ELEVATED — ${MSG}"
else
echo "0 Ansipa_Logins ${PERF} ${MSG}"
echo "0 Ansipa_SSHLogins ${PERF} ${MSG}"
fi'
fi
# ── usr_mon_rightsviolations: authorization/access-control denials ────
# An already-authenticated user attempting something they're not allowed
# to do: sudo denials (works on every distro — sudo always logs these),
# plus SELinux AVC denials (Fedora/RHEL, only if enforcing) and AppArmor
# denials (Debian/Ubuntu, only if loaded) via the journal. Each source
# degrades gracefully when not applicable to this host. This is a
# best-effort union of what's already logged system-wide, not a
# guarantee of catching every possible file-access denial — watching
# arbitrary paths needs auditd watches on specific files, which is a
# deliberate per-host decision this check does not assume for you.
if _mon_usr_active "rightsviolations"; then
_cmk_write_check "ansipa_rightsviolations" '#!/bin/bash
if command -v journalctl &>/dev/null && journalctl --since "24 hours ago" -q &>/dev/null; then
SUDO_LINES=$(journalctl --since "24 hours ago" --no-pager -q 2>/dev/null | grep -i "sudo")
else
SUDO_LINES=$(
{ grep "$(date "+%b %e")" /var/log/secure /var/log/auth.log 2>/dev/null
grep "$(date -d yesterday "+%b %e" 2>/dev/null || true)" /var/log/secure /var/log/auth.log 2>/dev/null
} | grep -i "sudo"
)
fi
SUDO_DENIED=0
[[ -n "$SUDO_LINES" ]] && SUDO_DENIED=$(grep -icE \
"not in the sudoers file|command not allowed|incorrect password attempt|authentication failure" \
<<< "$SUDO_LINES")
SELINUX_DENIED=0
if command -v getenforce &>/dev/null && [[ "$(getenforce 2>/dev/null)" == "Enforcing" ]] \
&& command -v journalctl &>/dev/null && journalctl --since "24 hours ago" -q &>/dev/null; then
SELINUX_DENIED=$(journalctl --since "24 hours ago" --no-pager -q 2>/dev/null | grep -c "avc: denied")
fi
APPARMOR_DENIED=0
if command -v journalctl &>/dev/null && journalctl --since "24 hours ago" -q &>/dev/null; then
APPARMOR_DENIED=$(journalctl --since "24 hours ago" --no-pager -q 2>/dev/null | grep -c "apparmor=\"DENIED\"")
fi
TOTAL=$(( SUDO_DENIED + SELINUX_DENIED + APPARMOR_DENIED ))
PERF="sudo_denied=${SUDO_DENIED} selinux_denied=${SELINUX_DENIED} apparmor_denied=${APPARMOR_DENIED} total=${TOTAL};3;10"
MSG="Rights violations 24h: ${SUDO_DENIED} sudo / ${SELINUX_DENIED} selinux / ${APPARMOR_DENIED} apparmor"
if [[ "$TOTAL" -ge 10 ]]; then
echo "2 Ansipa_RightsViolations ${PERF} HIGH ACTIVITY — ${MSG}"
elif [[ "$TOTAL" -ge 3 ]]; then
echo "1 Ansipa_RightsViolations ${PERF} ELEVATED — ${MSG}"
else
echo "0 Ansipa_RightsViolations ${PERF} ${MSG}"
fi'
fi
# ── usr_mon_iploc: public IP + geolocation, flag a country change ─────
# NOTE: this reports where the device's NETWORK EGRESS appears to be —
# if the host is behind a VPN, that is the VPN exit location, not
# necessarily the device's physical location. Uses two free,
# no-API-key third-party services (api.ipify.org for the IP,
# ip-api.com for geolocation) — the device's public IP is sent to
# both on every actual lookup; see docs/md/ansipa-administration.md.
# CheckMK invokes local checks on its own polling cadence (often every
# 1 min), far more often than external services should be hit, so this
# throttles itself to one real lookup per THROTTLE_SECONDS and replays
# its last result in between. The geolocation call additionally only
# re-fires when the public IP itself has changed.
if _mon_usr_active "iploc"; then
_cmk_write_check "ansipa_iploc" '#!/bin/bash
STATE_DIR="/var/lib/ansipa-policies"
TS_STATE="$STATE_DIR/iploc-last-ts"
IP_STATE="$STATE_DIR/iploc-last-ip"
LOC_STATE="$STATE_DIR/iploc-last-loc"
OUT_STATE="$STATE_DIR/iploc-last-output"
THROTTLE_SECONDS=1800
mkdir -p "$STATE_DIR" 2>/dev/null
NOW=$(date +%s)
LAST_TS=$(cat "$TS_STATE" 2>/dev/null || echo 0)
if [[ -f "$OUT_STATE" ]] && (( NOW - LAST_TS < THROTTLE_SECONDS )); then
cat "$OUT_STATE"
exit 0
fi
_emit() { echo "$1" | tee "$OUT_STATE" >/dev/null; echo "$1"; echo "$NOW" > "$TS_STATE"; }
PUB_IP=$(curl -fsS -m 5 https://api.ipify.org 2>/dev/null \
|| curl -fsS -m 5 https://ifconfig.me/ip 2>/dev/null \
|| curl -fsS -m 5 https://icanhazip.com 2>/dev/null | tr -d "[:space:]")
if [[ -z "$PUB_IP" ]]; then
_emit "3 Ansipa_IPLoc - Could not determine public IP (no internet / lookup services unreachable)"
exit 0
fi
LAST_IP=$(cat "$IP_STATE" 2>/dev/null || echo "")
LAST_LOC=$(cat "$LOC_STATE" 2>/dev/null || echo "")
if [[ "$PUB_IP" == "$LAST_IP" ]] && [[ -n "$LAST_LOC" ]]; then
CUR_LOC="$LAST_LOC"
else
GEO=$(curl -fsS -m 5 "http://ip-api.com/line/${PUB_IP}?fields=status,country,countryCode,regionName,city,isp" 2>/dev/null)
if [[ -z "$GEO" ]]; then
_emit "3 Ansipa_IPLoc - Public IP is ${PUB_IP}, geolocation lookup failed"
exit 0
fi
mapfile -t _G <<< "$GEO"
if [[ "${_G[0]:-}" != "success" ]]; then
_emit "3 Ansipa_IPLoc - Public IP is ${PUB_IP}, geolocation lookup returned: ${_G[0]:-unknown}"
exit 0
fi
CUR_LOC="${_G[1]:-unknown}|${_G[2]:-??}|${_G[3]:-}|${_G[4]:-}|${_G[5]:-}"
fi
IFS="|" read -r CUR_COUNTRY CUR_CC CUR_REGION CUR_CITY CUR_ISP <<< "$CUR_LOC"
LAST_CC=""
if [[ -n "$LAST_LOC" ]] && [[ "$LAST_LOC" != "$CUR_LOC" ]]; then
IFS="|" read -r LAST_COUNTRY LAST_CC _ _ _ <<< "$LAST_LOC"
fi
echo "$PUB_IP" > "$IP_STATE"
echo "$CUR_LOC" > "$LOC_STATE"
if [[ -n "$LAST_CC" ]] && [[ "$LAST_CC" != "$CUR_CC" ]]; then
_emit "1 Ansipa_IPLoc - COUNTRY CHANGED: ${LAST_COUNTRY} -> ${CUR_COUNTRY} (now ${CUR_CITY}, ${CUR_REGION} - public IP ${PUB_IP}, ${CUR_ISP})"
else
_emit "0 Ansipa_IPLoc - ${CUR_CITY}, ${CUR_REGION}, ${CUR_COUNTRY} (${CUR_ISP}) - public IP ${PUB_IP}"
fi'
fi

View File

@ -0,0 +1,72 @@
#!/usr/bin/env bash
# policy: usr_ctl_dnshostfile — grant specific IPA users write access to
# /etc/hosts via a POSIX ACL. Default (no membership) leaves /etc/hosts at
# its normal root-only permissions — membership is what GRANTS the right,
# it does not restrict anything for non-members.
#
# Scope: /etc/hosts only, deliberately not /etc/resolv.conf. On systems using
# systemd-resolved, /etc/resolv.conf is usually a symlink to a stub file on
# tmpfs that gets regenerated on boot/network changes, so a static ACL there
# would silently vanish; covering it properly would mean resolving and
# re-applying to whatever the symlink currently targets on every run, which
# is a deliberate follow-up rather than something folded in here silently.
#
# Requires: the 'acl' package (setfacl/getfacl) on the client.
# Security note: this is a real grant, not a cosmetic one — a member can
# redirect any hostname to any IP on this machine (local DNS override), which
# is enough to locally spoof a login page or update server. Grant deliberately.
HOSTS_FILE="/etc/hosts"
DNSHOST_STATE="$STATE_DIR/dnshostfile-acl-users"
[[ -f "$DNSHOST_STATE" ]] || touch "$DNSHOST_STATE"
if ! command -v setfacl &>/dev/null; then
warn "usr_ctl_dnshostfile: setfacl not found (install the 'acl' package) — skipping"
else
_dnshost_grp_exists=false
grep -qxF "usr_ctl_dnshostfile" <<< "$_ALL_USER_GROUPS" && _dnshost_grp_exists=true
_DNSHOST_USERS=()
if [[ "$_dnshost_grp_exists" == true ]]; then
while IFS= read -r _u; do
[[ -z "$_u" ]] && continue
_DNSHOST_USERS+=("$_u")
done < <(_ipa_group_member_users usr_ctl_dnshostfile)
fi
_dnshost_in_desired() {
local n="$1"
for _d in "${_DNSHOST_USERS[@]+"${_DNSHOST_USERS[@]}"}"; do [[ "$_d" == "$n" ]] && return 0; done
return 1
}
# Grant: add an ACL entry for every currently-desired user (idempotent).
for _u in "${_DNSHOST_USERS[@]+"${_DNSHOST_USERS[@]}"}"; do
getent passwd "$_u" &>/dev/null || continue
if ! getfacl "$HOSTS_FILE" 2>/dev/null | grep -qxF "user:${_u}:rw-"; then
setfacl -m "u:${_u}:rw" "$HOSTS_FILE" \
&& log "usr_ctl_dnshostfile: granted $_u write access to $HOSTS_FILE" \
|| warn "usr_ctl_dnshostfile: setfacl grant failed for $_u"
fi
done
# Revert: remove the ACL entry for anyone previously granted who is no
# longer in the group (or the group was deleted entirely).
_NEW_DNSHOST_STATE=()
while IFS= read -r _old_u; do
[[ -z "$_old_u" ]] && continue
if _dnshost_in_desired "$_old_u"; then
_NEW_DNSHOST_STATE+=("$_old_u")
else
setfacl -x "u:${_old_u}" "$HOSTS_FILE" 2>/dev/null \
&& log "usr_ctl_dnshostfile: revoked $_old_u write access to $HOSTS_FILE"
fi
done < "$DNSHOST_STATE"
if [[ ${#_DNSHOST_USERS[@]} -gt 0 ]]; then
printf '%s\n' "${_DNSHOST_USERS[@]}" | sort -u > "$DNSHOST_STATE"
else
> "$DNSHOST_STATE"
fi
unset _dnshost_grp_exists _DNSHOST_USERS _u _NEW_DNSHOST_STATE _old_u
fi

View File

@ -0,0 +1,85 @@
#!/usr/bin/env bash
# policy: usr_ctl_netman — grant specific IPA users the ability to modify this
# device's NON-WI-FI NetworkManager connections (IP address, gateway, DNS,
# routes, etc.) — NOT a blanket sudo/root grant, and deliberately NOT extended
# to Wi-Fi connections (see usr_ctl_wifi for that, kept separate on purpose).
#
# Mechanism: NetworkManager already lets a connection's designated non-root
# "permitted user" modify/activate THAT connection without a password
# (org.freedesktop.NetworkManager.settings.modify.own — allowed for local
# users by default on every mainstream distro's polkit rules). This policy
# adds the member to the `connection.permissions` list of every EXISTING
# non-Wi-Fi connection; Wi-Fi connections are left completely untouched, so
# a member can repoint the wired IP/gateway but cannot touch Wi-Fi settings.
#
# Requires: NetworkManager (nmcli) on the client.
# Note: applies to connections that exist at enforcement time; a brand-new
# wired connection created afterwards is picked up on the next 30-min tick.
# Users can already create/manage their OWN new connections by default
# (standard NetworkManager multi-user behavior) — this policy is specifically
# about granting access to EXISTING, previously root-owned connections.
NETMAN_STATE="$STATE_DIR/netman-acl-users"
[[ -f "$NETMAN_STATE" ]] || touch "$NETMAN_STATE"
if ! command -v nmcli &>/dev/null; then
warn "usr_ctl_netman: nmcli not found (NetworkManager not installed) — skipping"
else
_netman_grp_exists=false
grep -qxF "usr_ctl_netman" <<< "$_ALL_USER_GROUPS" && _netman_grp_exists=true
_NETMAN_USERS=()
if [[ "$_netman_grp_exists" == true ]]; then
while IFS= read -r _u; do
[[ -z "$_u" ]] && continue
_NETMAN_USERS+=("$_u")
done < <(_ipa_group_member_users usr_ctl_netman)
fi
_netman_in_desired() {
local n="$1"
for _d in "${_NETMAN_USERS[@]+"${_NETMAN_USERS[@]}"}"; do [[ "$_d" == "$n" ]] && return 0; done
return 1
}
# Every current non-Wi-Fi connection UUID (ethernet, bridge, bond, vlan, ...).
_NON_WIFI_UUIDS=()
while IFS=: read -r _type _uuid; do
[[ -z "$_uuid" ]] && continue
[[ "$_type" == "wifi" || "$_type" == "802-11-wireless" ]] && continue
_NON_WIFI_UUIDS+=("$_uuid")
done < <(nmcli -t -f TYPE,UUID connection show 2>/dev/null)
# Grant: add each desired user to every non-Wi-Fi connection's permissions.
for _u in "${_NETMAN_USERS[@]+"${_NETMAN_USERS[@]}"}"; do
getent passwd "$_u" &>/dev/null || continue
for _uuid in "${_NON_WIFI_UUIDS[@]+"${_NON_WIFI_UUIDS[@]}"}"; do
_perms=$(nmcli -t -g connection.permissions connection show "$_uuid" 2>/dev/null)
if [[ "$_perms" != *"user:${_u}:"* ]]; then
nmcli connection modify "$_uuid" +connection.permissions "user:${_u}:" &>/dev/null \
&& log "usr_ctl_netman: granted $_u modify access to connection $_uuid"
fi
done
done
# Revert: strip any previously-granted user from every non-Wi-Fi connection
# once they're no longer in the group (or the group is gone).
while IFS= read -r _old_u; do
[[ -z "$_old_u" ]] && continue
if ! _netman_in_desired "$_old_u"; then
for _uuid in "${_NON_WIFI_UUIDS[@]+"${_NON_WIFI_UUIDS[@]}"}"; do
_perms=$(nmcli -t -g connection.permissions connection show "$_uuid" 2>/dev/null)
[[ "$_perms" == *"user:${_old_u}:"* ]] && \
nmcli connection modify "$_uuid" -connection.permissions "user:${_old_u}:" &>/dev/null
done
log "usr_ctl_netman: revoked $_old_u network-modify access (left usr_ctl_netman)"
fi
done < "$NETMAN_STATE"
if [[ ${#_NETMAN_USERS[@]} -gt 0 ]]; then
printf '%s\n' "${_NETMAN_USERS[@]}" | sort -u > "$NETMAN_STATE"
else
> "$NETMAN_STATE"
fi
unset _netman_grp_exists _NETMAN_USERS _NON_WIFI_UUIDS _u _uuid _perms _old_u
fi

View File

@ -0,0 +1,81 @@
#!/usr/bin/env bash
# policy: usr_ctl_wifi — grant specific IPA users the ability to modify this
# device's Wi-Fi NetworkManager connections (SSID, PSK/security, etc.). Kept
# as its own group deliberately separate from usr_ctl_netman (wired IP/gateway
# control) — a user can be in either, both, or neither.
#
# Mechanism: identical to usr_ctl_netman but filtered to ONLY Wi-Fi
# connections — adds the member to `connection.permissions` on every existing
# Wi-Fi connection, relying on NetworkManager's own
# org.freedesktop.NetworkManager.settings.modify.own polkit rule (allowed for
# local users by default) to let them edit/activate those connections without
# a password. Wired/other connections are left completely untouched.
#
# Requires: NetworkManager (nmcli) on the client.
# Note: applies to connections that exist at enforcement time; a brand-new
# Wi-Fi connection created afterwards is picked up on the next 30-min tick.
WIFI_STATE="$STATE_DIR/wifi-acl-users"
[[ -f "$WIFI_STATE" ]] || touch "$WIFI_STATE"
if ! command -v nmcli &>/dev/null; then
warn "usr_ctl_wifi: nmcli not found (NetworkManager not installed) — skipping"
else
_wifi_grp_exists=false
grep -qxF "usr_ctl_wifi" <<< "$_ALL_USER_GROUPS" && _wifi_grp_exists=true
_WIFI_USERS=()
if [[ "$_wifi_grp_exists" == true ]]; then
while IFS= read -r _u; do
[[ -z "$_u" ]] && continue
_WIFI_USERS+=("$_u")
done < <(_ipa_group_member_users usr_ctl_wifi)
fi
_wifi_in_desired() {
local n="$1"
for _d in "${_WIFI_USERS[@]+"${_WIFI_USERS[@]}"}"; do [[ "$_d" == "$n" ]] && return 0; done
return 1
}
# Every current Wi-Fi connection UUID.
_WIFI_UUIDS=()
while IFS=: read -r _type _uuid; do
[[ -z "$_uuid" ]] && continue
[[ "$_type" == "wifi" || "$_type" == "802-11-wireless" ]] || continue
_WIFI_UUIDS+=("$_uuid")
done < <(nmcli -t -f TYPE,UUID connection show 2>/dev/null)
# Grant: add each desired user to every Wi-Fi connection's permissions.
for _u in "${_WIFI_USERS[@]+"${_WIFI_USERS[@]}"}"; do
getent passwd "$_u" &>/dev/null || continue
for _uuid in "${_WIFI_UUIDS[@]+"${_WIFI_UUIDS[@]}"}"; do
_perms=$(nmcli -t -g connection.permissions connection show "$_uuid" 2>/dev/null)
if [[ "$_perms" != *"user:${_u}:"* ]]; then
nmcli connection modify "$_uuid" +connection.permissions "user:${_u}:" &>/dev/null \
&& log "usr_ctl_wifi: granted $_u modify access to Wi-Fi connection $_uuid"
fi
done
done
# Revert: strip any previously-granted user from every Wi-Fi connection
# once they're no longer in the group (or the group is gone).
while IFS= read -r _old_u; do
[[ -z "$_old_u" ]] && continue
if ! _wifi_in_desired "$_old_u"; then
for _uuid in "${_WIFI_UUIDS[@]+"${_WIFI_UUIDS[@]}"}"; do
_perms=$(nmcli -t -g connection.permissions connection show "$_uuid" 2>/dev/null)
[[ "$_perms" == *"user:${_old_u}:"* ]] && \
nmcli connection modify "$_uuid" -connection.permissions "user:${_old_u}:" &>/dev/null
done
log "usr_ctl_wifi: revoked $_old_u Wi-Fi-modify access (left usr_ctl_wifi)"
fi
done < "$WIFI_STATE"
if [[ ${#_WIFI_USERS[@]} -gt 0 ]]; then
printf '%s\n' "${_WIFI_USERS[@]}" | sort -u > "$WIFI_STATE"
else
> "$WIFI_STATE"
fi
unset _wifi_grp_exists _WIFI_USERS _WIFI_UUIDS _u _uuid _perms _old_u
fi

View File

@ -171,10 +171,23 @@ for _hgrp in dev_mon_clamscan dev_mon_timeshift dev_mon_power; do
ipa hostgroup-add "$_hgrp" &>/dev/null || true
done
# usr_mon_logins is a USER group (monitoring follows the user, not the device).
# usr_mon_logins / usr_mon_sshlogins / usr_mon_rightsviolations / usr_mon_iploc
# are USER groups (monitoring follows the user, not the device). logins =
# failed LOCAL logins (console/tty/greeter); sshlogins = SSH activity;
# rightsviolations = sudo denials + SELinux/AppArmor access-control denials;
# iploc = public IP + geolocation, flags a country change — each tracked separately.
ipa group-show usr_mon_logins &>/dev/null 2>&1 || \
ipa group-add usr_mon_logins \
--desc="Members: failed local login monitoring enabled on enrolled devices" &>/dev/null || true
ipa group-show usr_mon_sshlogins &>/dev/null 2>&1 || \
ipa group-add usr_mon_sshlogins \
--desc="Members: SSH login monitoring enabled on enrolled devices" &>/dev/null || true
ipa group-show usr_mon_rightsviolations &>/dev/null 2>&1 || \
ipa group-add usr_mon_rightsviolations \
--desc="Members: sudo/SELinux/AppArmor rights-violation monitoring enabled on enrolled devices" &>/dev/null || true
ipa group-show usr_mon_iploc &>/dev/null 2>&1 || \
ipa group-add usr_mon_iploc \
--desc="Members: public IP/geolocation monitoring enabled on enrolled devices" &>/dev/null || true
kdestroy &>/dev/null || true