How to Secure SSH on a VPS: Keys, Custom Port, and fail2ban
A fresh VPS with root password SSH does not stay quiet for long. Put a server online, leave sshd on port 22 with password login enabled, and the authentication log will often start filling with automated login attempts within hours.
This guide shows how to secure SSH on a VPS in the right order: generate and test an SSH key, disable password login, change the listening port, tune fail2ban, then tighten the firewall. It also keeps the impact of each step honest. Key-only SSH is the real brute-force defense. A custom port reduces scan noise. fail2ban blocks abusive clients and keeps logs cleaner, but it does not replace strong authentication.
The rule for this whole guide is: build the new entrance before closing the old one. Never disable a login method, restart SSH, or remove a firewall rule until the replacement path has been tested from a new terminal.
This tutorial is for people running their own Linux VPS. If you are still choosing between a managed hosting account and root access, read VPS vs shared hosting first.
Before you start: don’t lock yourself out
Keep one working SSH session open for the whole job. Do not close it after a command succeeds. A restart normally keeps existing sessions alive, and that open terminal gives you a way back in if the next login fails.
Run the OpenSSH server config test before every restart:
sudo sshd -t
The -t flag asks sshd to validate configuration and exit without starting a new daemon, as documented in the OpenSSH sshd manual. If it prints an error, fix that error before touching the service.
After every change, open a new terminal and log in again. Move to the next step only after the new session works.
The service name differs by family:
# Debian and Ubuntu
sudo systemctl restart ssh
# AlmaLinux, Rocky Linux, and CentOS Stream
sudo systemctl restart sshd
Step 1: generate an SSH key and add it first
Start with the key because every later step assumes key login already works. OpenSSH supports several key types through ssh-keygen; for normal modern use, Ed25519 is the best default. Use RSA 4096 only when an old system or old client cannot use Ed25519. The ssh-keygen manual documents both key types.
On macOS or Linux:
ssh-keygen -t ed25519 -C "you@host"
The private key is usually saved as ~/.ssh/id_ed25519. The public key is ~/.ssh/id_ed25519.pub.
On Windows, use the built-in OpenSSH client from PowerShell:
ssh-keygen -t ed25519 -C "you@host"
PuTTY users can generate the key with PuTTYgen, then export the public key in OpenSSH format. If you use PuTTY for the actual login, keep the private key in PuTTY’s format. If you use Terminal, PowerShell, or WSL, keep the private key in OpenSSH format.
Set a passphrase when ssh-keygen asks. A passphrase protects the private key if your laptop is stolen or a backup leaks. ssh-agent keeps it usable without making you type the passphrase for every login:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
On Windows PowerShell:
Start-Service ssh-agent
ssh-add $env:USERPROFILE\.ssh\id_ed25519
For a new Riven Cloud server, add the key before installing the OS.
Option 1: generate the key locally, open the Riven Cloud control panel, go to SSH keys, click Add Key, paste the contents of your .pub file, give it a clear name, and select it during OS install or reinstall.
Option 2: generate it in the control panel. Go to SSH key > Add Key > Generate Key Pair, download the private key, and store it somewhere safe. Choose OpenSSH format for Terminal or PowerShell, or PuTTY format for PuTTY on Windows. Select that key when installing the server.
For an already-running server where password login still works, push the public key with ssh-copy-id:
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@SERVER_IP
Now test the key from a new terminal:
ssh -i ~/.ssh/id_ed25519 root@SERVER_IP
That login must succeed before you continue. If it fails, stop here and fix the key path, username, public key, file permissions, or control panel selection. Leave password login alone until this works.
Step 2: disable password login and lock down root
Password login is what turns random SSH scans into a real guessing attack. Once key login is confirmed, disable password authentication and keyboard-interactive authentication. Keep root key login available for now with PermitRootLogin prohibit-password, which blocks password login for root while still allowing a valid key. The options are documented in sshd_config(5).
Create a drop-in file:
sudo install -d -m 755 /etc/ssh/sshd_config.d
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf > /dev/null <<'EOF'
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
EOF
Most current Debian, Ubuntu, AlmaLinux, Rocky Linux, and CentOS Stream images include /etc/ssh/sshd_config.d/*.conf from the main sshd_config. If your image is unusual, confirm the main file has an Include /etc/ssh/sshd_config.d/*.conf line.
Validate first:
sudo sshd -t
Restart SSH:
# Debian and Ubuntu
sudo systemctl restart ssh
# AlmaLinux, Rocky Linux, and CentOS Stream
sudo systemctl restart sshd
Open a new terminal and test key login again:
ssh -i ~/.ssh/id_ed25519 root@SERVER_IP
Then confirm password-only login is refused:
ssh -o PubkeyAuthentication=no -o IdentitiesOnly=yes -o PreferredAuthentications=keyboard-interactive,password root@SERVER_IP
The server should reject the attempt immediately without ever showing a password prompt. That confirms password authentication is off. If key login still works and the password-only attempt is refused like this, the biggest SSH hardening win is already done.
The stronger long-term setup is a non-root sudo user plus PermitRootLogin no, but only after that user has been tested. It is optional here because people lock themselves out when they rush it. If you do it, create the sudo user, add its key, test sudo, then change root login.
Step 3: change the SSH port for noise reduction
Changing the SSH port is not real security. It does not make SSH stronger, and it does not protect a weak password. It mainly cuts down hits from scanners that only try port 22. Do it after key-only login works, and treat it as log noise reduction.
This example uses port 2222. If you choose another port, use the same port consistently in SSH, the firewall, and fail2ban.
First, open the new port in the firewall.
On AlmaLinux, Rocky Linux, and CentOS Stream, firewalld is commonly enabled on VPS templates and arbitrary new ports are closed by default. Add 2222/tcp before restarting SSH:
sudo firewall-cmd --permanent --add-port=2222/tcp
sudo firewall-cmd --reload
sudo firewall-cmd --list-ports
The firewall-cmd syntax is documented in the firewalld command manual.
On Ubuntu or Debian, UFW may or may not be enabled. Check first:
sudo ufw status
If UFW is active, allow the new port:
sudo ufw allow 2222/tcp
sudo ufw status
The Ubuntu ufw manual documents allow, delete, and status commands.
Now change the SSH listener. On most servers, use a temporary dual-port drop-in so port 22 stays available while you test port 2222:
sudo tee /etc/ssh/sshd_config.d/20-port.conf > /dev/null <<'EOF'
Port 22
Port 2222
EOF
Validate and restart:
sudo sshd -t
# Debian and Ubuntu
sudo systemctl restart ssh
# AlmaLinux, Rocky Linux, and CentOS Stream
sudo systemctl restart sshd
Open a new terminal and test the new port:
ssh -p 2222 -i ~/.ssh/id_ed25519 root@SERVER_IP
Only after that works should you remove port 22 from the SSH listener:
sudo tee /etc/ssh/sshd_config.d/20-port.conf > /dev/null <<'EOF'
Port 2222
EOF
sudo sshd -t
Restart SSH again with the correct service name for your distro, then open one more new session on port 2222.
Ubuntu and Debian socket activation gotcha
Some Debian or Ubuntu systems use ssh.socket. When socket activation is active, systemd owns the listening socket and the Port line in sshd_config can be ignored for the initial listener. Check it:
systemctl status ssh.socket
If it is active and listening, override the socket instead of relying on 20-port.conf:
sudo systemctl edit ssh.socket
Use both ports for the first test:
[Socket]
ListenStream=
ListenStream=22
ListenStream=2222
Then validate SSH config, reload systemd, and restart the socket:
sudo sshd -t
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
After ssh -p 2222 root@SERVER_IP works from a new terminal, edit the socket again and leave only this listener:
[Socket]
ListenStream=
ListenStream=2222
The ListenStream behavior comes from the systemd.socket documentation. Keep the same order: add the new listener, test it, then remove the old listener.
SELinux note
Riven Cloud default Linux templates ship with SELinux disabled, so no SELinux port change is needed on those templates.
If you enabled SELinux yourself on a RHEL-family server, allow the new SSH port before restart:
sudo dnf install policycoreutils-python-utils
sudo semanage port -a -t ssh_port_t -p tcp 2222
The semanage port command is documented in the semanage-port manual. Do not run this as a required step unless SELinux is actually enabled.
Once you can log in on port 2222, remove port 22 from the firewall.
For firewalld, the default SSH opening is usually the ssh service, not a raw port rule:
sudo firewall-cmd --permanent --remove-service=ssh
sudo firewall-cmd --permanent --remove-port=22/tcp
sudo firewall-cmd --reload
For UFW:
sudo ufw delete allow 22/tcp
sudo ufw delete allow OpenSSH
sudo ufw status
Run the delete command that matches what your firewall actually shows. Never remove port 22 before a new session on port 2222 works.
Step 4: stop brute force with fail2ban
Riven Cloud’s default Linux templates ship with fail2ban preinstalled. Verify it anyway:
sudo fail2ban-client --version
sudo fail2ban-client status sshd
sudo systemctl status fail2ban
If it is missing on a custom image, install it:
# Debian and Ubuntu
sudo apt update
sudo apt install fail2ban
# AlmaLinux, Rocky Linux, and CentOS Stream
sudo dnf install epel-release
sudo dnf install fail2ban
Configure local overrides under /etc/fail2ban/jail.d/. Do not edit jail.conf directly; fail2ban’s upstream configuration is meant to be overridden locally, as shown by its default jail configuration.
sudo tee /etc/fail2ban/jail.d/ssh.conf > /dev/null <<'EOF'
[sshd]
enabled = true
port = 2222
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
EOF
The port value must match the port from Step 3. If fail2ban watches port 22 while SSH listens on 2222, the jail no longer matches the SSH service you are trying to protect.
Modern Debian, Ubuntu, AlmaLinux, Rocky Linux, and CentOS Stream systems normally log SSH authentication through the systemd journal, so backend = systemd is a good default. If your image writes only to traditional log files, fail2ban can use a log file backend instead.
Enable and start fail2ban:
sudo systemctl enable --now fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
The sshd status output shows the jail, current failures, and banned IP addresses. With key-only auth already in place, brute force attempts cannot log in by guessing a password. fail2ban still earns its keep by blocking noisy scanners and keeping abusive clients from hammering your logs all day.
Step 5: tighten the firewall and extra SSH knobs
After SSH is stable on the new port, keep the firewall boring. Deny inbound traffic by default, then open only what the server actually serves.
For a web server, that often means SSH plus HTTP and HTTPS:
# firewalld
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-all
# UFW
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2222/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status
RHEL-family VPS templates are a useful model here: with firewalld enabled, SSH is allowed and random ports are closed. Keep that habit. Add a port when a service needs it, and remove the port when the service is gone.
Do not mix security cleanup with unrelated performance work. If a site feels slow after SSH hardening, diagnose that separately with a page-load and server check such as why is my website slow.
Optional SSH settings can make abuse less noisy:
MaxAuthTries 3
LoginGraceTime 30
AllowUsers deploy admin
# or:
# AllowGroups ssh-users
Use AllowUsers or AllowGroups only after the named user or group has been tested in a new session. The golden rule still applies.
What actually protects you when you secure SSH on a VPS
Do not treat every control as equal.
- Key-only authentication with password login disabled is the biggest win. It removes password guessing from the attack path.
- A minimal firewall and a non-root sudo user reduce exposure and limit blast radius.
- fail2ban cuts log noise and blocks abusive scanners.
- Changing the SSH port is pure noise reduction.
Treat the port change as cleanup, not as the main defense. A server on port 2222 with password login enabled is still a weak SSH server. A server on port 22 with passwords disabled and strong keys is already much harder to brute force.
Conclusion
The safe way to secure SSH on a VPS is still to build the new entrance before closing the old one: add and test the key, disable passwords, open and test the new port, tune fail2ban, then remove anything you no longer need.
Riven Cloud’s Linux templates ship with fail2ban preinstalled, and the control panel manages SSH keys directly. That makes the starting point for doing this right fairly low; you can view VPS plans when you need a VPS with root access.
Share