SSH Brute Force Starts Before You Finish Your Coffee
A fresh Ubuntu 24.04 VPS exposed to the public internet starts receiving unsolicited SSH connection attempts within minutes of getting an IP. Not because you are interesting, but because the entire IPv4 space is scanned continuously by automated infrastructure. Honeypot research documents this as a constant background process rather than an event: SANS ISC honeypot diaries routinely record dozens of attempts in the first minutes of exposure, and long-term honeynet studies show the traffic never stops, it only varies in volume.
You will see figures like "30 seconds" quoted a lot, including in earlier versions of this article. The honest version is that it depends on your provider's IP range and how recently that block was scanned. What is not in dispute: it happens fast, it is automated, and it is relentless.
The 15-minute run below does not turn a VPS into a fortress. It closes the obvious doors so the baseline noise floor never becomes a successful login. Everything here runs on a freshly provisioned root-access VPS, in order, no skipping.
What Each Step Actually Buys You
Before the commands, the map. If you only have time for some of this, the top three rows are the ones that matter.
| Step | Closes | If you skip it |
|---|---|---|
| Key-only SSH, no root login | Credential guessing entirely | Every bot on the internet gets unlimited guesses at your password |
| Firewall, default deny | Everything you did not mean to expose | That Redis you bound to 0.0.0.0 "temporarily" is now public |
| Automatic security updates | The next CVE, unattended | You patch when you remember, which is after the exploit is public |
| fail2ban | Log noise, slow credential attacks | Logs full of junk, and a slow-drip attack never gets throttled |
| Kernel sysctls | SYN floods, spoofing, ICMP tricks | Cheap network-level attacks that a one-line setting would have stopped |
| Time sync | TLS validation and log correlation breaking silently | Certificates "randomly" invalid, logs impossible to correlate |
| auditd | Not knowing what happened afterwards | An incident with no forensic trail |
Pre-Flight At Provisioning
Two decisions made at creation time save work later:
- Provide an SSH key in the create-server form. Hetzner, AWS, Vultr, and DigitalOcean all accept one. The VPS boots with the key in
/root/.ssh/authorized_keysand no password is ever set on root. This closes the window where a password-authenticated root account exists on a public IP. - Use ed25519, not RSA. If you are generating a key for this:
ssh-keygen -t ed25519 -a 100 -C "ops@yourcompany"
Ed25519 keys are shorter, faster, and have no key-size footgun. RSA is fine at 4096 bits but there is no reason to choose it for a new key in 2026.
- Check whether your provider has a network-level firewall too. Hetzner Cloud Firewalls, AWS security groups, and DigitalOcean Cloud Firewalls filter before traffic reaches the VPS. UFW filters on the VPS. They are different layers and you want both: the cloud firewall is your outer wall and survives a misconfigured UFW, while UFW protects you if the cloud rule is loosened by someone else on your team. Do not treat one as a replacement for the other.
Now SSH in as root.
Step 1: Patch Everything First
Before changing any other configuration, apply pending security updates.
apt-get update
apt-get -y upgrade
apt-get -y install unattended-upgrades apt-listchanges
dpkg-reconfigure --priority=low unattended-upgrades
Now the step that most guides omit, and the reason plenty of servers think they are patching and are not. Verify it actually runs:
# Are the enable flags really set?
cat /etc/apt/apt.conf.d/20auto-upgrades
# Want:
# APT::Periodic::Update-Package-Lists "1";
# APT::Periodic::Unattended-Upgrade "1";
# Dry-run the real thing and watch what it decides
unattended-upgrade --dry-run --debug 2>&1 | tail -20
# After a day, confirm it has actually been running
grep -c . /var/log/unattended-upgrades/unattended-upgrades.log
systemctl status unattended-upgrades --no-pager
Installing the package is not the same as enabling it, and a silent no-op here means you are unpatched while believing otherwise. Check the log again a week later.
Reboot if the upgrade installed a new kernel:
[ -f /var/run/reboot-required ] && reboot
Step 2: Create A Non-Root User With Sudo
adduser --disabled-password ops
usermod -aG sudo ops
mkdir -p /home/ops/.ssh
cp /root/.ssh/authorized_keys /home/ops/.ssh/
chown -R ops:ops /home/ops/.ssh
chmod 700 /home/ops/.ssh
chmod 600 /home/ops/.ssh/authorized_keys
Test it in a second terminal before continuing:
ssh ops@<vps-ip>
sudo whoami # should print "root"
Keep the original root session open until that works. This is not ceremony. The next step disables root login, and if the ops key is wrong you have just locked yourself out of a machine whose only recovery path is your provider's web console.
Step 3: Lock Down SSH
Drop a hardening file at /etc/ssh/sshd_config.d/99-hardening.conf:
PermitRootLogin no
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
AuthenticationMethods publickey
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
LoginGraceTime 30
AllowUsers ops
AllowUsers ops is the line to think twice about. It is a hard allowlist: any account not named here cannot SSH in, including ones you create later. That is the point, but it also means a future you, adding a deploy user and wondering why its key is rejected, will spend twenty minutes on it. Note it somewhere.
Restart and verify:
systemctl restart ssh
sshd -T | grep -E "permitrootlogin|passwordauthentication|allowusers"
sshd -T prints the effective configuration after all includes are merged, which is why it is the right check rather than reading the file back. If those three show the hardened values, root login is closed and password auth is gone. Now drop the original root session.
Changing the SSH port is a separate decision. It is security by obscurity at best, useful only for cutting brute-force log noise. It stops zero targeted attackers. If you want quieter logs, change it. If you want hardening, everything else on this page matters more.
Step 4: Firewall With UFW
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
# Add only what the service actually needs:
# ufw allow 80/tcp
# ufw allow 443/tcp
ufw --force enable
ufw status verbose
If the application sits behind a load balancer or reverse proxy on another machine, do not open the app port to the world. Restrict it to the proxy:
ufw allow from 10.0.0.5 to any port 8080
The most common real-world failure here is not UFW itself, it is a service bound to 0.0.0.0 that someone later exposes at the cloud-firewall layer, assuming UFW will save them. Check what is actually listening (Step 8) rather than assuming.
Step 5: Fail2ban For The Brute-Force Floor
Even with key-only SSH, bots hammer port 22 forever. Fail2ban bans IPs that cross a threshold.
apt-get -y install fail2ban
cat > /etc/fail2ban/jail.d/sshd.local <<'EOF'
[sshd]
enabled = true
port = 22
maxretry = 3
bantime = 1h
findtime = 10m
EOF
systemctl enable --now fail2ban
fail2ban-client status sshd
Be clear about what this is and is not. With PasswordAuthentication no already set, fail2ban is not what stops a break-in, the key requirement is. Fail2ban buys you quieter logs and throttles slow-drip attempts that stay under the radar. It is worth the two minutes, but do not let it substitute for Step 3.
Step 6: Kernel Network Hardening
cat > /etc/sysctl.d/99-hardening.conf <<'EOF'
# Source IP spoofing protection
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignore ICMP broadcast (smurf attack)
net.ipv4.icmp_echo_ignore_broadcasts = 1
# Ignore bogus ICMP responses
net.ipv4.icmp_ignore_bogus_error_responses = 1
# SYN flood mitigation
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 5
# Disable source routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# Disable ICMP redirect acceptance
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Log martian packets
net.ipv4.conf.all.log_martians = 1
EOF
sysctl --system
Verify they took, because a typo in a sysctl file fails quietly:
sysctl net.ipv4.tcp_syncookies net.ipv4.conf.all.rp_filter
One caveat on log_martians: it is useful for spotting spoofing, and it is also a way to fill a small disk if someone points a flood at you. If /var/log is tight, keep an eye on it or leave it off.
Step 7: Time Sync
A drifting clock breaks TLS validation, ruins log correlation, and silently breaks anything using timestamps for authentication (JWTs, signed API requests, TOTP).
apt-get -y install chrony
systemctl enable --now chrony
chronyc tracking
Last offset should be near zero after a minute. System time drifting by more than a second on a VPS usually means the host is oversubscribed, which is its own signal.
Step 8: Disable Services You Will Not Use
ss -tulpn
systemctl list-units --type=service --state=running
Read the ss output carefully: anything bound to 0.0.0.0 or [::] is listening on every interface, which is the thing you actually care about. A service on 127.0.0.1 is not exposed regardless of your firewall.
Ubuntu 24.04's defaults are reasonable, but check for avahi-daemon, cups, bluetoothd, and snapd:
systemctl disable --now snapd.socket snapd.service
apt-get -y purge snapd
Purging snapd is a preference, not a security requirement. If anything on the box needs snaps (some monitoring agents do), leave it.
Step 9: Basic Audit Logging
apt-get -y install auditd audispd-plugins
systemctl enable --now auditd
Defaults log identity changes, sudo activity, and filesystem modification to /var/log/audit/audit.log. Two things worth adding immediately, because the defaults will not tell you about the files that matter most:
cat > /etc/audit/rules.d/99-custom.rules <<'EOF'
# Watch authentication config
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /etc/ssh/sshd_config.d/ -p wa -k sshd_config
-w /etc/sudoers -p wa -k sudoers
-w /etc/sudoers.d/ -p wa -k sudoers
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
EOF
augenrules --load
auditctl -l
Now a change to your SSH config or sudoers is recorded with the user and timestamp. Search it later with ausearch -k sshd_config.
Audit logs are only useful if they leave the machine. An attacker with root deletes local logs. Ship them to a central collector as soon as you have one.
Step 10: A Periodic Reality Check
cat > /etc/cron.weekly/hardening-snapshot <<'EOF'
#!/bin/sh
{
echo "=== Listening sockets ==="
ss -tulpn
echo
echo "=== Recent successful logins ==="
last -20
echo
echo "=== Recent SSH failures ==="
journalctl -u ssh -n 50 --no-pager | grep -i fail
echo
echo "=== fail2ban status ==="
fail2ban-client status sshd
echo
echo "=== Pending security updates ==="
apt-get -s upgrade | grep -i security | head
} | mail -s "Weekly hardening snapshot $(hostname)" ops@example.com
EOF
chmod +x /etc/cron.weekly/hardening-snapshot
Requires mailutils and an SMTP relay. No outbound mail path? Write to a known directory and collect it on schedule instead. The point is that someone reads it: an unread weekly email is not a control, it is a filter rule.
Verification In Six Commands
sshd -T | grep -E "permitrootlogin|passwordauthentication" # both "no"
ufw status verbose # active, minimal rules
fail2ban-client status sshd # running
chronyc tracking # offset near zero
ss -tulpn # only expected listeners
cat /etc/apt/apt.conf.d/20auto-upgrades # both flags "1"
That last one is the addition worth keeping. It is the control most likely to be silently broken and the one nobody checks.
What This Does Not Cover
The 15-minute run is the baseline, not the destination:
- Application-layer hardening (web server, database, cache, etc.)
- Network segmentation across multiple servers
- Centralised log aggregation and SIEM
- Intrusion detection beyond fail2ban (Wazuh, OSSEC, Falco)
- Backup automation and, more importantly, tested restores
- Compliance frameworks (SOC 2, ISO 27001, PCI DSS)
- Multi-factor authentication on SSH, which is worth adding for anything sensitive
Each is separate work, and the right configuration depends on the application.
Bottom Line
Fifteen minutes after first boot the VPS has key-only SSH, a default-deny firewall, fail2ban, kernel network hardening, verified automatic security updates, time sync, audit rules on the files that matter, and a weekly self-report. The automated background noise of the internet stops being a risk and becomes what it should be: noise.
The one habit that matters more than any command here: check the boring controls periodically. Unattended-upgrades silently not running is far more likely to hurt you than an exotic kernel exploit.
If you want this run automated across a fleet, version-controlled and audited, or layered with application-specific hardening for Magento, WordPress, Node.js, Kubernetes, or Postgres, that is what our servers management and security and compliance work covers.
Sources
- Ubuntu Server documentation: security
- Ubuntu: automatic updates with unattended-upgrades
- OpenSSH sshd_config manual
- UFW documentation
- Fail2ban documentation
- Linux auditd documentation
- SANS Internet Storm Center: SSH honeypot attack analysis
- Attacks Come to Those Who Wait: long-term SSH honeynet observations, IMC 2025
Talk to the engineer who will own your stack.
No account managers, no offshore handoff. Senior DevOps, direct. Tell us what you are dealing with and you get a straight answer.
Related Articles
The Ultimate Guide to Linux Server Management in 2025
A comprehensive guide to modern Linux server management covering automation, containerization, cloud integration, AI-driven operations, security best practices, and essential tooling for 2025.
Server & DevOpsFixing "421 Misdirected Request" for Plesk Sites on Ubuntu 22.04 After Apache Update
Resolve the 421 Misdirected Request error affecting all HTTPS sites on Plesk for Ubuntu 22.04 after an Apache update, caused by changed SNI requirements in the nginx-to-Apache proxy chain.
Server & DevOpsHow to Set Up GlusterFS on Ubuntu
A complete guide to setting up a distributed, replicated GlusterFS filesystem across multiple Ubuntu 22.04 nodes, including installation, volume creation, client mounting, maintenance, and troubleshooting.