- Posted on
- • Artificial Intelligence
Artificial Intelligence PAM Audits
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence PAM Audits: Turn Noisy Auth Logs into Actionable Signals
If you’ve ever chased a mysterious lockout, stared at endless auth logs, or wondered if someone quietly tweaked your PAM stack, you know the pain: authentication is both critical and noisy. Linux’s Pluggable Authentication Modules (PAM) gives us flexibility and power—but also more surface area to misconfigure or abuse.
Here’s the good news: you can use a small amount of automation and a dash of AI to make PAM audits smarter, faster, and more reliable—using tools you already trust on Linux plus a lightweight anomaly detector. This guide shows you how to capture the right signals, baseline your PAM stack, and apply AI to flag suspicious behavior before it becomes an incident.
Why “AI for PAM Audits” makes sense
Logs are dense and inconsistent. PAM and auth logs vary across distros, services (sshd, sudo, su, display managers), and modules (pam_unix, pam_sss). Humans miss patterns; machines thrive on them.
Attacks blend into the noise. Password spraying, lateral movement, and misconfig changes are subtle. Anomaly detection can surface “unusual for this host” without brittle rules.
PAM misconfig is risky. A single misplaced pam_permit, disabled pam_faillock, or altered order can break controls. Baselines + change detection + AI triage keep you safe.
What follows is a practical, Linux-first workflow focused on Bash, auditd, journalctl, jq, and a tiny Python script using IsolationForest to flag anomalies.
Prerequisites (install with your package manager)
auditd to watch PAM config changes
jq to normalize journalctl output
Python 3 + pip to run a small anomaly detector
pamtester to safely generate test auth events
Apt (Debian/Ubuntu):
sudo apt update
sudo apt install -y auditd jq python3 python3-pip python3-venv python3-dev build-essential pamtester
DNF (Fedora/RHEL/CentOS Stream):
sudo dnf install -y audit jq python3 python3-pip python3-devel gcc pamtester
Zypper (openSUSE/SLE):
sudo zypper refresh
sudo zypper install -y audit jq python3 python3-pip python3-devel gcc pamtester
OS-specific helpers (optional but handy):
- Fedora/RHEL authselect:
sudo dnf install -y authselect
- openSUSE pam-config:
sudo zypper install -y pam-config
- Debian/Ubuntu pam-auth-update lives in libpam-runtime (usually installed). If needed:
sudo apt install -y libpam-runtime
Step 1: Watch PAM for silent changes (auditd)
Track edits to PAM configuration and related security files. Create a persistent auditd rule:
sudo tee /etc/audit/rules.d/pam.rules >/dev/null <<'EOF'
-w /etc/pam.d/ -p wa -k pam_stack
-w /etc/security/ -p wa -k pam_security
EOF
# Load the new rules
if command -v augenrules >/dev/null; then
sudo augenrules --load
else
sudo systemctl restart auditd || sudo service auditd restart
fi
Quick check for any recent PAM edits:
sudo ausearch -k pam_stack -k pam_security --start today --format text
Why this matters: even a well-meaning admin can introduce risk. Change signals + time window make for fast incident triage.
Step 2: Baseline your PAM stack and lock it in
- Snapshot your current PAM files:
sudo mkdir -p /var/lib/pam-audit
sudo find /etc/pam.d -type f -print0 | sudo xargs -0 sha256sum | sudo tee /var/lib/pam-audit/baseline.sha256
- Capture a readable inventory:
sudo grep -R "^[^#]" /etc/pam.d | sed 's/\t/ /g' | sudo tee /var/lib/pam-audit/pam-inventory.txt
- Validate with OS tools:
- Fedora/RHEL:
authselect current - openSUSE/SLE:
sudo pam-config --list-modules - Debian/Ubuntu:
sudo pam-auth-update --package
- Fedora/RHEL:
On any deviation (hash mismatches, unexpected module orders, or an inserted pam_permit), you’ll have evidence and context.
Step 3: Normalize PAM/auth events from journald
We’ll extract relevant PAM/auth records from journald into line-delimited JSON (NDJSON). This creates structured input for AI/anomaly analysis.
journalctl --since "24 hours ago" -o json \
| jq -r '
select(.SYSLOG_IDENTIFIER
| test("sshd|sudo|su|login|systemd-logind|gdm-password|lightdm|sssd|pam_unix|pam_sss"))
| {
ts: (.__REALTIME_TIMESTAMP|tonumber/1000000|gmtime|strftime("%Y-%m-%dT%H:%M:%SZ")),
host: .HOSTNAME,
unit: .SYSLOG_IDENTIFIER,
pid: ._PID,
msg: .MESSAGE
}
| @json
' | tee /var/lib/pam-audit/pam-events.ndjson
Tip: If your distro logs auth to files (e.g., /var/log/auth.log or /var/log/secure), you can ingest those too. Journald is preferred for consistency and JSON export.
Step 4: Add a lightweight AI anomaly detector
We’ll use a short Python script with IsolationForest to flag “unusual” auth patterns (e.g., many failures from one IP across many users, sudden success-after-fail patterns, late-night spikes). It parses common sshd and pam_unix/pam_sss messages.
Save this as pam_ai_audit.py:
#!/usr/bin/env python3
import json, re, sys, math
from collections import defaultdict, Counter
from datetime import datetime
from sklearn.ensemble import IsolationForest
# Regex patterns for common messages (Debian/Ubuntu/Fedora/EL variants)
RE_SSH_FAIL = re.compile(r'(Failed password for (invalid user )?(?P<user>\S+) from (?P<ip>\d{1,3}(?:\.\d{1,3}){3}))')
RE_SSH_OK = re.compile(r'(Accepted (password|publickey) for (?P<user>\S+) from (?P<ip>\d{1,3}(?:\.\d{1,3}){3}))')
RE_PAM_FAIL = re.compile(r'authentication failure;.*user=(?P<user>\S*) .*rhost=(?P<ip>\S*)')
def parse_line(rec):
msg = rec.get("msg","")
unit = rec.get("unit","")
ts = rec.get("ts")
dt = None
try:
dt = datetime.fromisoformat(ts.replace("Z","+00:00"))
except Exception:
pass
user = None; ip = None; action = None; service = None
m = RE_SSH_FAIL.search(msg)
if m:
user = m.group("user")
ip = m.group("ip")
action = "fail"
service = "sshd"
else:
m = RE_SSH_OK.search(msg)
if m:
user = m.group("user")
ip = m.group("ip")
action = "ok"
service = "sshd"
else:
if "sudo" in unit or "su" in unit or "login" in unit or "pam_unix" in unit or "pam_sss" in unit:
m = RE_PAM_FAIL.search(msg)
if m:
user = (m.group("user") or "").strip() or "unknown"
ip = (m.group("ip") or "").strip()
ip = ip if ip and ip != "=" else "local"
action = "fail"
service = "pam"
if not (user or ip or action):
return None
hour = dt.hour if dt else None
return {
"user": user or "unknown",
"ip": ip or "local",
"action": action,
"service": service or unit,
"hour": hour
}
def vectorize(agg):
# Features per (ip,user,service) key:
# [total, fails, oks, fail_ratio, active_hours, unique_days]
total = agg["total"]
fails = agg["fails"]
oks = agg["oks"]
ratio = fails/total if total else 0
hours = len(agg["hours"])
days = len(agg["days"])
return [total, fails, oks, ratio, hours, days]
def main(path):
events = []
with open(path) as f:
for line in f:
try:
rec = json.loads(line)
except Exception:
continue
p = parse_line(rec)
if p:
# derive day bucket from timestamp if present
ts = rec.get("ts")
day = ts[:10] if ts else "na"
p["day"] = day
events.append(p)
# Aggregate by (ip,user,service)
buckets = defaultdict(lambda: {"total":0, "fails":0, "oks":0, "hours":set(), "days":set()})
for e in events:
key = (e["ip"], e["user"], e["service"])
b = buckets[key]
b["total"] += 1
if e["action"] == "fail":
b["fails"] += 1
else:
b["oks"] += 1
if e["hour"] is not None:
b["hours"].add(e["hour"])
b["days"].add(e["day"])
if not buckets:
print("No parsable PAM/auth events found.")
return
keys = list(buckets.keys())
X = [vectorize(buckets[k]) for k in keys]
# Fit IsolationForest
clf = IsolationForest(
n_estimators=200, contamination=0.03, random_state=42
)
clf.fit(X)
scores = clf.score_samples(X) # more negative = more anomalous
# Rank anomalies
ranked = sorted(zip(keys, X, scores), key=lambda t: t[2])
print("=== Suspicious auth principals (ip,user,service) ===")
for (ip,user,svc), feats, s in ranked[:20]:
total, fails, oks, ratio, hours, days = feats
print(f"- {ip} {user} {svc} | total={total} fails={fails} ok={oks} fail_ratio={ratio:.2f} hours={hours} days={days} score={s:.3f}")
# Simple heuristics worth highlighting
print("\n=== Heuristics ===")
# 1) Many users from one IP (possible password spraying)
by_ip_users = defaultdict(set)
for (ip,user,svc) in keys:
if ip != "local":
by_ip_users[ip].add(user)
for ip, users in sorted(by_ip_users.items(), key=lambda x: -len(x[1]))[:10]:
if len(users) >= 5:
print(f"* IP {ip} targeted {len(users)} users (possible spraying).")
# 2) Success after multiple failures (possible brute-force success)
for (ip,user,svc), feats in zip(keys, X):
total, fails, oks, ratio, hours, days = feats
if fails >= 5 and oks >= 1:
print(f"* {ip} {user} {svc} had {fails} fails then {oks} success (investigate).")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: pam_ai_audit.py /path/to/pam-events.ndjson")
sys.exit(1)
main(sys.argv[1])
Install Python deps:
python3 -m pip install --user scikit-learn
Run it:
python3 pam_ai_audit.py /var/lib/pam-audit/pam-events.ndjson
You’ll get:
Anomaly-ranked principals (ip, user, service) with quick stats
Heuristic flags for password spraying and success-after-fail
Note: This is intentionally lightweight—no agents or daemons—and runs anywhere Python does. You can expand parsers for local conventions.
Step 5: Safely generate test events with pamtester
Use pamtester to create known-good and known-bad attempts and validate your pipeline:
- Trigger a failed sshd auth (enter a wrong password at prompt):
pamtester sshd "$USER" authenticate
- Trigger a failed sudo auth:
pamtester sudo "$USER" authenticate
Then re-export events and re-run the AI audit:
journalctl --since "5 minutes ago" -o json | jq -r 'select(.SYSLOG_IDENTIFIER|test("sshd|sudo|pam_unix|pam_sss")) | {ts:(.__REALTIME_TIMESTAMP|tonumber/1000000|gmtime|strftime("%Y-%m-%dT%H:%M:%SZ")),host:.HOSTNAME,unit:.SYSLOG_IDENTIFIER,pid:._PID,msg:.MESSAGE} | @json' \
| tee -a /var/lib/pam-audit/pam-events.ndjson
python3 pam_ai_audit.py /var/lib/pam-audit/pam-events.ndjson
You should see your test entries influence stats and, if abnormal enough, get flagged.
Real-world signals to watch
Password spraying: One IP touches many users with failures. Your heuristic catches it; the anomaly score should drop for that IP.
Sudden late-night auth bursts: A quiet server suddenly authenticates across many hours; IsolationForest flags the shift.
Success after long failure streaks: Potential compromising guess; gets an explicit heuristic flag.
PAM stack drift: auditd reveals changes to /etc/pam.d or /etc/security that don’t match your baseline hash set.
Operationalizing it (quick wins)
- Cron or systemd timer every hour:
0 * * * * root journalctl --since "-65 minutes" -o json | jq -r 'select(.SYSLOG_IDENTIFIER|test("sshd|sudo|su|pam_unix|pam_sss")) | {ts:(.__REALTIME_TIMESTAMP|tonumber/1000000|gmtime|strftime("%Y-%m-%dT%H:%M:%SZ")),host:.HOSTNAME,unit:.SYSLOG_IDENTIFIER,pid:._PID,msg:.MESSAGE} | @json' >> /var/lib/pam-audit/pam-events.ndjson && /usr/bin/python3 /usr/local/bin/pam_ai_audit.py /var/lib/pam-audit/pam-events.ndjson | logger -t pam-ai-audit
- Alert on change events:
sudo ausearch -k pam_stack -k pam_security --start recent --format short | logger -t pam-audit-change
- Keep data lean: rotate /var/lib/pam-audit files with logrotate.
Conclusion and next steps
You don’t need a full-blown SIEM to get meaningful, AI-assisted PAM audits. With auditd watching your controls, journald exporting clean signals, and a small IsolationForest pass, you can spot unusual auth behavior and config drift quickly—using standard Linux tooling and Bash-friendly workflows.
Next steps:
Tune the regex patterns and features for your environment (e.g., include sssd, Kerberos, or VPN gateways).
Wire the output into your alerting path (email, Slack, syslog to a central collector).
Extend the model: add per-host baselines, holiday/weekend profiles, or service-specific detectors.
If you’d like a follow-up, I can share:
A systemd unit + timer for hands-free hourly audits
A richer parser covering more PAM modules
A dashboard-ready CSV summary for SIEM ingestion
Stay ahead of auth noise—let AI do the sifting, and keep your PAM stack trustworthy.