Skip to content
Get Started

How to Secure SSH on a VPS Without Locking Yourself Out

SSH is usually the first service that internet scanners touch. Put a fresh VPS online with password login enabled on port 22, and the authentication log will often start collecting junk within hours.

The fix is mostly order. Add and test an SSH key first. Then disable password login, move the listener only after the new port is open, configure fail2ban, and close whatever you no longer need. Key-only SSH does the serious work. A custom port makes the logs quieter. fail2ban blocks noisy clients, but it does not make weak authentication safe.

Use one rule for the whole job: build the new entrance before closing the old one. Do not disable a login method, restart SSH, or remove a firewall rule until the replacement path works from a new terminal.

This is written for people running an unmanaged Linux VPS. If you are still deciding whether root access is worth the operational work, read VPS vs shared hosting first.

Before you start: don’t lock yourself out

Keep one working SSH session open the whole time. Do not close it just because the last command succeeded. SSH restarts usually leave existing sessions alone, and that terminal is your way back in if the next login test fails.

Run the OpenSSH server config test before every restart:

sudo sshd -t

The -t flag makes sshd check the configuration and keys, then exit without starting a new daemon, as documented in the OpenSSH sshd manual. If it prints an error, fix that first.

After each change, open a second terminal and log in again. Move on only after the new session works.

The service name depends on the distro:

# 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 depends on key login already working. OpenSSH supports several key types through ssh-keygen; for a normal modern server, Ed25519 is a sensible default. Use RSA 4096 only when an older server or 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 log in with PuTTY, keep the private key in PuTTY’s format. If you log in from Terminal, PowerShell, or WSL, keep the private key in OpenSSH format.

Set a passphrase when ssh-keygen asks. It protects the private key if your laptop is stolen or a backup leaks. ssh-agent keeps the key 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 you install the OS.

Option 1: generate the key locally. In 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 the key 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 you install 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

Test the key from a new terminal:

ssh -i ~/.ssh/id_ed25519 root@SERVER_IP

That login must work before you continue. If it fails, stop here and check the key path, username, public key, file permissions, or control panel selection. Leave password login alone until key login works.

Step 2: disable password login and lock down root

Password login is what turns random SSH scans into a guessing attack. Once key login works, disable password authentication and keyboard-interactive authentication. For this pass, keep root key login available with PermitRootLogin prohibit-password: root cannot use a password, but a valid key still works. 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, check the main file for this line:

Include /etc/ssh/sshd_config.d/*.conf

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 from a new terminal:

ssh -o PubkeyAuthentication=no -o IdentitiesOnly=yes -o PreferredAuthentications=keyboard-interactive,password root@SERVER_IP

The server should reject the attempt without showing a password prompt. If key login still works and that password-only attempt fails, the biggest SSH hardening win is already done.

A non-root sudo user plus PermitRootLogin no is a stronger long-term setup, but do it only after that user has been tested. This guide keeps it optional because this is where people lock themselves out. If you make that change, create the sudo user, add its key, test sudo, then disable root login.

Step 3: change the SSH port for noise reduction

Moving SSH away from port 22 is housekeeping. It does not make SSH stronger, and it will not save a server that still accepts weak passwords. It mainly cuts down hits from scanners that only try the default port. Do it after key-only login works, and treat it as log cleanup.

This example uses port 2222. If you choose another port, use the same value in SSH, the firewall, and fail2ban.

Open the new port in the firewall before you restart SSH.

On AlmaLinux, Rocky Linux, and CentOS Stream, firewalld is commonly enabled on VPS templates and new inbound ports are closed by default. Add 2222/tcp first:

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 before you add rules:

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

Remove port 22 from the SSH listener only after port 2222 works:

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: check ssh.socket

Some Debian or Ubuntu systems use ssh.socket. When socket activation is active, systemd owns the listening socket, so the Port line in sshd_config may not control the initial listener. Check it:

systemctl status ssh.socket

If it is active and listening, override the socket instead of relying only 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 order boring: add the new listener, test it, then remove the old listener.

SELinux note

Riven Cloud default Linux templates ship with SELinux disabled, so those templates do not need an SELinux port change.

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 it 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

fail2ban is useful after SSH authentication is already sane. 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

Put 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 is watching the wrong place.

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, 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 in place, brute force attempts cannot log in by guessing a password. fail2ban still helps by blocking noisy scanners and keeping abusive clients from hitting your logs all day.

Step 5: tighten the firewall and extra SSH knobs

After SSH is stable on the new port, keep the firewall plain. 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 SSH cleanup with unrelated performance work. If a site feels slow after hardening, diagnose that separately with a page-load and server check such as why is my website slow.

A few 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 same rule still applies: test the new path before closing the old one.

What actually protects you when you secure SSH on a VPS

Do not treat every control as equal. They are not doing the same job.

  1. Key-only authentication with password login disabled removes password guessing from the attack path.
  2. A minimal firewall and a non-root sudo user reduce exposure and limit blast radius.
  3. fail2ban cuts log noise and blocks abusive scanners.
  4. Changing the SSH port reduces noise, not risk by itself.

Treat the port change as cleanup, not 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 order is simple: add and test the key, disable passwords, open and test the new port, configure fail2ban, then remove anything you no longer need.

Riven Cloud’s Linux templates ship with fail2ban preinstalled, and the control panel can add SSH keys during deployment. If you need an unmanaged KVM VPS with root access, daily backups, and Tokyo or Singapore locations, you can view VPS plans.

Share