SSH brute-force fingerprints: how to read /var/log/auth.log without grep madness — awk one-liners that actually work

Open /var/log/auth.log on a public-facing server and you’ll see thousands of lines per day — failed logins, accepted logins, sudo events, cron registrations. The signal you usually care about (who’s brute-forcing me, from where, against which users?) is buried in that noise. The tutorials all suggest grep -i fail piped through more grep. That works, technically, and produces a wall of unanalysed text.

Here are the awk one-liners I actually use to turn auth.log into useful summaries — top attacker IPs, most-targeted usernames, attack rate over time. They run in milliseconds even on multi-GB log files and tell you what you need to act on.

The format you’re parsing

A typical failed-login line on Ubuntu 22:

May 06 13:42:17 myhost sshd[12345]: Failed password for invalid user admin from 185.234.219.40 port 50232 ssh2

On systems using rsyslog with the older format the date columns are 1-3 ($1=month, $2=day, $3=time); on systemd-journald-only systems use journalctl -u ssh -n 10000 --no-pager > /tmp/auth.log first.

1. Top attacker IPs (last 24h)

journalctl -u ssh --since "24 hours ago" --no-pager \
  | awk '/Failed password/ {
        for(i=1;i<=NF;i++) if($i=="from") print $(i+1)
    }' \
  | sort | uniq -c | sort -rn | head -20

The trick is the for loop: instead of pinning the IP to a column number that breaks across distros, find the word “from” and print the next field. Works regardless of whether the line is “Failed password from 1.2.3.4” or “Failed password for invalid user X from 1.2.3.4.”

Output looks like:

   2347 185.234.219.40
   1822 192.168.99.12
    910 167.99.10.4
    ...

Anything above ~50 attempts in 24h is a candidate for a targeted ban. Below that is normal background scan noise.

2. Most-targeted usernames

journalctl -u ssh --since "24 hours ago" --no-pager \
  | awk '/Failed password for/ {
        for(i=1;i<=NF;i++) if($i=="for") {
            user=$(i+1)
            if(user=="invalid") user=$(i+3)
            print user
            break
        }
    }' \
  | sort | uniq -c | sort -rn | head -20

The “invalid user X” wrinkle is what most one-liners get wrong. The word “for” appears in two contexts:

  • Failed password for root from ... — root exists, attacker tried it.
  • Failed password for invalid user admin from ... — admin doesn’t exist on the box, but they tried.

Both are interesting, but they’re at different field offsets. The awk above handles both correctly.

Typical output: root, admin, ubuntu, postgres, test, oracle, git. If you see one of your real usernames near the top, that’s a focused attack and worth reacting to.

3. Attack rate per hour

journalctl -u ssh --since "24 hours ago" --no-pager \
  | awk '/Failed password/ {
        # systemd format: "May 06 13:42:17 ..."
        printf "%s %02d:00\n", $1" "$2, $3+0
    }' \
  | cut -d: -f1 | uniq -c

Rolls up failed attempts per hour. Useful for spotting bursts:

     12 May 06 09
     14 May 06 10
   2347 May 06 11   ← burst
   2810 May 06 12   ← burst
     18 May 06 13

A 100× spike across an hour says someone targeted you specifically. Background scanning is steady at ~10-20/hour on a public IP.

4. Successful logins (the line that should make you sit up)

journalctl -u ssh --since "7 days ago" --no-pager \
  | awk '/Accepted (publickey|password)/ {
        for(i=1;i<=NF;i++) {
            if($i=="for") user=$(i+1)
            if($i=="from") ip=$(i+1)
        }
        print $1, $2, $3, user, ip
    }'

Lists every successful SSH login in the last 7 days with timestamp, user, and source IP. Should be a short list. If it isn’t — or if you see a login from an IP you don’t recognise — investigate that account immediately.

5. The “did the brute force work?” check

journalctl -u ssh --since "24 hours ago" --no-pager \
  | awk '/Accepted/ {
        for(i=1;i<=NF;i++) if($i=="from") accepted[$(i+1)]++
    }
    /Failed password/ {
        for(i=1;i<=NF;i++) if($i=="from") failed[$(i+1)]++
    }
    END {
        for(ip in accepted)
            if(failed[ip] > 5)
                print ip, "got in after", failed[ip], "failures"
    }'

This one’s a panic alarm. It finds any IP that had multiple failures and a successful login. Output should be empty. If it’s not, lock down everything.

Putting it together

Save these as /usr/local/sbin/auth-summary.sh and call it from cron:

0 9 * * * /usr/local/sbin/auth-summary.sh | mail -s "Daily SSH report — $(hostname)" you@example.com

9 AM email, two minutes of scan time. The day someone shows up at the top of “got in after N failures,” you’ll know within hours instead of days.

None of this replaces fail2ban / CrowdSec for active blocking. It complements them: those tools act; awk + auth.log shows you what’s happening. Together they’re the difference between confidence and hope.

Cover photo: Tima Miroshnichenko on Pexels.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.