- Posted on
- • Artificial Intelligence
Artificial Intelligence SSH Security
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Artificial Intelligence SSH Security: Let Your Server Learn to Fight Back
Bots hammer SSH 24/7. They’re faster than ever, they rotate IPs, and they love “low-and-slow” attempts that sneak under static rules. If your logs are full of “Failed password” messages, you’re not alone. The good news: you don’t need a SOC or a data lake to get smarter. In this guide, you’ll wire up a tiny machine-learning assist to your Linux box with a few lines of Bash and Python—so your server adapts to attack patterns in real time.
What you’ll get:
Clear reasons why AI-driven SSH defense is worth it (and when it’s not).
A practical, copy-paste setup using systemd, journalctl, nftables, and a lightweight ML model.
3–5 actionable steps to harden, detect, and auto-block attackers—with install commands for apt, dnf, and zypper.
Why AI for SSH Defense?
Attackers evolve. Static thresholds (e.g., ban after N failures) miss slow-distributed attacks. Simple anomaly detection learns what “normal” events look like for your host and flags the weird stuff—like a sudden flurry, username spraying, or unusual timing.
Defense in depth. AI-scoring complements classics like key-based auth and fail2ban. Keep your basics; add learning on top.
Practical and local. You don’t need to stream logs to the cloud. A small local model (IsolationForest) can score events in milliseconds.
Caveats:
Start conservative to avoid false positives.
Keep an allowlist (office VPN, CI runners).
Monitor early results; tune thresholds gradually.
Step 1: Baseline SSH Hardening (Do This First)
1) Disable password logins and require keys:
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?KbdInteractiveAuthentication.*/KbdInteractiveAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart sshd || sudo systemctl restart ssh
2) Tighten auth parameters:
sudo bash -c 'cat >> /etc/ssh/sshd_config' <<'EOF'
MaxAuthTries 3
LoginGraceTime 20
MaxStartups 10:30:60
EOF
sudo systemctl restart sshd || sudo systemctl restart ssh
3) Optional but helpful: move SSH off 22 (doesn’t replace security, but reduces noise).
sudo sed -i 's/^#\?Port .*/Port 2222/' /etc/ssh/sshd_config
sudo systemctl restart sshd || sudo systemctl restart ssh
Step 2: Install the Pieces (apt, dnf, zypper)
We’ll use:
systemd-journald for logs
nftables for fast, time-limited blocking
Python 3 + pip for a tiny ML model
Optional: fail2ban for classic pattern bans
Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y python3 python3-venv python3-pip nftables fail2ban
# OpenSSH server (if needed):
sudo apt install -y openssh-server
Fedora/RHEL/CentOS Stream (dnf):
sudo dnf install -y python3 python3-pip nftables fail2ban
# OpenSSH server (if needed):
sudo dnf install -y openssh-server
openSUSE Leap/Tumbleweed or SLE (zypper):
sudo zypper refresh
sudo zypper install -y python3 python3-pip nftables fail2ban
# OpenSSH server (if needed; package name is "openssh" on SUSE):
sudo zypper install -y openssh
Enable services if they’re not already on:
sudo systemctl enable --now nftables
sudo systemctl enable --now sshd || sudo systemctl enable --now ssh
# Optional:
sudo systemctl enable --now fail2ban
Step 3: Create an AI-Assisted SSH Guard
We’ll build a small pipeline:
Follow ssh/sshd logs from journalctl.
Extract failed login events and compute simple features per IP.
Score with a small ML model and heuristics.
Block bad IPs quickly using nftables with timeouts.
Directory layout:
/opt/ssh-ai/
venv/ # Python virtualenv
score.py # ML + heuristic scorer
ssh-ai-guard.sh # Bash watcher and auto-block
allowlist.txt # IPs never to ban (one per line)
Set up environment and Python deps:
sudo mkdir -p /opt/ssh-ai
sudo python3 -m venv /opt/ssh-ai/venv
sudo /opt/ssh-ai/venv/bin/pip install --upgrade pip
sudo /opt/ssh-ai/venv/bin/pip install scikit-learn
sudo touch /opt/ssh-ai/allowlist.txt
sudo chmod 640 /opt/ssh-ai/allowlist.txt
nftables rule bootstrap (auto-created by our script too)
We’ll ensure at runtime that a deny set and input rule exist. No need to edit /etc/nftables.conf for this demo.
The ML scorer: /opt/ssh-ai/score.py
Uses a conservative heuristic first.
Falls back to IsolationForest when it has enough observations.
#!/usr/bin/env python3
import os, sys, time, json, math
from pathlib import Path
# Lazy import only if available
try:
from sklearn.ensemble import IsolationForest
HAVE_SK = True
except Exception:
HAVE_SK = False
BASE = Path("/opt/ssh-ai")
OBS = BASE / "observations.csv"
MODEL = BASE / "model.joblib"
def safe_float(x, default=0.0):
try: return float(x)
except: return default
def load_data():
rows = []
if OBS.exists():
with OBS.open() as f:
for line in f:
parts = line.strip().split(",")
if len(parts) != 6: continue
# ts, ip, cnt60, cnt300, users60, avg_gap, hour
rows.append([float(parts[2]), float(parts[3]), float(parts[4]), float(parts[5]), float(parts[6]) if len(parts)>6 else 0.0])
return rows
def save_obs(ip, cnt60, cnt300, users60, avg_gap, hour):
ts = time.time()
with OBS.open("a") as f:
f.write(f"{ts},{ip},{cnt60},{cnt300},{users60},{avg_gap},{hour}\n")
def heuristic(cnt60, cnt300, users60, avg_gap):
# Conservative thresholds you can tune:
# Quick burst
if cnt60 >= 5: return True, "heuristic_cnt60>=5"
# Sustained probing
if cnt300 >= 12: return True, "heuristic_cnt300>=12"
# Username spray
if users60 >= 3: return True, "heuristic_users60>=3"
# Very tight spacing implies automation
if cnt60 >= 3 and avg_gap > 0 and avg_gap < 5: return True, "heuristic_gap<5s"
return False, None
def ml_predict(sample, rows):
if not HAVE_SK or len(rows) < 200:
return 0, "ml_unavailable_or_insufficient_data"
# Train simple IsolationForest (quick and dirty; retrains per call)
try:
from joblib import dump
X = rows[-2000:] # recent window
iso = IsolationForest(n_estimators=100, contamination=0.03, random_state=42)
iso.fit(X)
pred = iso.predict([sample])[0] # -1 anomaly, 1 normal
score = iso.decision_function([sample])[0] # lower = more anomalous
reason = f"iso_pred={pred},score={score:.4f}"
return (-1 if pred == -1 else 1), reason
except Exception as e:
return 0, f"ml_error:{e}"
def main():
if len(sys.argv) < 7:
print("PASS insufficient_args")
sys.exit(0)
ip = sys.argv[1]
cnt60 = safe_float(sys.argv[2])
cnt300 = safe_float(sys.argv[3])
users60 = safe_float(sys.argv[4])
avg_gap = safe_float(sys.argv[5])
hour = safe_float(sys.argv[6])
# Log observation
save_obs(ip, cnt60, cnt300, users60, avg_gap, hour)
# Heuristic first
do_ban, why = heuristic(cnt60, cnt300, users60, avg_gap)
if do_ban:
print(f"BAN {why}")
return
# ML second
rows = load_data()
pred, why_ml = ml_predict([cnt60, cnt300, users60, avg_gap, hour], rows)
if pred == -1:
print(f"BAN {why_ml}")
else:
print(f"PASS {why_ml}")
if __name__ == "__main__":
main()
sudo tee /opt/ssh-ai/score.py >/dev/null <<'PY'
[PASTE THE CONTENTS ABOVE]
PY
sudo chmod +x /opt/ssh-ai/score.py
The watcher + auto-blocker: /opt/ssh-ai/ssh-ai-guard.sh
Follows ssh/sshd logs in real time.
Computes features per IP over sliding windows.
Calls the scorer; bans via nftables set with timeout.
#!/usr/bin/env bash
set -euo pipefail
VENV=/opt/ssh-ai/venv
SCORER=/opt/ssh-ai/score.py
STATE_DIR=/run/ssh-ai
EVENTS_FILE="$STATE_DIR/events.log"
ALLOWLIST=/opt/ssh-ai/allowlist.txt
BAN_TIMEOUT="2h" # adjust as you like
WINDOW_SHORT=60
WINDOW_LONG=300
mkdir -p "$STATE_DIR"
touch "$EVENTS_FILE"
# Ensure nftables building blocks exist (idempotent)
ensure_nft() {
sudo nft list table inet filter >/dev/null 2>&1 || sudo nft add table inet filter
sudo nft list chain inet filter input >/dev/null 2>&1 || sudo nft add chain inet filter input '{ type filter hook input priority 0; }'
sudo nft list set inet filter ssh_deny >/dev/null 2>&1 || sudo nft 'add set inet filter ssh_deny { type ipv4_addr; flags timeout; }'
# Drop rule referencing the set (insert once)
if ! sudo nft list chain inet filter input | grep -q 'ip saddr @ssh_deny drop'; then
sudo nft insert rule inet filter input tcp dport 22 ip saddr @ssh_deny drop
fi
}
ensure_nft
is_allowed() {
local ip="$1"
[[ -f "$ALLOWLIST" ]] && grep -qE "^${ip}$" "$ALLOWLIST"
}
prune_old() {
local now epoch min_epoch
now=$(date +%s)
min_epoch=$(( now - WINDOW_LONG - 5 ))
awk -v m="$min_epoch" '$1 >= m' "$EVENTS_FILE" > "${EVENTS_FILE}.new" || true
mv "${EVENTS_FILE}.new" "$EVENTS_FILE"
}
features_for_ip() {
local ip="$1"
local now=$(date +%s)
local short_from=$(( now - WINDOW_SHORT ))
local long_from=$(( now - WINDOW_LONG ))
local cnt60 cnt300 users60 avg_gap
cnt60=$(awk -v ip="$ip" -v s="$short_from" '$1>=s && $2==ip {c++} END{print c+0}' "$EVENTS_FILE")
cnt300=$(awk -v ip="$ip" -v s="$long_from" '$1>=s && $2==ip {c++} END{print c+0}' "$EVENTS_FILE")
users60=$(awk -v ip="$ip" -v s="$short_from" '$1>=s && $2==ip {u[$3]=1} END{print length(u)}' "$EVENTS_FILE")
# Average inter-arrival gap over long window (rough)
local gaps sum n prev t
sum=0; n=0; prev=""
while read -r t; do
if [[ -n "$prev" ]]; then
gaps=$(( t - prev ))
if (( gaps >= 0 )); then sum=$(( sum + gaps )); n=$(( n + 1 )); fi
fi
prev="$t"
done < <(awk -v ip="$ip" -v s="$long_from" '$1>=s && $2==ip {print $1}' "$EVENTS_FILE" | sort -n)
if (( n > 0 )); then
avg_gap=$(awk -v s="$sum" -v n="$n" 'BEGIN{printf("%.3f", s/n)}')
else
avg_gap="0"
fi
echo "$cnt60" "$cnt300" "$users60" "$avg_gap"
}
ban_ip() {
local ip="$1"
sudo nft add element inet filter ssh_deny { $ip timeout $BAN_TIMEOUT; } || true
logger -t ssh-ai "Banned $ip for $BAN_TIMEOUT"
}
# Follow journald for ssh/sshd; use short-unix to get epoch timestamp
journalctl -o short-unix -u ssh -u sshd -f | \
while IFS= read -r line; do
# Keep only failed attempts or invalid users
[[ "$line" =~ Failed\ password|Invalid\ user ]] || continue
# Extract epoch (first token, may be SEC.USEC)
epoch_raw="${line%% *}"
epoch="${epoch_raw%%.*}"
# Extract IPv4 (demo focuses on IPv4; extend for IPv6 if needed)
ip=$(echo "$line" | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | head -n1)
[[ -n "${ip:-}" ]] || continue
# Extract username heuristic
user=$(echo "$line" | sed -nE 's/.*(Failed password for (invalid user )?|Invalid user )([^ ]+).*/\3/p')
[[ -n "${user:-}" ]] || user="-"
# Allowlist check
if is_allowed "$ip"; then
continue
fi
# Append event and prune
echo "$epoch $ip $user" >> "$EVENTS_FILE"
prune_old
# Compute features and score
read -r f60 f300 u60 gap < <(features_for_ip "$ip")
hour=$(date +%H)
result=$("$VENV/bin/python3" "$SCORER" "$ip" "$f60" "$f300" "$u60" "$gap" "$hour" || echo "PASS scorer_error")
action=${result%% *}
reason=${result#* }
if [[ "$action" == "BAN" ]]; then
ban_ip "$ip"
fi
done
sudo tee /opt/ssh-ai/ssh-ai-guard.sh >/dev/null <<'SH'
[PASTE THE CONTENTS ABOVE]
SH
sudo chmod +x /opt/ssh-ai/ssh-ai-guard.sh
Run it as a systemd service
sudo tee /etc/systemd/system/ssh-ai-guard.service >/dev/null <<'UNIT'
[Unit]
Description=AI-assisted SSH Guard (log watcher + nftables)
After=network-online.target systemd-journald.service
Wants=network-online.target
[Service]
Type=simple
ExecStart=/opt/ssh-ai/ssh-ai-guard.sh
Restart=always
RestartSec=2s
User=root
AmbientCapabilities=CAP_NET_ADMIN
CapabilityBoundingSet=CAP_NET_ADMIN
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-ai-guard.service
Tip: If your distro uses firewalld on top of nftables, this set-based drop rule still works, because it hooks in the inet filter/input chain. If you have custom nftables rules, ensure no later “accept” shadows this drop.
Step 4: Real-World Tuning and Examples
Allowlisting:
- Put trusted IPs or CIDRs (expanded to IPs) into
/opt/ssh-ai/allowlist.txt(one per line). The guard skips those entirely.
- Put trusted IPs or CIDRs (expanded to IPs) into
Test locally:
- From another host, attempt a few failed logins to simulate bursts:
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no user@your.server- Watch bans appear:
sudo watch -n1 'nft list set inet filter ssh_deny' sudo journalctl -u ssh-ai-guard -fSee fewer “low-and-slow” penetrations:
- Username spraying (admin, test, deploy, ubuntu) across a few minutes often evades naive limits. The users60 feature plus ML scoring catches this pattern even when attempts are spaced out.
Persisting across reboots:
- The service re-creates the nftables set/rule on startup and repopulates bans as new events come in. If you want static persistence of the table + rule at boot (before the service starts), you can add an include to /etc/nftables.conf, but for many hosts the dynamic approach is fine.
Combine with fail2ban (optional):
- Let fail2ban handle classic fast bursts with well-tested filters, and keep the AI guard for subtle anomalies.
- Installation:
- apt:
sudo apt install -y fail2ban - dnf:
sudo dnf install -y fail2ban - zypper:
sudo zypper install -y fail2ban - Then
sudo systemctl enable --now fail2banand tune/etc/fail2ban/jail.local.
Step 5: Safety, Observability, Next Steps
Start strict basics first (keys only). Then turn on the AI guard with a higher threshold (e.g., raise heuristic limits) and monitor logs.
Log reasons:
- The scorer outputs why it banned an IP (e.g., heuristic_cnt60>=5, iso_pred=-1). You can extend ssh-ai-guard.sh to write CSV or send metrics to Prometheus/ELK.
Extend to IPv6:
- Add a second set for ipv6_addr and a parallel rule:
sudo nft list set inet filter ssh_deny6 >/dev/null 2>&1 || sudo nft 'add set inet filter ssh_deny6 { type ipv6_addr; flags timeout; }' sudo nft insert rule inet filter input tcp dport 22 ip6 saddr @ssh_deny6 drop- Update ban_ip() to add to ssh_deny6 for IPv6 addresses.
Smarter features:
- Country or ASN (requires geoip), ratio of successes to attempts, diurnal patterns (hour-based baselines), and per-user anomaly scores.
Conclusion and Call to Action
Static SSH defenses stop the obvious stuff; learning-based detection catches the rest. With a few commands and small scripts, your server can:
Observe SSH behavior in real time.
Learn what’s normal for your host.
Block abnormal sources automatically with fast, time-limited bans.
Your next steps: 1) Apply Step 1 hardening immediately. 2) Install the dependencies with apt/dnf/zypper. 3) Deploy the AI guard service and watch your ban set grow while brute-force noise drops. 4) Tune thresholds and allowlist, then extend to IPv6 or SIEM as needed.
If this reduced your nightly “Failed password” flood, consider packaging your tweaks into a role or container and sharing it with your team. Smarter SSH starts here—ship it.