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:
- Generate SSH keys on your local machine (if you haven’t already)
- Copy the public key to the server with
ssh-copy-id - Test key login before touching any config
- Disable password authentication —
PasswordAuthentication no - Disable root login —
PermitRootLogin no - Restrict users —
AllowUserswith only the accounts that need SSH - Limit attempts and timeouts —
MaxAuthTries 3,LoginGraceTime 20 - Install fail2ban — configure with reasonable ban times
- Change the SSH port — reduces log noise from automated scanners
- 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)