A fresh VPS is a clean slate, but the internet is hostile from minute one. Your IPv4 will see SSH brute-force attempts within hours of provisioning. None of these steps are exotic, most are 10 minutes of work. Do them once per server, save yourself an incident.
Run them in order. Each step is independent but builds on the previous one.
1. Create a non-root sudo user
Logging in as root is fine for the first 10 minutes; after that, switch.
$ adduser deploy # interactive, set a strong password
$ usermod -aG sudo deploy
$ mkdir -p /home/deploy/.ssh
$ cp /root/.ssh/authorized_keys /home/deploy/.ssh/
$ chown -R deploy:deploy /home/deploy/.ssh
$ chmod 700 /home/deploy/.ssh
$ chmod 600 /home/deploy/.ssh/authorized_keys
Test SSH as deploy from another terminal before continuing, don't lock yourself out.
$ ssh deploy@your-server
deploy@server:~$ sudo whoami
root
2. Disable root SSH and password authentication
Edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
Reload:
$ sudo systemctl reload ssh
Try logging in as root, should fail. SSH from a wrong machine, should fail. Now password-based brute force is impossible regardless of password strength.
3. Change the SSH port (optional, low value)
Port 22922 (or any high port). Reduces the log noise from internet scanners; doesn't add real security because anyone serious will port-scan first. Worth it only if you hate seeing thousands of failed-login lines in your auth log.
4. Set up UFW (firewall)
UFW is a sane wrapper around iptables. Default: deny everything in, allow everything out.
$ sudo apt install -y ufw
$ sudo ufw default deny incoming
$ sudo ufw default allow outgoing
$ sudo ufw allow 22922/tcp # or whatever your SSH port is
$ sudo ufw allow 80/tcp
$ sudo ufw allow 443/tcp
$ sudo ufw enable
Verify:
$ sudo ufw status
Status: active
To Action From
-- ------ ----
22922/tcp ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
For each service you run, open the port explicitly. Don't ufw allow from any to any, that's the same as no firewall.
5. Enable unattended-upgrades
$ sudo apt install -y unattended-upgrades
$ sudo dpkg-reconfigure --priority=low unattended-upgrades
This patches security updates daily. Default config is reasonable; if you want to also reboot automatically when a kernel update needs it, edit /etc/apt/apt.conf.d/50unattended-upgrades:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
For critical workloads, prefer manual reboots. For most apps, automatic at 4 AM is fine.
6. Install fail2ban
$ sudo apt install -y fail2ban
$ sudo systemctl enable --now fail2ban
Default config bans IPs after 5 failed SSH attempts for 10 minutes. Adjust in /etc/fail2ban/jail.local:
[sshd]
enabled = true
maxretry = 3
bantime = 1h
findtime = 10m
Check banned IPs:
$ sudo fail2ban-client status sshd
7. Set up swap (if your VPS doesn't have any)
OOM kills are a common cause of mystery downtime. Add 2 GB swap on a 1-4 GB RAM VPS:
$ sudo fallocate -l 2G /swapfile
$ sudo chmod 600 /swapfile
$ sudo mkswap /swapfile
$ sudo swapon /swapfile
$ echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
Tune swappiness (prefer RAM, use swap only when really needed):
$ sudo sysctl vm.swappiness=10
$ echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
8. Configure a separate audit log destination (recommended)
/var/log/auth.log records every SSH attempt, sudo use, and login. If an attacker gets in, the first thing they do is wipe logs. Ship them to somewhere they can't reach.
Simple version, rotate aggressively and back up offsite:
$ sudo nano /etc/logrotate.d/auth-extra
/var/log/auth.log {
daily
rotate 30
compress
delaycompress
notifempty
postrotate
rsync /var/log/auth.log.1.gz backup-host:/path/auth-logs/$(hostname)-$(date +%Y%m%d).gz
endscript
}
Production version: ship to Loki / CloudWatch / Datadog. Pick whatever you already have.
9. Set up basic monitoring + alerting
Even a simple "is the server up" check beats no monitoring:
- Healthchecks.io for dead-man-switch crons (free tier is generous).
- UptimeRobot for HTTP endpoint checks.
- Self-hosted Prometheus + Grafana if you want time-series metrics.
Wire whatever alerts you set up to a channel you actually read. SMS, Telegram, PagerDuty. An email alert to an inbox you check once a day isn't monitoring.
10. Take a baseline snapshot
# Via dashboard or:
$ rarecloud server backup create $SRV
This is your clean-state restore point. If you ever suspect a compromise, you can spin up a fresh server from this snapshot and migrate data over, instead of debugging a possibly-rooted machine.
What this does and doesn't protect against
This checklist protects against:
- Brute-force SSH attempts (steps 2, 6)
- Unknown vulnerabilities in unpatched packages (step 5)
- Casual port-scanning attempts (step 4)
- Out-of-memory crashes (step 7)
- Total data loss from a single bad change (step 10)
It does not protect against:
- Vulnerabilities in your application code itself
- Stolen API tokens or secrets in your repo
- A compromised SSH key on your laptop (see SSH key management)
- Supply-chain attacks via npm / pip dependencies
For application-layer security, that's a different doc. This one gets you to a baseline-hardened OS.