How to Harden SSH and Stop Brute Force Attacks on Linux Servers — Read the Logs Before You Lock the Door

By banditz

Saturday, January 3, 2026 • 12 min read

Linux terminal showing auth.log with failed SSH login attempts and fail2ban blocking IPs

I’m going to tell you something that’ll either scare you or make you shrug depending on how long you’ve been managing Linux servers: the VPS you spun up 20 minutes ago is already being attacked.

Not by a person sitting in a dark room with a hoodie — by bots. Thousands of them. Automated scripts running on compromised machines across the globe, systematically scanning every single IP address on the internet, looking for SSH servers running on port 22 with password authentication enabled. They try root/admin, root/password, admin/123456, deploy/deploy, ubuntu/ubuntu, and about ten thousand other common credential combos.

This isn’t hypothetical. Go look at your auth log right now:

sudo tail -200 /var/log/auth.log

If your server has been online for more than an hour with SSH on port 22, you’ll see a wall of failed login attempts from IP addresses you’ve never seen before. That’s the background radiation of the internet. It never stops. And if your only defense is a password — even a decent one — you’re playing a statistical game that you’ll eventually lose.

What the Bots Are Actually Doing

Before you start hardening things, it helps to understand the attack pattern. Most people picture brute force as one bot trying password1, password2, password3 at lightning speed against a single account. That’s the version from 2005.

Modern SSH brute force is smarter. Here’s what actually shows up in your logs:

Mar 28 04:17:32 web01 sshd[12847]: Invalid user admin from 185.224.128.47 port 42816

Mar 28 04:17:34 web01 sshd[12849]: Invalid user test from 185.224.128.47 port 42920

Mar 28 04:17:36 web01 sshd[12851]: Invalid user oracle from 185.224.128.47 port 43018

Mar 28 04:17:38 web01 sshd[12853]: Invalid user postgres from 185.224.128.47 port 43112

Mar 28 04:17:40 web01 sshd[12855]: Failed password for root from 185.224.128.47 port 43200 ssh2

Notice what’s happening. The bot isn’t just hammering root. It’s cycling through usernames — admin, test, oracle, postgres, deploy, git, jenkins, ubuntu — because these are default accounts that exist on millions of servers, and people frequently leave them with weak or default passwords.

The smarter bots also throttle their attempts. Instead of 100 attempts per second (which is trivially detectable), they’ll do 3 attempts, wait 60 seconds, try 3 more. Some distribute the attack across multiple IPs from the same botnet, so no single IP triggers rate limiting.

On RHEL/CentOS systems, the same information lives in /var/log/secure instead of /var/log/auth.log. The format is identical.

To see a summary of who’s been trying to get in:

grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20

This gives you a ranked list of the most persistent attacking IPs. On a server that’s been online for a week, don’t be surprised if the top IP has thousands of attempts.

Now let’s shut them down properly.

Step 1: Switch to SSH Key-Only Authentication

This is the nuclear option against brute force, and it should be the first thing you do on any server. Not the last. Not “eventually.” First.

SSH key authentication replaces passwords with a cryptographic key pair. Your private key stays on your local machine, your public key goes on the server. When you connect, the server challenges your client to prove it has the private key without ever transmitting it. No password is sent. No password can be guessed. Brute force doesn’t work because there’s nothing to brute force.

Generate a key pair on your local machine (if you don’t already have one):

ssh-keygen -t ed25519 -C "you@yourdomain.com"

Ed25519 is the modern choice — it’s faster, shorter, and more secure than RSA. If you’re dealing with legacy systems that don’t support ed25519, fall back to RSA with a 4096-bit key:

ssh-keygen -t rsa -b 4096

Copy the public key to your server:

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@your-server-ip

Test key login before disabling passwords. Open a new terminal and connect:

ssh user@your-server-ip

If you get in without being asked for a password, key auth is working. Keep this session open as a safety net.

Now disable password authentication. Edit the SSH daemon config:

sudo nano /etc/ssh/sshd_config

Find and set these three directives:

PasswordAuthentication no

