Most VPS providers hand you a root password and an open port 22. That combination survives about as long as it takes an automated scanner to find your IP, often just minutes on the public internet. SSH is the front door to your server, and the default configuration leaves it wide open.

This article covers every practical SSH hardening step: key-based auth, disabling passwords, config lockdown, custom ports, TOTP two-factor, and jump host setups. If you have already run through the VPS security checklist or the initial VPS setup guide, you have the foundation. This is where you finish the job.

Set Up Key-Based Authentication

Key-based login replaces the guessable password with a cryptographic key pair. The private key stays on your workstation; the public key goes on the server. Without the private key file, there is nothing to brute-force.

Generate a key pair on your local machine:

ssh-keygen -t ed25519 -C "yourname@yourmachine"

Ed25519 is the recommended algorithm. It produces shorter keys than RSA, uses a modern elliptic curve, and performs well on every OS released in the last decade. If you need compatibility with very old systems (OpenSSH before 6.5), fall back to RSA 4096:

ssh-keygen -t rsa -b 4096 -C "yourname@yourmachine"

Copy the public key to the server:

ssh-copy-id -i ~/.ssh/id_ed25519.pub youruser@YOUR_SERVER_IP

If ssh-copy-id is not available (Windows without WSL, for example), do it manually:

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"

Test the key login in a new terminal session before making any other changes:

ssh youruser@YOUR_SERVER_IP

If that drops you into a shell without asking for a password, key auth is working. Do not close this session yet.

Gotcha: if you skip the chmod 600 on authorized_keys or chmod 700 on .ssh, OpenSSH silently refuses to use the key and falls back to password auth. You will not see an error. The login just quietly asks for a password, and you will assume the key did not work. The permissions check is strict by design: the server will not trust a key file that other users on the system can read.

Disable Password Authentication

Once key login works, turn off passwords entirely. Open the SSH daemon config:

sudo nano /etc/ssh/sshd_config

Find and set these directives (or add them if they appear nowhere in the file):

PasswordAuthentication no
KbdInteractiveAuthentication no

On older distributions, the second directive may be called ChallengeResponseAuthentication. Set whichever exists to no.

Restart the SSH daemon:

sudo systemctl restart sshd

Open a second terminal and test logging in with a password to confirm it is rejected:

ssh -o PubkeyAuthentication=no youruser@YOUR_SERVER_IP

That command should fail immediately with "Permission denied (publickey)." If it still accepts a password, check that the directives are not overridden later in the file or inside a Match block.

Gotcha: some distributions ship a separate /etc/ssh/sshd_config.d/ directory with drop-in files that can silently re-enable password auth. Run grep -r "PasswordAuthentication" /etc/ssh/sshd_config.d/ to make sure nothing overrides your setting. On Ubuntu 22.04 and later, look specifically for 50-cloud-init.conf, which often contains PasswordAuthentication yes.

Restrict Login to Named Users

By default, every system account can attempt SSH login. Limit it to the accounts that actually need remote access:

AllowUsers youruser

Add that directive to /etc/ssh/sshd_config. Only the listed usernames will be permitted to log in. Everyone else gets rejected before authentication even starts. If you have multiple admins, list them space-separated:

AllowUsers youruser anotheradmin

Restart sshd after any config change:

sudo systemctl restart sshd

Verify by attempting to log in as a non-listed user from another terminal. The connection should be rejected immediately with "Permission denied."

You should also disable root login over SSH. Root access via password is the single most common exploit path on a new VPS:

PermitRootLogin no

If you need root commands, sudo from your regular user account after logging in.

Change the Default SSH Port

Moving SSH off port 22 does not stop a targeted attacker, but it eliminates the bulk of automated scanning traffic. Scanners hit port 22 on every IP range; a non-standard port avoids that noise entirely and keeps your auth logs clean.

In /etc/ssh/sshd_config:

Port 2222

Pick a port between 1024 and 65535 that is not used by another service. Before restarting sshd, open the new port in your firewall:

sudo ufw allow 2222/tcp
sudo ufw deny 22/tcp

See the UFW firewall guide for the full setup if you have not configured one yet.

Restart sshd:

sudo systemctl restart sshd

Test from a new terminal:

ssh -p 2222 youruser@YOUR_SERVER_IP

Gotcha: if you restart sshd before opening the new port in the firewall, you lock yourself out. Always add the firewall rule first, test in a new terminal, and only then remove the rule for port 22. Keep your existing session open as a lifeline until the new port is confirmed working. If you do lock yourself out, most VPS providers offer a web console or KVM/VNC rescue session through their control panel.

Harden the sshd_config

Beyond the basics above, several directives tighten the SSH daemon against common attack vectors. Add these to /etc/ssh/sshd_config:

MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowAgentForwarding no

What each one does:

Directive What it controls
MaxAuthTries 3 Disconnects after 3 failed auth attempts per connection. Slows brute-force scripts.
LoginGraceTime 30 Gives 30 seconds to complete login. Drops connections that sit idle at the prompt.
ClientAliveInterval 300 Sends a keepalive probe every 5 minutes.
ClientAliveCountMax 2 Disconnects after 2 missed keepalive responses (10 minutes of dead connection).
X11Forwarding no Disables X11 forwarding. Unless you run GUI apps through SSH, turn this off.
AllowAgentForwarding no Prevents forwarding your local SSH agent to the server. Limits lateral movement if the server is compromised.

For servers that do not need SFTP subsystem access, disable it by commenting out or removing the Subsystem sftp line. Most VPS administration does not require SFTP when scp or rsync work over the SSH connection directly.

