ssh
27 TopicsSSH Brute Force Protection with SSH Proxy
This script is designed to add brute force protection for SSH services on a virtual server. It makes use of the Protocol Security feature called SSH Proxy which makes it possible to look into the ssh session and see the actual logon results. Documentation for setting up SSH Proxy can be found here: https://techdocs.f5.com/en-us/bigip-14-1-0/big-ip-network-firewall-policies-and-implementations-14-1-0/ssh-proxy-security.html The script basically looks in the ltm log for these lines: Jun 8 11:40:57 f5-01 info tmm[12656]: 23003164 "Jun 08 2025 11:40:57","ssh_serverside_auth_fail","10.1.0.2","10.10.9.64","49862","22","4092","TCP","root","Password authentication failure" and counts the number of failed attempts for each IP. When a threshold has been reached that IP goes into a AFM address-list. This address-list needs to be part of a rule and policy, and attached to the SSH VS. This way be can block access for that source. The name of the address-list is specified in a variable in the script, so you can adjust it easily for your needs. You should use iCall to run the script periodically: sys icall script ssh_bf { app-service none definition { exec /shared/scripts/ban_failed_ssh.sh > /var/log/ssh_log.log } description none events none } sys icall handler periodic ssh_bf { interval 300 script ssh_bf } Make sure that the location of the script matches the exec statement in the iCall script, and that the script is executable. If you are having an HA, you also need to make sure you have it on both units. You can consume the above config by running this command: tmsh load sys config merge from-terminal and simply paste it in and on an empty line hit CTRL-d. Next you need to have a logging profile configured with these settings: It is important you have that particular publisher set as it is sending the failures to the ltm log. You should be aware of the limitations of logging locally before you make use of this solution. If it is a very busy unit the disk might not be happy with some extra writing. In a perfect world you could log these failures to a remote logging server, but then you need to have the counting logic running elsewhere and have the script use the API to ingest the IPs. Or make use of a IP Intelligence feed which can load the IPs from a webserver. This is however out of scope for this solution, but I might make a follow up article on this if time allows it. With the AFM policy on the SSH VS and the logging profile attached, you now only need to save this script to your favorite script location on the BigIP: #!/bin/bash # # ban_failed_ssh.sh - Detect and ban IPs with repeated SSH login failures from /var/log/ltm # # This script uses AFM address lists and maintains a state file with banned IPs and expiry timestamps. # Author: Thomas Domingo Dahlmann # # --- Configuration --- LOG_FILE="/var/log/ltm" STATE_FILE="/var/tmp/ssh_ban_state.csv" AFM_ADDR_LIST="/Common/ssh_banned_ips" DUMMY_IP="192.0.2.254" BAN_THRESHOLD=3 BAN_WINDOW_SECONDS=300 # 5 minutes BAN_DURATION_SECONDS=1800 # 1 hour TMP_LOG="/var/tmp/ssh_ban_tmp.log" DEBUG=1 # Set to 1 for verbose debug logging, 0 to disable # --- Logging helper --- log_debug() { [[ "$DEBUG" -eq 1 ]] && echo "[DEBUG] $(date '+%F %T') - $*" >&2 } # --- Ensure state file and dummy IP exists --- ensure_state_file() { touch "$STATE_FILE" if ! grep -q "^$DUMMY_IP," "$STATE_FILE"; then echo "$DUMMY_IP,9999999999" >> "$STATE_FILE" log_debug "Adding dummy IP $DUMMY_IP to state file and AFM list" tmsh modify security firewall address-list "$AFM_ADDR_LIST" { addresses add { "$DUMMY_IP" } } 2>/dev/null else log_debug "Dummy IP $DUMMY_IP already present in state" fi } # --- Remove expired IPs from state and AFM --- cleanup_expired_bans() { local now now=$(date +%s) local new_state=() log_debug "Cleaning up expired bans at epoch time $now" while IFS=',' read -r ip expiry; do [[ -z "$ip" || -z "$expiry" ]] && continue if [[ "$ip" == "$DUMMY_IP" ]]; then new_state+=("$ip,9999999999") continue fi if (( expiry > now )); then log_debug "Keeping active ban for $ip (expires at $expiry)" new_state+=("$ip,$expiry") else log_debug "Unbanning expired IP $ip (expired at $expiry)" tmsh modify security firewall address-list "$AFM_ADDR_LIST" { addresses delete { "$ip" } } 2>/dev/null fi done < "$STATE_FILE" printf "%s\n" "${new_state[@]}" > "$STATE_FILE" log_debug "State file rewritten with ${#new_state[@]} active entries" } # --- Add or update ban for IP --- ban_ip() { local ip=$1 local expiry=$(( $(date +%s) + BAN_DURATION_SECONDS )) log_debug "Initiating ban for IP $ip until $(date -d "@$expiry") [$expiry]" # Remove old entry grep -v "^$ip," "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE" log_debug "Removed previous entry for $ip (if any)" # Add new ban echo "$ip,$expiry" >> "$STATE_FILE" log_debug "Added $ip,$expiry to $STATE_FILE" # Add to AFM list tmsh modify security firewall address-list "$AFM_ADDR_LIST" { addresses add { "$ip" } } 2>/dev/null log_debug "Added IP $ip to AFM address list $AFM_ADDR_LIST" } # --- Check recent log entries for violations --- check_for_new_violations() { local now start_epoch now=$(date +%s) start_epoch=$(( now - BAN_WINDOW_SECONDS )) log_debug "Checking for new violations between $start_epoch and $now" > "$TMP_LOG" awk -v threshold="$start_epoch" ' match($0, /"([A-Z][a-z]{2} [ 0-9][0-9] [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2})","ssh_.*auth_fail","([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)"/, m) { cmd = "date -d \"" m[1] "\" +%s" cmd | getline epoch close(cmd) if (epoch >= threshold) { print epoch, m[2] } } ' "$LOG_FILE" > "$TMP_LOG" log_debug "Parsed entries written to $TMP_LOG:" [[ "$DEBUG" -eq 1 ]] && cat "$TMP_LOG" >&2 awk '{count[$2]++} END { for (ip in count) if (count[ip] >= '"$BAN_THRESHOLD"') print ip }' "$TMP_LOG" | while read -r ip; do log_debug "Found IP with threshold violations: $ip" if grep -q "^$ip," "$STATE_FILE"; then log_debug "IP $ip already in state file, skipping ban" else ban_ip "$ip" fi done } # --- Main --- log_debug "==== SSH Ban Monitor Started ====" ensure_state_file cleanup_expired_bans check_for_new_violations log_debug "==== SSH Ban Monitor Finished ====" Happy hunting 😄82Views1like0Commentsssh authorized_keys softlink missing on F5 Node1
Hello, Need clarification regarding with authorized_keys missing softlink! The difference is that there is a softlink to ‘/var/ssh/root/authorized_keys’ on Node2, which is not on Node1. My question is, why is there such a difference with Node1 and Node2. Any idea? Version: BIG-IP 14.1.0.3 Build 0.0.6 (VE) Node1: [root@node1:ModuleNotLicensed::Standby:In Sync] .ssh # ll total 4307 -rw-r--r--. 1 root root 4395008 May 23 12:12 authorized_keys -rw-r--r--. 1 root root 23 May 23 12:12 config -rw-r--r--. 1 root root 23 May 23 12:12 config.backup lrwxrwxrwx. 1 root root 22 May 18 08:37 identity -> /var/ssh/root/identity lrwxrwxrwx. 1 root root 26 May 18 08:37 identity.pub -> /var/ssh/root/identity.pub -rw-------. 1 root root 891 May 21 04:25 id_rsa -rw-r--r--. 1 root root 259 May 21 04:25 id_rsa.pub lrwxrwxrwx. 1 root root 25 May 18 08:37 known_hosts -> /var/ssh/root/known_hosts -rw-r--r--. 1 root root 262 Mar 25 18:22 README Node2: [root@node2:ModuleNotLicensed::Active:In Sync] .ssh # ll total 14 lrwxrwxrwx. 1 root root 29 May 18 08:50 authorized_keys -> /var/ssh/root/authorized_keys -rw-r--r--. 1 root root 23 May 23 12:12 config -rw-r--r--. 1 root root 23 May 23 12:12 config.backup lrwxrwxrwx. 1 root root 22 May 18 08:50 identity -> /var/ssh/root/identity lrwxrwxrwx. 1 root root 26 May 18 08:50 identity.pub -> /var/ssh/root/identity.pub -rw-------. 1 root root 887 May 21 04:26 id_rsa -rw-r--r--. 1 root root 259 May 21 04:26 id_rsa.pub lrwxrwxrwx. 1 root root 25 May 18 08:50 known_hosts -> /var/ssh/root/known_hosts -rw-r--r--. 1 root root 262 Mar 25 18:22 README317Views1like0Comments