PubkeyAuthentication yes

ChallengeResponseAuthentication no

That third one is important. ChallengeResponseAuthentication can bypass PasswordAuthentication on some PAM configurations, effectively keeping password login alive even when you think you’ve disabled it. Set it to no.

Also disable empty passwords while you’re in there:

PermitEmptyPasswords no

Test the config before applying:

sudo sshd -t

If it returns silently with no errors, restart:

sudo systemctl restart sshd

From this point on, every brute force attempt in the world is wasted effort against your server. The bots will keep trying — they don’t know you’ve disabled passwords — but every single attempt fails instantly. Check your auth log after a few minutes:

sudo tail -50 /var/log/auth.log

You’ll see the attempts now ending with Connection closed by authenticating user or Disconnected from authenticating user instead of Failed password. They can’t even get to the password prompt.

Step 2: Lock Down sshd_config

Key-only auth handles brute force, but there’s more to SSH security than just authentication. Open /etc/ssh/sshd_config and layer these additional restrictions:

Disable root login:

PermitRootLogin no

Even with key-only auth, allowing direct root login is bad practice. If an attacker somehow gets your private key (stolen laptop, compromised backup), they’d have immediate root access. Force the use of a regular user account that escalates with sudo.

If you absolutely must allow root key-based login (some automation tools require it), use:

PermitRootLogin prohibit-password

This allows root login with SSH keys but not passwords. It’s a compromise, not ideal, but better than yes.

Restrict which users can log in:

AllowUsers deployer admin

This is a whitelist. Only the users listed here can SSH in. Everyone else — even users with valid system accounts and SSH keys — gets rejected. This is powerful because it means a compromised application user (like www-data or postgres) can’t be leveraged for SSH access even if an attacker manages to plant a key in their authorized_keys file.

Limit authentication attempts and timing:

MaxAuthTries 3

LoginGraceTime 20

MaxAuthTries 3 means after 3 failed authentication attempts within a single connection, the server disconnects. LoginGraceTime 20 gives only 20 seconds to complete authentication — if you haven’t authenticated in 20 seconds, you’re disconnected. Legitimate users authenticate in under 2 seconds; only bots and manual attackers need more time.

Disconnect idle sessions:

ClientAliveInterval 300

ClientAliveCountMax 2

The server sends a keepalive probe every 300 seconds (5 minutes). If the client doesn’t respond to 2 consecutive probes, the connection is terminated. This prevents abandoned sessions from sitting open indefinitely, which reduces the window for session hijacking.

Disable unnecessary features:

X11Forwarding no

AllowTcpForwarding no

AllowAgentForwarding no

Unless you specifically need X11 forwarding (running graphical apps over SSH), TCP forwarding (tunneling), or agent forwarding (chaining SSH connections), disable them. Each enabled feature is a potential attack vector. Turn them off by default and enable them selectively when needed.

After making all changes, test and restart:

sudo sshd -t

sudo systemctl restart sshd

Always — and I mean always — keep an existing SSH session open while you restart sshd. If you made a typo that prevents new connections, your existing session stays alive and lets you fix it. If you close your only session before testing, and the new config has an error, you’re locked out and praying your VPS provider has a console rescue option.

Step 3: Configure Fail2ban

Key-only auth makes brute force ineffective, but the bots don’t know that. They’ll keep hammering your server, consuming bandwidth and filling your logs. Fail2ban fixes this by watching your auth log and automatically firewall-blocking IPs that fail too many times.

Install fail2ban:

sudo apt install fail2ban      # Debian/Ubuntu

sudo dnf install fail2ban      # RHEL/CentOS/Fedora

Create a local config (never edit jail.conf directly — it gets overwritten on updates):

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Edit the local config:

sudo nano /etc/fail2ban/jail.local

Find the [sshd] section and configure it:

[sshd]

enabled = true

port = ssh

filter = sshd

logpath = /var/log/auth.log

maxretry = 3

findtime = 600

bantime = 3600

This means: if an IP fails 3 times within 10 minutes (600 seconds), ban it for 1 hour (3600 seconds). For most servers, this is a reasonable starting point.

