Hardening SSH on a New VPS: Keys, Ports, fail2ban, and CrowdSec
A freshly provisioned VPS is reachable from the entire internet within seconds of boot, and automated scanners will find its SSH port almost immediately. The default configuration on most distributions still permits password authentication, which means the only thing between an attacker and a shell is the strength of a password and the patience of a botnet. This guide walks through a layered approach to locking down SSH: switching to key-only authentication, disabling root login, moving the daemon off port 22, and adding an intrusion-prevention layer with either fail2ban or CrowdSec. Each layer is independent, so a weakness in one does not undo the others.
Set up key-based authentication first
Key authentication is the single highest-impact change you can make. It removes password guessing as an attack vector entirely, because there is no password to guess. Generate a key pair on your local machine, not on the server. Ed25519 is the modern default and produces short, fast keys:
ssh-keygen -t ed25519 -a 100 -C "you@laptop"
The -a 100 flag sets the number of KDF rounds used to protect the private key on disk. Copy the public half to the server. If password login still works, ssh-copy-id is the least error-prone method:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@your-server
If you provisioned the VPS with a key already injected, append the public key to ~/.ssh/authorized_keys manually and fix the permissions, which OpenSSH enforces strictly:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
Before you disable passwords, open a second terminal and confirm you can log in with the key. Keep your current session open the entire time. A broken config plus a closed session is how people lock themselves out of a remote machine.
Harden sshd_config
With key login confirmed, edit the daemon configuration. On current distributions, do not edit /etc/ssh/sshd_config in place if a drop-in directory exists; instead create a file under /etc/ssh/sshd_config.d/ so your settings survive package upgrades:
sudo tee /etc/ssh/sshd_config.d/99-hardening.conf >/dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AllowUsers deploy
EOF
A few notes on why each line matters:
- PermitRootLogin no forces attackers to know a valid username as well as hold its key, and keeps
sudoas an auditable boundary. - PasswordAuthentication no disables password guessing. On PAM-based systems this is not enough on its own, because keyboard-interactive auth can still prompt for a password, so set KbdInteractiveAuthentication no as well.
- AllowUsers restricts login to an explicit allowlist. Replace
deploywith your real account.
Validate the syntax before restarting so a typo does not take the daemon down:
sudo sshd -t && sudo systemctl restart ssh
On some distributions the unit is named sshd rather than ssh. See the sshd_config manual for the authoritative directive list.
Changing the port: obscurity, not security
Moving SSH off port 22 is worth doing, but be clear about what it buys you. It does not make the daemon harder to break into. It only hides it from the broad, indiscriminate scans that hammer port 22, which cuts log noise dramatically and reduces the volume of automated attempts. A targeted attacker will find the new port with a single nmap pass. Treat it as a way to quiet the logs, not as a security control.
echo "Port 2222" | sudo tee /etc/ssh/sshd_config.d/10-port.conf
If your distribution uses socket-based activation for SSH, the Port directive may be ignored and you must change the socket instead:
sudo systemctl edit ssh.socket
# add under [Socket]:
# ListenStream=
# ListenStream=2222
Open the new port and close the old one in your firewall before restarting, or you will lock yourself out:
sudo ufw allow 2222/tcp
sudo systemctl restart ssh
Add fail2ban for local rate-limiting
Key-only auth stops credential guessing, but a determined botnet can still flood your daemon with connection attempts and fill your logs. fail2ban watches those logs and bans offending IPs at the firewall after a threshold of failures. Install it and create a local override; never edit the stock jail.conf, since upgrades overwrite it:
sudo apt install fail2ban
sudo tee /etc/fail2ban/jail.d/sshd.local >/dev/null <<'EOF'
[sshd]
enabled = true
port = 2222
backend = systemd
maxretry = 3
findtime = 10m
bantime = 1h
EOF
sudo systemctl restart fail2ban
The backend = systemd line matters on current releases such as Debian 12 and Ubuntu 24.04, which log through the systemd journal rather than a traditional /var/log/auth.log file. Check the jail status and see who is currently banned:
sudo fail2ban-client status sshd
fail2ban is reactive and entirely local: it only knows about attacks your own host has already seen. That simplicity is also its strength, with minimal dependencies and well-understood behavior. The fail2ban project documents additional filters and actions.
CrowdSec as a modern complement
CrowdSec covers the same job from a different angle. It splits detection from enforcement: an agent parses logs and decides what is malicious, while a separate bouncer applies the block at the firewall. Crucially, agents can optionally share anonymized attack signals (source IP, the scenario triggered, a timestamp) and in return receive a community blocklist. When one participant is attacked, others can block that IP before it ever reaches them.
Install the agent using the official repository script, then add the SSH collection and a firewall bouncer:
curl -s https://install.crowdsec.net | sudo sh
sudo apt install crowdsec
sudo cscli collections install crowdsecurity/sshd
sudo apt install crowdsec-firewall-bouncer-nftables
Use the -iptables package instead of -nftables if your host still uses iptables as its primary backend. Confirm the collection is loaded and inspect any active decisions:
sudo cscli collections list
sudo cscli decisions list
Which one, and when
- fail2ban when you want minimal dependencies and a single, self-contained process on one server. Its regex jails react only to what your host observes.
- CrowdSec when you run multiple servers or want proactive, crowd-sourced blocking. Its scenarios reason over behavior across time (for example, several failed logins within a window) rather than matching individual log lines, which tends to reduce false positives.
The tradeoff is data sharing: the community blocklist is powered by telemetry contributed back to CrowdSec's network. The data is anonymized, and you can run CrowdSec without sharing at all, though you then forgo the community feed. Some teams cannot send any security telemetry to a third party, and for them local fail2ban remains the right call. Running both is also viable, since they operate on different layers. The CrowdSec documentation covers collections and bouncers in depth.
Wrap-up
The layers reinforce each other. Key-only authentication is the real security win because it eliminates password guessing outright. Disabling root login narrows the attack surface to named accounts. Moving off port 22 quiets your logs without pretending to be a control. fail2ban or CrowdSec then absorbs the residual noise and, in CrowdSec's case, borrows the internet's collective view of who is currently hostile. None of this is specific to any one provider; it applies to any fresh VPS the moment it comes online. Apply the changes in order, keep a second session open, and verify each step before you trust it.