After setting up my VPS, security was the next priority. A server on the public internet gets probed constantly. Within hours of going live, the auth log fills up with failed SSH login attempts from all over the world.
This post covers the measures I took. None of this is novel, but having it written down in one place is useful.
SSH hardening
SSH is the primary way you access a Linux server, which makes it the primary target for attackers. The default configuration is functional but permissive.
Disable root login
Root login over SSH should be off. You should connect as a regular user and use sudo when needed.
Edit /etc/ssh/sshd_config:
PermitRootLogin no
Disable password authentication
Key-based authentication is both more convenient and more secure than passwords. Once your public key is set up, disable password login entirely:
PasswordAuthentication no
PubkeyAuthentication yes
Make sure your key is working before you apply this. Otherwise you will lock yourself out.
Change the default port (optional)
Moving SSH to a non-standard port reduces noise in the logs. It is not real security, but it cuts automated scan attempts significantly:
Port 2222
Pick any unused port above 1024. Remember to update your firewall rules to allow the new port before restarting SSH.
Other useful settings
A few more options worth setting:
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 2
MaxAuthTries 3limits authentication attempts per connection.LoginGraceTime 30closes the connection if authentication is not completed in 30 seconds.ClientAliveIntervalandClientAliveCountMaxdisconnect idle sessions after about 10 minutes.
After making changes:
sshd -t # test the configuration
systemctl restart sshd
Always test the configuration before restarting. A syntax error in sshd_config can lock you out.
Fail2ban
Fail2ban monitors log files and bans IP addresses that show malicious behavior. It is particularly useful for SSH, where brute-force attempts are constant.
Installation
apt install fail2ban
Configuration
Fail2ban uses jail configurations. Create a local override file so your changes survive package updates:
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local. The key settings for SSH:
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
This bans an IP for one hour after 3 failed login attempts within 10 minutes. You can increase bantime for more aggressive banning.
If you changed the SSH port, update the port value:
port = 2222
Start and enable the service:
systemctl enable fail2ban
systemctl start fail2ban
Checking status
fail2ban-client status sshd
This shows the number of currently banned IPs and total bans since the service started. On a public server, you will see bans accumulating within minutes.
To unban an IP manually:
fail2ban-client set sshd unbanip 203.0.113.50
UFW firewall
UFW (Uncomplicated Firewall) is a frontend for iptables that makes basic firewall management straightforward.
Installation and setup
UFW is often pre-installed on Debian/Ubuntu. If not:
apt install ufw
Start by setting the default policies:
ufw default deny incoming
ufw default allow outgoing
This blocks all incoming connections except those you explicitly allow. Outgoing traffic is unrestricted.
Allowing services
Allow the services you need:
ufw allow 22/tcp # SSH (or your custom port)
ufw allow 80/tcp # HTTP
ufw allow 443/tcp # HTTPS
If you moved SSH to a different port:
ufw allow 2222/tcp
Enabling the firewall
ufw enable
UFW will warn you that this may disrupt existing SSH connections. Make sure you have allowed SSH before enabling.
Check the current rules:
ufw status verbose
Rate limiting
UFW has a built-in rate limiting feature for SSH:
ufw limit 22/tcp
This allows a maximum of 6 connections per 30 seconds from a single IP. It is a simple complement to fail2ban.
Unattended upgrades
Security updates should be applied promptly. On a personal server that you might not check every day, automatic security updates make sense.
Installation
apt install unattended-upgrades apt-listchanges
Configuration
Enable automatic updates:
dpkg-reconfigure -plow unattended-upgrades
This creates /etc/apt/apt.conf.d/20auto-upgrades with:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
The default configuration in /etc/apt/apt.conf.d/50unattended-upgrades is already set to install security updates. You can verify:
grep -A 5 "Allowed-Origins" /etc/apt/apt.conf.d/50unattended-upgrades
It should include lines for security updates like:
"${distro_id}:${distro_codename}-security";
Optional: email notifications
If you want to receive email when updates are applied, set the mail address in 50unattended-upgrades:
Unattended-Upgrade::Mail "martin@packetlog.org";
You will need a working mail setup on the server for this.
Optional: automatic reboot
Some kernel updates require a reboot. You can configure unattended-upgrades to reboot automatically at a specific time:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
I leave this off and reboot manually when needed. I prefer to know when my server restarts.
Testing
Run a dry-run to verify the configuration:
unattended-upgrades --dry-run --debug
Additional measures
A few more things I do that are quick and worthwhile.
Remove unnecessary packages
A fresh Debian install is already minimal, but check for anything you do not need:
apt list --installed | wc -l
The fewer packages installed, the smaller the attack surface.
Check listening ports
Periodically verify that only expected services are listening:
ss -tlnp
This shows all TCP ports in listening state along with the process using them. If you see something unexpected, investigate.
Set up log monitoring
Review logs periodically. The important ones:
journalctl -u sshd --since "1 hour ago"
tail -100 /var/log/auth.log
Failed login attempts, unusual process activity, and unexpected network connections are all worth investigating.
Keep backups
This is not a security hardening step per se, but it is the most important thing you can do. If something goes wrong, whether from an attack or a misconfiguration, backups let you recover.
I keep automated daily backups of the server’s data to an external storage location. A simple cron job with rsync or rclone works well for this.
Summary
The measures here are the baseline. They will not stop a determined, targeted attacker, but they handle the vast majority of real-world threats to a personal server: automated scans, brute-force attempts, and unpatched vulnerabilities.
The key points:
- SSH: key-only auth, no root login, rate limiting
- Fail2ban: automatic banning of repeat offenders
- UFW: deny by default, allow only what is needed
- Unattended upgrades: automatic security patches
All of this takes about 30 minutes to set up on a fresh server. The Debian security documentation and the fail2ban wiki are good references for going deeper.