For servers that take a real beating, you can get more aggressive:

bantime = 86400          # 24-hour ban

findtime = 3600          # 3 failures within an hour

maxretry = 3

Add the recidive jail for repeat offenders. This catches IPs that get banned, wait out the ban, and come back. Add this to the bottom of jail.local:

[recidive]

enabled = true

logpath = /var/log/fail2ban.log

banaction = %(banaction_allports)s

bantime = 604800

findtime = 86400

maxretry = 3

This watches fail2ban’s own log. If an IP gets banned 3 times within 24 hours, the recidive jail bans them for a full week across all ports. Persistent bots learn the hard way.

Start and enable fail2ban:

sudo systemctl enable fail2ban

sudo systemctl start fail2ban

Check the status:

sudo fail2ban-client status sshd

You’ll see the number of currently banned IPs and the total number of bans since fail2ban started. On a fresh server with SSH on port 22, expect this number to climb quickly.

Unban an IP (if you accidentally lock yourself out):

sudo fail2ban-client set sshd unbanip 203.0.113.50

Pro tip: whitelist your own IP so fail2ban never bans you, even if you fat-finger your key passphrase multiple times:

# In jail.local, under [DEFAULT]

ignoreip = 127.0.0.1/8 ::1 203.0.113.50

Replace 203.0.113.50 with your actual IP or IP range.

Step 4: Reduce the Noise — Change the Port

I’ll be upfront about this: changing the SSH port is not a security measure. It’s a noise reduction measure. Any attacker who specifically targets your server will find your SSH port within seconds using a port scan. Changing it from 22 to 2222 or 4822 or 39122 doesn’t add meaningful security against a determined attacker.

What it does do is eliminate 99% of the automated bot traffic that exclusively targets port 22. After changing the port, your auth.log goes from thousands of daily entries to nearly zero. This makes it much easier to spot real threats among the remaining entries.

Change the port in sshd_config:

sudo nano /etc/ssh/sshd_config

Change:

Port 22

To:

Port 4822

Update the firewall BEFORE restarting SSH (otherwise you lock yourself out):

For UFW:

sudo ufw allow 4822/tcp

sudo ufw delete allow 22/tcp

For iptables:

sudo iptables -A INPUT -p tcp --dport 4822 -j ACCEPT

sudo iptables -D INPUT -p tcp --dport 22 -j ACCEPT

Update fail2ban to watch the new port. In jail.local, change the sshd section:

[sshd]

port = 4822

Restart everything:

sudo sshd -t

sudo systemctl restart sshd

sudo systemctl restart fail2ban

Connect from a new terminal using the new port:

ssh -p 4822 user@your-server-ip

Keep your old session alive until you confirm the new connection works.

Step 5: IP Whitelisting and Port Knocking (For the Paranoid)

If you always connect from the same IP or IP range — say, your home IP or your office VPN — you can lock SSH down to only accept connections from those addresses.

With UFW:

sudo ufw allow from 203.0.113.0/24 to any port 4822

This means only IPs in the 203.0.113.0/24 range can even reach the SSH port. Everyone else gets a timeout. The bots don’t even see that SSH exists on your server.

The problem with IP whitelisting is that your IP might change (dynamic ISP, travel, different networks). If your IP changes and you haven’t updated the whitelist, you’re locked out.

Port knocking solves this problem. It keeps the SSH port completely closed — invisible to port scans — until a client sends a specific sequence of connection attempts to other ports in the correct order.

Install knockd:

sudo apt install knockd

Configure it:

sudo nano /etc/knockd.conf

[options]

    UseSyslog

[openSSH]

    sequence = 7000,8000,9000

    seq_timeout = 15

    command = /usr/sbin/ufw allow from %IP% to any port 4822

    tcpflags = syn

[closeSSH]

    sequence = 9000,8000,7000

    seq_timeout = 15

    command = /usr/sbin/ufw delete allow from %IP% to any port 4822

    tcpflags = syn

