Skip to main content
Server & DevOpsMay 23, 20268 min read

Hardening A Fresh Ubuntu 24.04 VPS In 15 Minutes

A fresh public Ubuntu 24.04 VPS sees SSH brute-force attempts within a minute of getting an IP. The 15-minute hardening run that closes the obvious doors before production traffic, with the verification commands.

SSH Brute Force Starts In Under 60 Seconds

A fresh Ubuntu 24.04 VPS exposed to the public internet sees its first SSH connection attempt within 30 to 60 seconds of going live. By the end of the first hour it has been scanned by automated bots for several thousand passwords. None of this is exotic. It is the baseline noise floor of the modern internet.

The 15-minute hardening run in this post does not turn a VPS into a fortress. It closes the obvious doors so the baseline noise floor does not become a successful login. Anything more sophisticated is a separate exercise. Everything below is meant to run on a freshly-provisioned root-access VPS, in order, no skipping.

Pre-Flight At Provisioning

Two decisions made at provisioning time save work later:

  • Provide an SSH key during VPS creation. Most providers (Hetzner, AWS, Vultr, DigitalOcean) accept an SSH public key in the create-server form. The new VPS boots with that key already in /root/.ssh/authorized_keys. You log in with the key on the first try. No password ever set on root.
  • Pick a region in line with your data-residency requirements. A reversible choice at provisioning, much harder to migrate later.

Now SSH in as root and run through the steps below.

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

The unattended-upgrades package is configured to automatically apply security updates on a daily cron. The next CVE that drops gets patched without you logging in.

Reboot if the upgrade installed a new kernel:

[ -f /var/run/reboot-required ] && reboot

Step 2: Create A Non-Root User With Sudo

Operating as root is a habit worth breaking, on a fresh VPS more than anywhere else.

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 SSH session open until the verification works. If something breaks, you still have a way in.

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

Restart SSH:

systemctl restart ssh

Verify the new policy is in effect:

sshd -T | grep -E "permitrootlogin|passwordauthentication|allowusers"

If all three show the hardened values, root login over SSH is closed and password auth is gone. Drop the original root SSH session.

Changing the SSH port is a separate decision. It is security-by-obscurity at best, useful only for cutting brute-force log noise. If you want the noise reduction, change it. If you want hardening, the steps below matter much more.

Step 4: Firewall With UFW

Open only what you need.

ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
# Add other ports your service needs:
# ufw allow 80/tcp
# ufw allow 443/tcp
ufw --force enable
ufw status verbose

If your application server lives behind a load balancer or reverse proxy on another machine, restrict the application ports to the proxy's IP only:

ufw allow from 10.0.0.5 to any port 8080

Step 5: Fail2ban For The Brute-Force Floor

Even with key-only SSH, bots hammer port 22 forever. Fail2ban temporarily bans IPs that hit the failed-attempt 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

The bans are temporary, in-memory, and refresh on the schedule above. The log noise drops within an hour.

Step 6: Kernel Network Hardening

A small set of sysctls closes common network-level attacks. Drop a file into /etc/sysctl.d/:

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

Step 7: Time Sync

A drifting clock breaks TLS, breaks log correlation, and silently breaks anything that uses timestamps for authentication (JWTs, signed requests).

apt-get -y install chrony
systemctl enable --now chrony
chronyc tracking

The Last offset value in the tracking output should be near zero after a minute of sync.

Step 8: Disable Services You Will Not Use

Check what is listening and trim it.

ss -tulpn
systemctl list-units --type=service --state=running

The defaults on Ubuntu 24.04 are reasonable, but check for avahi-daemon, cups, bluetoothd, and snapd (if not using snaps). Disable what you do not need:

systemctl disable --now snapd.socket snapd.service
apt-get -y purge snapd

Step 9: Basic Audit Logging

Enable auditd to log security-relevant events:

apt-get -y install auditd audispd-plugins
systemctl enable --now auditd

The defaults log identity changes, sudo activity, and file system modifications to /var/log/audit/audit.log. For a production server, point this stream at your central log collector when one is configured.

Step 10: A Periodic Reality Check

Set a weekly cron that emails the operator a summary of what is listening, who has logged in, and what failed:

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
} | mail -s "Weekly hardening snapshot $(hostname)" ops@example.com
EOF
chmod +x /etc/cron.weekly/hardening-snapshot

Requires mailutils and an SMTP relay. If the VPS has no outbound mail path, swap the mail call for a writeout to a known directory and grab the file on schedule.

What This Does Not Cover

The 15-minute run is the baseline. What it does not cover:

  • Application-layer hardening (web server, database, Redis, etc.).
  • Network segmentation across multiple servers.
  • Centralized log aggregation and SIEM integration.
  • Intrusion detection beyond fail2ban (OSSEC, Wazuh, Falco).
  • Backup automation and tested restore procedures.
  • Compliance frameworks (SOC 2, ISO 27001, PCI-DSS).

Each is a separate body of work and the right configuration depends on the application. The hardening above is what every Ubuntu 24.04 VPS should have before you put anything on it.

Verification In Five Commands

When the run is done, these five commands confirm the state:

sshd -T | grep -E "permitrootlogin|passwordauthentication"
ufw status verbose
fail2ban-client status sshd
chronyc tracking
ss -tulpn

SSH root login no, password auth no, ufw active with a clean rule set, fail2ban running, chrony in sync, and only the services you expect listening: the box is ready for production workload.

Bottom Line

Fifteen minutes after first boot, the VPS has key-only SSH, a working firewall, fail2ban on port 22, kernel-level network hardening, automatic security updates, time sync, and a weekly self-report. The brute-force baseline noise floor stops mattering. You spent five minutes more than the cloud-init shell would have, and you got a meaningfully harder target.

If you want this run automated across a fleet, or layered on top of application-specific hardening (Magento, WordPress, Node.js, Kubernetes, Postgres), that is what our servers management and security and compliance work covers. Repeatable, version-controlled, and audited.

Need help with this?

Our team handles this kind of work daily. Let us take care of your infrastructure.