#!/bin/zsh # # SSH Failed Login Monitor # Detects failed SSH login attempts and sends alerts via Telegram # Runs every minute via cron for near-instant detection # STATE_DIR="/Users/mattbruce/.openclaw/workspace/scripts/security-monitors/state" LOG_FILE="/Users/mattbruce/.openclaw/workspace/scripts/security-monitors/logs/ssh-monitor.log" ALERT_COOLDOWN_FILE="$STATE_DIR/ssh-alert-cooldown" LAST_CHECK_FILE="$STATE_DIR/ssh-last-check" # Create directories mkdir -p "$(dirname $LOG_FILE)" "$STATE_DIR" # Telegram config - read from OpenClaw config TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-default}" # Timestamp helper timestamp() { date '+%Y-%m-%d %H:%M:%S %Z' } # Log to file log() { echo "[$(timestamp)] $1" >> "$LOG_FILE" } # Send Telegram alert via OpenClaw message tool send_alert() { local message="$1" # Use the OpenClaw delivery queue or message tool # Since we're in a script, we'll write to a queue that can be picked up echo "$(timestamp) | ALERT | $message" >> "$STATE_DIR/alerts.queue" # Also try direct message if possible (this requires the OpenClaw agent to be running) # For now, we log and rely on the agent heartbeat to check this queue } # Get failed SSH attempts since last check check_failed_ssh() { local last_check=0 if [[ -f "$LAST_CHECK_FILE" ]]; then last_check=$(cat "$LAST_CHECK_FILE") fi local current_time=$(date +%s) echo "$current_time" > "$LAST_CHECK_FILE" # Method 1: Check last command for failed attempts local failed_attempts=0 local attempt_details="" # On macOS, use 'last' command and filter for failed entries (they show as still logged in but with date) # Failed SSH attempts typically show in last as entries that don't have a logout time attempt_details=$(last -f /var/log/wtmp 2>/dev/null | grep -E "ssh|pts" | head -20 || echo "") # Method 2: Check macOS unified logs for SSH authentication failures (more reliable) # This captures PAM authentication failures if command -v log >/dev/null 2>&1; then local log_output log_output=$(log show --predicate 'subsystem == "com.openssh.sshd" OR process == "sshd"' --info --last 2m 2>/dev/null | grep -i "failed\|invalid\|authentication failure" | head -10 || echo "") if [[ -n "$log_output" ]]; then failed_attempts=$(echo "$log_output" | wc -l | tr -d ' ') attempt_details="$log_output" fi fi # Method 3: Check system.log for auth failures (fallback) if [[ "$failed_attempts" -eq 0 ]] && [[ -f /var/log/system.log ]]; then local system_log_entries system_log_entries=$(grep -i "authentication failure\|failed password" /var/log/system.log 2>/dev/null | tail -10 || echo "") if [[ -n "$system_log_entries" ]]; then # Count only recent entries (within last 2 minutes) local recent_entries recent_entries=$(echo "$system_log_entries" | grep "$(date '+%b %e')" || echo "") if [[ -n "$recent_entries" ]]; then failed_attempts=$(echo "$recent_entries" | wc -l | tr -d ' ') attempt_details="$recent_entries" fi fi fi # Get source IPs from the attempts local source_ips="" if [[ -n "$attempt_details" ]]; then source_ips=$(echo "$attempt_details" | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | sort -u | head -5 | tr '\n' ', ') fi echo "${failed_attempts}|${source_ips}|${attempt_details}" } # Main monitoring logic main() { local result result=$(check_failed_ssh) local failed_count=$(echo "$result" | cut -d'|' -f1) local source_ips=$(echo "$result" | cut -d'|' -f2) local details=$(echo "$result" | cut -d'|' -f3-) # Check cooldown (don't alert more than once per 5 minutes for same pattern) local cooldown_expired=true if [[ -f "$ALERT_COOLDOWN_FILE" ]]; then local last_alert=$(cat "$ALERT_COOLDOWN_FILE") local current_time=$(date +%s) if [[ $((current_time - last_alert)) -lt 300 ]]; then cooldown_expired=false fi fi if [[ "$failed_count" -gt 0 ]] && [[ "$cooldown_expired" == "true" ]]; then # Log the detection log "⚠️ SECURITY ALERT: $failed_count failed SSH attempt(s) detected" log "Source IPs: $source_ips" # Create alert message local hostname=$(hostname -s) local alert_msg="🚨 **SECURITY ALERT: Failed SSH Login Attempts** 🚨 **Host:** $hostname **Time:** $(timestamp) **Failed Attempts:** $failed_count **Source IP(s):** ${source_ips:-Unknown} Please check system security immediately. _Detected by OpenClaw SSH Monitor_" # Send alert send_alert "$alert_msg" # Update cooldown date +%s > "$ALERT_COOLDOWN_FILE" # Log to daily security log local daily_log="/Users/mattbruce/.openclaw/workspace/memory/$(date '+%Y-%m-%d')-security.log" echo "SSH_FAILED_ATTEMPTS|$(timestamp)|$failed_count|$source_ips" >> "$daily_log" elif [[ "$failed_count" -gt 0 ]]; then log "Detected $failed_count failed SSH attempts (cooldown active, no alert sent)" fi # Always log that we checked (for debugging) if [[ "${DEBUG:-}" == "1" ]]; then log "Check completed: $failed_count failed attempts found" fi } # Run main function main "$@"