This configuration works like a secret handshake. To open SSH, you knock on ports 7000, 8000, 9000 in that exact order within 15 seconds. To close it afterward, knock in reverse: 9000, 8000, 7000.

From your local machine, knock with:

knock your-server-ip 7000 8000 9000

ssh -p 4822 user@your-server-ip

# When done:

knock your-server-ip 9000 8000 7000

Or if you don’t have the knock client, you can use nmap:

nmap -Pn --host-timeout 201 --max-retries 0 -p 7000 your-server-ip

nmap -Pn --host-timeout 201 --max-retries 0 -p 8000 your-server-ip

nmap -Pn --host-timeout 201 --max-retries 0 -p 9000 your-server-ip

Port knocking is overkill for most setups. But if you’re managing a server that handles sensitive data, or if you just want the satisfaction of knowing that your SSH port is completely invisible to the entire internet, it’s a satisfying layer to add.

The SSH Hardening Checklist

Here’s the complete order of operations for any new Linux server:

  1. Generate SSH keys on your local machine (if you haven’t already)
  2. Copy the public key to the server with ssh-copy-id
  3. Test key login before touching any config
  4. Disable password authenticationPasswordAuthentication no
  5. Disable root loginPermitRootLogin no
  6. Restrict usersAllowUsers with only the accounts that need SSH
  7. Limit attempts and timeoutsMaxAuthTries 3, LoginGraceTime 20
  8. Install fail2ban — configure with reasonable ban times
  9. Change the SSH port — reduces log noise from automated scanners
  10. Whitelist IPs or set up port knocking — if you connect from known locations

Do them in this order. Each layer addresses a different threat, and together they make SSH brute force a non-issue. The bots will keep scanning, the botnets will keep running, but your server is no longer a target that can yield results.

I’ve been watching these logs for decades. The attacks never stop, they only evolve. But a properly hardened SSH setup hasn’t changed much in that time either — because the fundamentals work. Keys beat passwords. Automatic bans beat manual blocking. And a healthy dose of paranoia beats blind trust every single time.


If you found this guide helpful, check out our other resources:

  • (More articles coming soon in the Cyber Security category)

Step-by-Step Guide

1

Read auth.log to understand what is actually hitting your server

Before hardening anything check what is already happening. Run sudo tail -200 /var/log/auth.log on Debian or Ubuntu systems or sudo tail -200 /var/log/secure on RHEL or CentOS systems. Look for lines containing Failed password and Invalid user. The Failed password lines show attempts against real accounts on your system. The Invalid user lines show bots trying common usernames like admin test deploy git oracle and postgres. Note the IP addresses and how frequently they appear. You will likely see hundreds or thousands of attempts from dozens of different IPs. This is normal for any internet-facing server and it never stops. Understanding the attack pattern tells you which defenses matter most.

2

Disable password authentication and switch to SSH key-only login

The single most effective defense against brute force attacks is eliminating passwords entirely. SSH key authentication makes brute force mathematically impossible because an attacker would need to guess a 2048-bit or 4096-bit private key. First generate a key pair on your local machine with ssh-keygen -t ed25519 -C your_email@example.com. Copy the public key to your server with ssh-copy-id user@your-server. Test that key login works by connecting with ssh user@your-server without being asked for a password. Then edit /etc/ssh/sshd_config and set PasswordAuthentication no and PubkeyAuthentication yes and ChallengeResponseAuthentication no. Restart SSH with sudo systemctl restart sshd. After this change password brute force attempts will all fail instantly because the server no longer accepts passwords at all.

3

Harden sshd_config with restrictive settings

Open /etc/ssh/sshd_config with sudo nano /etc/ssh/sshd_config and apply these settings. Set PermitRootLogin no to prevent direct root access. Set MaxAuthTries 3 to limit authentication attempts per connection. Set LoginGraceTime 20 to give only 20 seconds to authenticate before disconnecting. Set AllowUsers followed by the specific usernames that should be allowed to SSH in which blocks all other users even if they have valid accounts. Set ClientAliveInterval 300 and ClientAliveCountMax 2 to disconnect idle sessions after 10 minutes. Set X11Forwarding no unless you specifically need it. Test the configuration with sudo sshd -t before restarting. If the test passes restart with sudo systemctl restart sshd. Always keep an existing SSH session open while testing so you do not lock yourself out.

