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
parent
042103f131
commit
cd2f1424c7
|
|
@ -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.
|
`lm_sensors` if RAPL is unavailable.
|
||||||
|
|
||||||
### `usr_mon_logins` (user group)
|
### `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
|
failed + invalid-user), with thresholds. Installed fleet-wide when the group
|
||||||
exists; needs `dev_mon_base` on the hosts.
|
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_*`)
|
## 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
|
fleet-wide on the next 30-min enforcer tick, no redeploy needed. Credential in
|
||||||
the group description (auto-set by `ansipa-smb-setup.sh`).
|
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)
|
## BaseUser sync (`BaseUser` group)
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,48 @@
|
||||||
# automatically: commit and push (GPG-signed) to the ansipa-policy
|
# automatically: commit and push (GPG-signed) to the ansipa-policy
|
||||||
# git server, which ansipa-pull-apply.sh verifies and distributes
|
# git server, which ansipa-pull-apply.sh verifies and distributes
|
||||||
# fleet-wide. See docs/md/freeipa-ansible.md.
|
# 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.
|
# successful + failed + invalid-user counts with thresholds.
|
||||||
# WARN: ≥10 failed or ≥5 invalid; CRIT: ≥50 failed or ≥20 invalid.
|
# WARN: ≥10 failed or ≥5 invalid; CRIT: ≥50 failed or ≥20 invalid.
|
||||||
# Installed fleet-wide when the group exists; requires dev_mon_base.
|
# 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:
|
# Notes:
|
||||||
# - Install scan tools first: add the host to dev_mod_anti-malware.
|
# - Install scan tools first: add the host to dev_mod_anti-malware.
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,14 @@
|
||||||
# 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;
|
# 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)
|
# 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):
|
# 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,8 +51,18 @@
|
||||||
# dev_mon_timeshift Local check for most recent Timeshift snapshot age.
|
# dev_mon_timeshift Local check for most recent Timeshift snapshot age.
|
||||||
# WARN if >5 days, CRIT if >10 days, CRIT if no snapshots found.
|
# 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.
|
# 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.
|
# 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:
|
# Prerequisites:
|
||||||
# - Host enrolled in FreeIPA (sssd + ipa CLI available)
|
# - Host enrolled in FreeIPA (sssd + ipa CLI available)
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,20 @@ _smb_parse_cred() {
|
||||||
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]}"
|
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
|
# NOTE: this file previously had an _ansipa_sync_policystore() here, which
|
||||||
# synced *.sh files from the ansipa-policystore SMB share directly into
|
# synced *.sh files from the ansipa-policystore SMB share directly into
|
||||||
# $POLICY_DIR with no integrity check before they were sourced as root. That
|
# $POLICY_DIR with no integrity check before they were sourced as root. That
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@
|
||||||
# dev_mon_base install agent, register host, installed-packages check
|
# dev_mon_base install agent, register host, installed-packages check
|
||||||
# dev_mon_timeshift Timeshift snapshot age check
|
# dev_mon_timeshift Timeshift snapshot age check
|
||||||
# dev_mon_power CPU package power via RAPL / lm-sensors
|
# 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
|
# (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.)
|
# 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.
|
# Revert: deregisters the host and removes the agent socket when dev_mon_base leaves.
|
||||||
|
|
@ -284,9 +290,40 @@ else
|
||||||
fi'
|
fi'
|
||||||
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
|
if _mon_usr_active "logins"; then
|
||||||
_cmk_write_check "ansipa_logins" '#!/bin/bash
|
_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() {
|
count_journal() {
|
||||||
local pat="$1"
|
local pat="$1"
|
||||||
journalctl -u sshd --since "24 hours ago" --no-pager -q 2>/dev/null \
|
journalctl -u sshd --since "24 hours ago" --no-pager -q 2>/dev/null \
|
||||||
|
|
@ -308,13 +345,138 @@ else
|
||||||
INVALID=$(count_secure "sshd.*Invalid user")
|
INVALID=$(count_secure "sshd.*Invalid user")
|
||||||
fi
|
fi
|
||||||
PERF="successful=${ACCEPTED};; failed=${FAILED};10;50 invalid=${INVALID};5;20"
|
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
|
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
|
elif [[ "$FAILED" -ge 10 ]] || [[ "$INVALID" -ge 5 ]]; then
|
||||||
echo "1 Ansipa_Logins ${PERF} ELEVATED — ${MSG}"
|
echo "1 Ansipa_SSHLogins ${PERF} ELEVATED — ${MSG}"
|
||||||
else
|
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'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -171,10 +171,23 @@ for _hgrp in dev_mon_clamscan dev_mon_timeshift dev_mon_power; do
|
||||||
ipa hostgroup-add "$_hgrp" &>/dev/null || true
|
ipa hostgroup-add "$_hgrp" &>/dev/null || true
|
||||||
done
|
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-show usr_mon_logins &>/dev/null 2>&1 || \
|
||||||
ipa group-add usr_mon_logins \
|
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
|
--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
|
kdestroy &>/dev/null || true
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue