skip to content
afterblue.
Table of Contents

Overview

A VPS will usually receive automated SSH login attempts within minutes or hours of being online. Most of these attempts are background internet noise, but if you’re like me and prefer the default password-based SSH login provided by cloud service providers to avoid the headache, that makes your server much easier to attack.

This guide serves as a record and walks through the practical hardening steps:

  • Inspect SSH logs — confirm whether the server is receiving login attacks.
  • Review successful logins — look for unfamiliar access before changing settings.
  • Move to SSH keys — generate and use SSH key from local disk.
  • Disable password login — remove the main brute-force target.
  • Enable host-level protection — turn on UFW and add Fail2ban.
  • Tighten optional controls — restrict users or change the SSH port when useful.

Assumptions

The commands below use these placeholders (since it’s my set-up,and you should easily find the corresponding commands for other systems simply by asking AI tools):

  • Server OS: Ubuntu 24.04 LTS
  • Local computer: macOS Sequoia
  • SSH user: youruser
  • Server IP: your_server_ip

Replace youruser, your_server_ip, and any other placeholder values with your own details before running the commands.

Before You Begin

If your server is hosted by a cloud provider, also confirm that you have a recovery option, such as a web console, serial console, snapshot, or rescue mode.

Check Recent SSH Activity

Start by reading recent SSH logs:

Terminal window
sudo journalctl -u ssh --since today --no-pager

To show failed SSH login attempts from today:

Terminal window
sudo journalctl -u ssh --since today --no-pager \
| grep -iE "failed|invalid user|authentication failure"

To check the past 7 days:

Terminal window
sudo journalctl -u ssh --since "7 days ago" --no-pager \
| grep -iE "failed password|invalid user|authentication failure"

Common signs of brute-force attempts look like this:

Failed password for invalid user admin from 203.0.113.10 port 51234 ssh2
Failed password for root from 203.0.113.20 port 49822 ssh2
Invalid user test from 203.0.113.30

These usually mean bots are guessing usernames and passwords.

Find the Most Frequent Attacking IP Addresses

Use this command to list the top IP addresses causing failed password attempts:

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

Example output:

128 203.0.113.10
64 198.51.100.25
31 192.0.2.44

The first number is the number of failed attempts. The second value is the source IP address.

Check for Successful SSH Logins

Failed attempts are annoying. Unknown successful logins are serious.

Run:

Terminal window
sudo journalctl -u ssh --since "30 days ago" --no-pager \
| grep -E "Accepted (password|publickey)"

You may see output like this:

Accepted password for youruser from 203.0.113.10 port 53422 ssh2
Accepted publickey for youruser from 203.0.113.10 port 53422 ssh2

Pay close attention to successful password logins:

Accepted password

You can also check current and previous login sessions:

Terminal window
who
w
last -a | head -50
lastlog

Use this quick interpretation table:

Log entry Meaning
Failed password Someone tried a password and failed
Invalid user Someone tried a username that does not exist
Accepted password Someone successfully logged in with a password
Accepted publickey Someone successfully logged in with an SSH key
Unknown IP with Accepted password Investigate immediately

Generate an SSH Key on macOS

Password login is the main risk. The better approach is to use SSH key authentication.

On your Mac, open Terminal and generate an Ed25519 key:

Terminal window
ssh-keygen -t ed25519 -a 64 -C "mac-to-server"

When prompted for a file location, press Enter to accept the default:

/Users/YOU/.ssh/id_ed25519

When prompted for a passphrase, use a strong one.

An SSH key uses a private key on your local machine and a public key on the remote server. A passphrase adds another layer of protection if the private key file is ever copied or exposed.

Add the SSH Key to the macOS ssh-agent

Start the ssh-agent:

Terminal window
eval "$(ssh-agent -s)"

Create or edit your local SSH config:

Terminal window
touch ~/.ssh/config
open ~/.ssh/config

Add this block:

Host my-server
HostName your_server_ip
User youruser
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519

Then add your private key to the agent and macOS Keychain:

Terminal window
ssh-add --apple-use-keychain ~/.ssh/id_ed25519

After this, you can connect with:

Terminal window
ssh my-server

Copy Your Public Key to the Server

If your Mac has ssh-copy-id, run:

Terminal window
ssh-copy-id youruser@your_server_ip

If not, use this command:

Terminal window
cat ~/.ssh/id_ed25519.pub | ssh youruser@your_server_ip \
'mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'

Now test key-based login from a new Terminal window:

Terminal window
ssh youruser@your_server_ip

Or, if you added the Host my-server block:

Terminal window
ssh my-server

Disable SSH Password Login

Once SSH key login works, disable password authentication.

Create a dedicated OpenSSH configuration snippet:

Terminal window
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf >/dev/null <<'EOF'
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
MaxAuthTries 3
EOF

Validate the configuration:

Terminal window
sudo sshd -t

If there is no output, the configuration is valid.

Reload SSH:

Terminal window
sudo systemctl reload ssh.service

Test again from a new Terminal window:

Terminal window
ssh youruser@your_server_ip

Check the active SSH settings:

Terminal window
sudo sshd -T | grep -E 'passwordauthentication|kbdinteractiveauthentication|permitrootlogin|pubkeyauthentication|maxauthtries'

Expected output:

passwordauthentication no
kbdinteractiveauthentication no
permitrootlogin no
pubkeyauthentication yes
maxauthtries 3

At this point, password-based SSH brute-force attempts should no longer work.

Enable the Firewall with UFW

Ubuntu’s default firewall tool is ufw, short for Uncomplicated Firewall. It provides a simpler way to manage host-based firewall rules.

Allow SSH before enabling the firewall:

Terminal window
sudo ufw allow OpenSSH

Enable firewall logging:

Terminal window
sudo ufw logging on

Enable UFW:

Terminal window
sudo ufw enable

Check the firewall status:

Terminal window
sudo ufw status verbose

You should see SSH allowed.

If your local IP address is stable, you can restrict SSH to only that IP:

Terminal window
sudo ufw allow from YOUR_PUBLIC_IP to any port 22 proto tcp

Then remove the broad SSH rule:

Terminal window
sudo ufw status numbered
sudo ufw delete RULE_NUMBER

Install Fail2ban

Fail2ban watches logs for repeated failed login attempts and temporarily bans abusive IP addresses.

Install it:

Terminal window
sudo apt update
sudo apt install fail2ban

Enable and start the service:

Terminal window
sudo systemctl enable --now fail2ban

Create an SSH jail config:

Terminal window
sudo tee /etc/fail2ban/jail.d/sshd.local >/dev/null <<'EOF'
[sshd]
enabled = true
backend = systemd
port = ssh
maxretry = 5
findtime = 10m
bantime = 1h
bantime.increment = true
EOF

Restart Fail2ban:

Terminal window
sudo systemctl restart fail2ban

Check status:

Terminal window
sudo fail2ban-client status
sudo fail2ban-client status sshd

Optional: Allow Only One SSH User

If only one user should be able to SSH into the server, add an AllowUsers rule:

Terminal window
sudo tee -a /etc/ssh/sshd_config.d/99-hardening.conf >/dev/null <<'EOF'
AllowUsers youruser
EOF

Validate and reload SSH:

Terminal window
sudo sshd -t
sudo systemctl reload ssh.service

Test from a new Terminal window:

Terminal window
ssh youruser@your_server_ip

This prevents other local users from logging in over SSH.

Optional: Change the SSH Port

Changing the SSH port can reduce log noise, but it is not a real security boundary. Bots can scan all ports. SSH keys and disabled password login matter much more.

If you still want to change the port, add the new port while keeping port 22 available during testing:

Terminal window
sudo tee -a /etc/ssh/sshd_config.d/99-hardening.conf >/dev/null <<'EOF'
Port 22
Port 2222
EOF

Allow the new port through the firewall:

Terminal window
sudo ufw allow 2222/tcp

Validate and reload SSH:

Terminal window
sudo sshd -t
sudo systemctl reload ssh.service

Test from a new Terminal window:

Terminal window
ssh -p 2222 youruser@your_server_ip

Only after confirming the new port works should you remove port 22 from the SSH config and delete the old firewall rule.

If You Find a Suspicious Successful Login

If you see an unfamiliar Accepted password entry, assume the account password may be compromised.

Check local users:

Terminal window
awk -F: '$3 == 0 || $3 >= 1000 {print $1 ":" $3 ":" $7}' /etc/passwd

Check sudo users:

Terminal window
getent group sudo

Check authorized SSH keys:

Terminal window
sudo find /home /root -maxdepth 3 -name authorized_keys -type f \
-exec ls -l {} \; \
-exec sed -n '1,40p' {} \;

Check recent sudo activity:

Terminal window
sudo journalctl --since "14 days ago" --no-pager | grep -E "sudo:|COMMAND="

Check successful SSH logins again:

Terminal window
sudo journalctl -u ssh --since "30 days ago" --no-pager \
| grep "Accepted"

Then rotate credentials:

Terminal window
passwd
sudo passwd -l root

Update the system:

Terminal window
sudo apt update
sudo apt full-upgrade

Final Hardening Checklist

Use this checklist when setting up a new Ubuntu server:

[ ] Create a normal sudo user
[ ] Generate an Ed25519 SSH key on your Mac
[ ] Add the key to the macOS ssh-agent
[ ] Copy the public key to the server
[ ] Confirm key-based SSH login works
[ ] Disable SSH password login
[ ] Disable keyboard-interactive login
[ ] Disable root SSH login
[ ] Set MaxAuthTries 3
[ ] Enable UFW
[ ] Allow only the ports you need
[ ] Install and enable Fail2ban
[ ] Check logs for suspicious successful logins
[ ] Keep one recovery path available before changing SSH settings

The most important SSH hardening file from this tutorial is /etc/ssh/sshd_config.d/99-hardening.conf:

PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
MaxAuthTries 3

Once password login is disabled and key-based login is working, most SSH password attacks become harmless noise instead of a serious login risk.