4

Install and configure fail2ban to automatically ban attacking IPs

Fail2ban watches your auth log for failed login attempts and automatically blocks offending IPs using firewall rules. Install with sudo apt install fail2ban on Debian or Ubuntu. Create a local configuration file with sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local. Edit jail.local and under the sshd section set enabled to true, set port to your SSH port, set filter to sshd, set logpath to /var/log/auth.log, set maxretry to 3, set bantime to 3600 for a one hour ban, and set findtime to 600 which means 3 failures within 10 minutes triggers a ban. For repeat offenders add the recidive jail which bans IPs that get banned multiple times for a longer period such as one week. Start fail2ban with sudo systemctl enable fail2ban and sudo systemctl start fail2ban. Check banned IPs with sudo fail2ban-client status sshd.

5

Add additional layers with non-standard port and firewall rules

Changing the SSH port from 22 to a non-standard port like 2222 or 4822 does not add real security but it eliminates the noise from automated scanners that only target port 22. Edit /etc/ssh/sshd_config and change Port 22 to your chosen port. Update your firewall rules before restarting SSH. For UFW run sudo ufw allow 4822/tcp then sudo ufw delete allow 22/tcp. For iptables run sudo iptables -A INPUT -p tcp --dport 4822 -j ACCEPT. Additionally restrict SSH access to specific IP ranges if you connect from known locations by using sudo ufw allow from 203.0.113.0/24 to any port 4822. For the most paranoid setups implement port knocking using knockd which keeps the SSH port completely closed until a specific sequence of connection attempts on other ports is received in the correct order.

Frequently Asked Questions

Is changing the SSH port enough to stop brute force attacks?
No. Changing the port from 22 to something else only stops the laziest automated scanners that exclusively target port 22. Any attacker running a port scan with nmap or masscan will find your SSH port within seconds regardless of which port it runs on. Changing the port reduces log noise significantly which makes it easier to spot real attacks among fewer entries but it should never be your only defense. Think of it as closing the screen door. It keeps the flies out but it will not stop anyone who actually wants to get in. Real protection comes from key-only authentication and fail2ban.
What happens if I lock myself out after disabling password authentication?
This is the most common fear and the reason you should always test carefully. Before disabling password authentication make sure your SSH key login works by opening a second terminal and connecting with your key while the first session stays open. If key login works you are safe to disable passwords. If you do lock yourself out most VPS providers offer a web-based console or VNC access that lets you log in directly without SSH. From there you can re-enable password authentication temporarily to fix the issue. Some providers like DigitalOcean and Hetzner also let you boot into a recovery mode and mount your filesystem to edit sshd_config directly.
How many failed SSH attempts per day is normal for a public server?
A freshly deployed server with SSH on port 22 will typically receive anywhere from 500 to 10000 failed login attempts per day depending on the IP address range. Some IP ranges are scanned more aggressively than others. This is completely normal background noise from botnets constantly scanning the entire IPv4 address space. With key-only authentication enabled these attempts are harmless because no password will ever be accepted. With fail2ban running most of these IPs get banned after their first few attempts. After changing to a non-standard port the attempts typically drop to near zero because most bots only probe port 22.
Should I use fail2ban or sshguard?
Both do essentially the same job of monitoring logs and banning IPs that show brute force behavior. Fail2ban is more widely used and has more community documentation configuration examples and tutorials. It is written in Python and is very flexible because it supports custom jails for protecting services beyond SSH. Sshguard is lighter weight because it is written in C and uses less memory and CPU. For most people fail2ban is the better choice simply because you will find more help when you need it. If you are running a very resource-constrained server such as a 256 MB RAM VPS then sshguard's lower resource usage might matter. Either one combined with key-only authentication provides excellent protection.
banditz

Research Bug bounty at javahack team

Freeland Reseacrh Bug Bounty

View all articles →