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.
|
||||
|
||||
### `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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue