GreyNoise deployed a set of fresh sensors in November 2024 and timed how quickly the internet found them. The fastest scanners made contact within five minutes. By the time you've finished the provisioning screen and run your first apt update, your new server has already been probed.
This post covers six things to do on any fresh Ubuntu or Debian VPS before you run anything on it. No particular provider assumed — these steps are the same on Hetzner, DigitalOcean, Vultr, or wherever you are. Prerequisites: root SSH access and basic terminal familiarity.
The order matters. Do them in this order.
1. Create a non-root user — before anything else
Everything else in this guide depends on having a working non-root user set up first. If you skip this and go straight to disabling root SSH access, you'll lock yourself out. Ask me how I know.
adduser username
usermod -aG sudo username
groups username # verify "sudo" appears in the list
Now set up SSH keys for the new user. On your local machine, if you don't have an Ed25519 key yet:
ssh-keygen -t ed25519 -a 100
The -a 100 sets 100 key derivation rounds. If your private key file ever gets stolen, that flag makes brute-forcing the passphrase orders of magnitude slower. Ed25519 is the current recommendation for new key generation — smaller, faster signing than RSA, and NIST is winding down RSA-dependent algorithms anyway.
Copy the public key to the server:
ssh-copy-id username@your_server_ip
Or add it manually to ~/.ssh/authorized_keys in the new user's home directory. Make sure permissions are right: 700 on ~/.ssh/, 600 on authorized_keys.
Open a second terminal window, SSH in as the new user, and confirm it works. Run sudo whoami — it should return root. Only once that's confirmed do you continue.
2. Lock down SSH
Four changes to /etc/ssh/sshd_config. Together they make automated SSH attacks essentially pointless.
sudo nano /etc/ssh/sshd_config
Find and set — or add if missing:
PasswordAuthentication no
PermitRootLogin no
MaxAuthTries 3
LoginGraceTime 60
AllowUsers username
PasswordAuthentication no removes the entire password-based authentication path. Brute-force tools have nothing to work with.
PermitRootLogin no is now safe to set because you have a working sudo user. Even if an attacker gets a foothold, they land as an unprivileged user.
MaxAuthTries 3 drops the connection after three failed attempts instead of six. Minor slowdown for automated scanners, zero cost to legitimate users.
LoginGraceTime 60 reduces the window an unauthenticated connection can sit open waiting for credentials. The default is 120 seconds — worth reducing for a specific reason. Qualys disclosed CVE-2024-6387 in July 2024: an unauthenticated remote code execution vulnerability affecting OpenSSH 8.5p1 through 9.7p1 that exploited a signal handler race condition. Winning the race took around 10,000 attempts and roughly 3–4 hours under the default 120-second grace period. The CVE is patched in OpenSSH 9.8p1, so if your system is current you're fine — but shortening the grace window is a sensible hardening step regardless.
AllowUsers means any login attempt for any username not on that list gets rejected before authentication even starts.
Once the file is saved:
sudo systemctl restart sshd
Then, before closing your current session: open another terminal, SSH in as the non-root user, and confirm it still works. This step is not optional. If something is wrong with the sshd_config, your current session is the lifeline. Close it prematurely and you're relying on whatever emergency console your provider offers.
A note on the SSH port: Moving SSH off port 22 isn't a security measure — any competent attacker scans all ports. What it does do is eliminate most automated low-effort scanning, which can cut auth log noise by close to 98% in practice. That's not nothing if you're paying attention to your logs. It's a valid noise-reduction layer within a defense-in-depth setup. It's not a substitute for any of the above.
3. Firewall — deny by default
UFW handles this in six commands. Run them in this exact order, because enabling the firewall before allowing SSH access will lock you out immediately.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw enable
sudo ufw status verbose
If you moved SSH to a non-standard port, replace the third line with sudo ufw allow [port]/tcp.
Running a web server? Add the ports before enabling:
sudo ufw allow http
sudo ufw allow https
One thing to verify before moving on: run grep IPV6 /etc/default/ufw. It should say IPV6=yes. If it says no, UFW applies its rules to IPv4 only, and IPv6 traffic bypasses the firewall entirely. Modern Ubuntu defaults to yes, but this is worth confirming explicitly rather than assuming.
4. Automatic security updates
Security patches should not require you to remember to apply them. Two commands:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
The second command runs an interactive prompt asking if you want automatic updates enabled. Say yes. By default, unattended-upgrades only touches packages from the -security pocket — security patches go in automatically, everything else waits for a deliberate apt upgrade. That's the right behavior.
Kernel updates are not applied automatically by default, because they require a reboot. If this is a low-traffic personal server and you're comfortable with occasional unscheduled reboots, you can configure that in /etc/apt/apt.conf.d/50unattended-upgrades. Ubuntu's documentation covers the auto-reboot options. For anything resembling a production machine, you'd want to schedule that manually during a maintenance window rather than leave it to chance.
5. Intrusion detection — CrowdSec or fail2ban
Both tools do the same core job: detect attack patterns in logs and ban offending IPs. The difference is scope.
fail2ban is purely local. It parses your logs, identifies brute-force attempts, and adds ban rules to iptables. It's been around since 2004, it's well understood, and for a constrained VPS — say 512 MB or 1 GB RAM, running SSH only — it does the job without complaint.
CrowdSec adds something fail2ban can't: collective threat intelligence. When one node on the CrowdSec network detects an attack, that IP gets flagged across the whole network. As of 2025 that network processes around 15 million threat signals per day — meaning your server can block an attacker before they ever reach your logs, because someone else's server already identified them. CrowdSec also uses nftables IP sets instead of individual iptables rules, which handles large ban lists without the performance degradation you'd see with fail2ban at scale. For a web-facing VPS with 2 GB or more of RAM, it's the better option.
They're not mutually exclusive — you can run both simultaneously if you want fast local SSH bans from fail2ban and broader proactive protection from CrowdSec. I wrote a full installation and configuration walkthrough for CrowdSec here, covering the correct setup order, bouncers, blocklists, and the console.
6. Swap file
A 1 GB VPS running a web server will eventually hit a memory spike — a traffic burst, a cron job, a misconfigured PHP process. Without swap, the Linux OOM killer steps in and terminates the process using the most memory, which is often your web server or database. Not a graceful failure. A swap file gives the kernel somewhere to put pages that aren't actively needed, so RAM pressure doesn't immediately become a service outage.
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
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
If fallocate fails — some OpenVZ-based VPS providers don't support it — replace the first line with:
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress
The swappiness value of 10 (default is 60) tells the kernel to stay in RAM as long as possible and only move pages to disk when things are actually tight. On SSD-backed VPS storage, unnecessary swapping adds latency with no benefit. DigitalOcean's swap guide has more on sizing if you're running more or less than 1 GB of RAM.
Three quick additions
None of these take more than a couple of minutes, and you'll be glad you did them now rather than hunting for them later.
Set your hostname. Your provider assigned something generic. Fix it before logs get confusing:
sudo hostnamectl set-hostname your-hostname
Then update /etc/hosts so 127.0.1.1 points to the new hostname.
Install Logwatch. It's a daily cron job that emails you a digest of the previous day's SSH logins, sudo usage, and auth failures. Not real-time monitoring — just a morning summary that makes anomalies obvious:
sudo apt install logwatch
Configure the email output in /etc/logwatch/conf/logwatch.conf.
Check time sync. Ubuntu's default systemd-timesyncd is running and adequate for most VPS workloads. If you're running a database, managing TLS certificates, or anything where timestamp accuracy matters, install chrony instead: sudo apt install chrony && sudo systemctl enable --now chrony.
What's next
The baseline is in place. The server isn't accepting password logins, root SSH access is closed, the firewall is running default-deny, security patches are applying automatically, and intrusion detection is watching for attack patterns.
Now you can actually run something on it. If you're setting up DNS filtering across all devices on your network, the AdGuard Home self-hosted guide covers that. To expose self-hosted services without opening additional firewall ports, Cloudflare Tunnel with Docker is clean and free. And when you're ready for a deeper pass at system hardening beyond this baseline — audit logging, service minimisation, more granular access controls — the Linux security guide picks up where this one leaves off.
"If you want to audit what you've set up, the server hardening checklist scores your configuration across all 29 items in real time."