You can also restrict which ciphers and key exchange algorithms the server accepts. Adding explicit Ciphers and KexAlgorithms directives limits the daemon to modern, audited algorithms and rejects legacy options like diffie-hellman-group1-sha1 or 3des-cbc. The safe defaults shipped with current OpenSSH versions are reasonable, but if your compliance environment or threat model demands a narrow list, the ssh-audit tool (available in most package managers) will scan your server and flag weak algorithms.

After all changes, validate the config before restarting:

sudo sshd -t

That command checks syntax without restarting. If it prints nothing, the config is valid. If errors appear, fix them before running systemctl restart sshd.

For brute-force protection beyond MaxAuthTries, pair these settings with fail2ban. The fail2ban setup guide walks through the SSH jail configuration in detail.

Add Two-Factor Authentication (TOTP)

Key-based auth is already strong, but two-factor adds a second layer: something you have (the key) plus something you know (a time-based one-time password from an authenticator app). If an attacker somehow obtains your private key, they still cannot log in without the TOTP code.

Install the Google Authenticator PAM module:

sudo apt install libpam-google-authenticator

On RHEL/Rocky/Alma:

sudo dnf install google-authenticator

Run the setup as your regular user (not root):

google-authenticator

Answer the prompts:

  1. Time-based tokens? Yes.
  2. Update .google_authenticator file? Yes.
  3. Disallow multiple uses of the same token? Yes.
  4. Increase the time window? No (keep the default 30-second window).
  5. Rate limiting? Yes.

Scan the QR code with your authenticator app (Google Authenticator, Authy, or any TOTP-compatible app). Save the emergency scratch codes somewhere offline.

Configure PAM to require the TOTP code. Edit /etc/pam.d/sshd:

sudo nano /etc/pam.d/sshd

Add this line at the end:

auth required pam_google_authenticator.so

Then edit /etc/ssh/sshd_config to enable challenge-response and specify the required authentication methods:

KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

The AuthenticationMethods line means the user must present a valid key and then pass the keyboard-interactive challenge (the TOTP prompt). Restart sshd:

sudo systemctl restart sshd

Test from a new terminal. You should see a "Verification code:" prompt after your key is accepted. Enter the 6-digit code from your authenticator app.

Gotcha: the earlier section set KbdInteractiveAuthentication no to disable passwords. This section re-enables it specifically for TOTP. The critical difference is that PasswordAuthentication remains no, which means raw passwords are still rejected. The keyboard-interactive channel is now used exclusively for the TOTP challenge because PAM handles the prompt. If you get "Permission denied" after enabling this, check that AuthenticationMethods lists publickey,keyboard-interactive (comma-separated, no spaces), not publickey keyboard-interactive (space-separated means "either method," not "both methods").

Use a Jump Host for Layered Access

A jump host (also called a bastion) is an intermediary server that sits between the public internet and your production VPS. Instead of exposing SSH on the production server directly, you SSH into the jump host first, and from there connect to the target.

This is practical when you manage multiple servers. The jump host is the only machine with a public SSH port; all other servers accept SSH connections only from the jump host's private IP.

Set up the jump host's firewall to allow SSH only from your IP:

sudo ufw allow from YOUR_WORKSTATION_IP to any port 2222 proto tcp

On the production server, restrict SSH to the jump host's private IP only:

sudo ufw allow from JUMP_HOST_PRIVATE_IP to any port 22 proto tcp
sudo ufw deny 22/tcp

Configure your local SSH client to proxy through the jump host automatically. Add this to ~/.ssh/config on your workstation:

Host jump
    HostName JUMP_HOST_PUBLIC_IP
    Port 2222
    User youruser
    IdentityFile ~/.ssh/id_ed25519

Host production
    HostName PRODUCTION_PRIVATE_IP
    User youruser
    IdentityFile ~/.ssh/id_ed25519
    ProxyJump jump

Now ssh production transparently hops through the jump host. You never need to manually SSH into the bastion and then SSH again.

For providers that offer virtual private networks or VLANs between servers in the same data center, the production server does not need any public SSH port at all. The jump host is the only entry point.

Gotcha: if the jump host goes down, you lose access to every server behind it. Always make sure your VPS provider's web console (KVM, VNC, or browser-based terminal) works as a recovery path. Test it before you need it, not during an outage.

Keep OpenSSH Updated

None of these hardening steps matter if the SSH daemon itself has a known vulnerability. Package your OpenSSH updates with your regular system patching. On Debian and Ubuntu:

sudo apt update && sudo apt upgrade openssh-server

The patching and update guide covers unattended-upgrades configuration so security patches apply automatically.

Pulling It All Together

After working through every section, here is what SSH access to your server looks like:

  1. Key-based login only. Passwords are rejected.
  2. Only named users can connect, and root login is disabled.
  3. SSH listens on a non-standard port behind a firewall.
  4. Failed login attempts are capped and idle connections are dropped.
  5. TOTP provides a second factor beyond the key itself.
  6. Optionally, a jump host removes direct public exposure entirely.

Not every server needs every layer. A personal project VPS behind key auth, fail2ban, and a firewall is well protected. A production server handling customer data benefits from the full stack, especially TOTP and a jump host.

SSH is the starting point, not the finish line. The VPS security checklist covers the rest of the picture: firewalls, automatic updates, backups, and monitoring. If you are still evaluating providers, browse the provider directory and check real user reviews to see how different hosts handle security features, console access, and support responsiveness when something goes wrong.