← All articles

How to Harden a Fresh VPS: The Mental Model Behind Linux Security

EPT Lab · 2026-06-26
Server room with rack-mounted servers illuminated by blue indicator lights

How to Harden a Fresh VPS: The Mental Model Behind Linux Security

The median time between a CVE going public and an attacker weaponizing it dropped to 5 days in 2025, according to the Rapid7 2026 Global Threat Landscape Report. Your cloud provider hands you a fresh Ubuntu server. You think you have time to configure it properly. You don’t.

Every fresh VPS ships with root SSH enabled, password authentication on, no firewall rules, and zero intrusion detection. That’s not a vendor oversight. It’s a deliberate tradeoff between compatibility and security. The problem is that the tradeoff was made for general audiences, not for a server that’s about to receive real traffic.

This guide teaches you the mental model behind Linux server hardening, then walks through four concrete steps: SSH lockdown, UFW firewall setup, fail2ban installation, and automatic security updates. You’ll finish in under 30 minutes and eliminate the most common attack vectors.

Learn Linux from scratch with the EPT Lab Linux fundamentals course.

Key Takeaways - A fresh VPS has 120+ settings that fail CIS Level 2 secure configuration (CIS Benchmarks, Oct 2024) - The median time-to-exploit for top routinely exploited CVEs dropped to 5 days in 2025; organizations still take a median 32 days to patch (Rapid7 2026) - SSH key auth + PermitRootLogin no eliminates the attack vector behind 427 million annual brute-force attempts - UFW deny-by-default + fail2ban + unattended-upgrades form a layered defense you can deploy in one session


What Does “Secure by Default” Actually Mean?

A fresh Ubuntu 24.04 LTS server fails more than 120 default settings when measured against the CIS Level 2 Benchmark (CIS Benchmarks, Oct 2024). That number surprises most people. It shouldn’t. “Secure by default” is a marketing phrase, not a technical guarantee.

The mental model you need is this: attack surface equals everything that can receive input. A network port is an input. An SSH service accepting password logins is an input. An unpatched kernel module is an input. Every one of those inputs is a potential entry point, and a fresh VPS has dozens of them open by default.

[UNIQUE INSIGHT] Most hardening guides give you a checklist. What they skip is the threat model. Without understanding why each surface exists, you can’t prioritize which ones to close first. A VPS facing the public internet has three main attack vectors.

Open ports. Services bind to ports. Ports are visible to anyone who scans your IP. A port scanner will find your server within minutes of it going live. That’s not paranoia; it’s documented behavior in honeypot research.

Default or weak credentials. Root SSH with password authentication is the single most exploited configuration on cloud servers. Attackers don’t need zero-days. They use credential lists.

Unpatched software. Known vulnerabilities with public exploits are indexed by the attack community. If your kernel or SSH daemon hasn’t been updated, a script can match your version to a CVE and exploit it automatically.

Citation Capsule: The CIS Benchmark for Ubuntu 24.04 LTS identifies more than 120 default configuration settings that fail Level 2 secure configuration. These include open network services, default SSH settings, world-readable directories, and missing kernel hardening flags. (CIS Benchmarks, Oct 2024)

See the EPT Lab sysadmin fundamentals series for the Linux mental models this guide assumes.


Why Does the Patch Window Kill You Before You Even Know It?

The Rapid7 2026 Global Threat Landscape Report tracks two different metrics for how fast attackers move. The median time-to-exploit for the top routinely exploited CVEs dropped from 8.5 days in 2024 to 5.0 days in 2025 — the headline number for the most aggressively targeted vulnerabilities. Across all CVEs tracked, the average weaponization time is 19.5 days, while organizations take a median of 30.6 days to patch — an 11-day structural exposure window baked into every patch cycle (Rapid7 2026 Global Threat Landscape Report, Mar 2026). Either figure is bad; the 5-day number tells you how fast the worst-case scenarios move.

The mental model here is uncomfortable: the exploit clock starts at CVE publication, not at your awareness. You don’t have to know about a vulnerability for it to be used against you. The moment a CVE goes public, automated scanners start probing the internet for vulnerable versions.

It gets worse. The CISA Known Exploited Vulnerabilities (KEV) catalog documents cases where exploits were already active before the corresponding CVE was publicly disclosed. Even with perfect situational awareness, organizations taking 30+ days to patch are structurally exposed.

In 2025, the NIST National Vulnerability Database recorded 48,185 new CVEs — approximately 131 per day. The Linux kernel alone accumulated 2,879 of them (NIST NVD, 2025 CVE Statistics). Even with perfect awareness, manual patching at that velocity is impossible.

Median Time-to-Exploit: Top Routinely Exploited CVEs 2024 8.5 days 2025 5.0 days ↓ 41% Source: Rapid7 2026 Global Threat Landscape Report
Median exploitation time for the top actively targeted CVEs fell 41% in one year.

This is why automatic security updates aren’t a convenience. They’re the only realistic response to a 5-day exploitation window.

For monitoring what gets patched, see the EPT Lab guide to reading apt logs.


SSH Lockdown: Why Your Front Door Is Wide Open

A 2024 USENIX NSDI study from the University of Utah — Where The Wild Things Are: Brute-Force SSH Attacks In The Wild And How To Stop Them — analyzed 427 million SSH brute-force attempts across 500+ servers over four years and concluded that attacks are “rapidly growing more aggressive” (Singh et al., USENIX NSDI 2024). SSH is not a niche attack surface. It’s the primary target for automated credential stuffing on every public-facing Linux server.

A dark terminal window showing an active SSH session with green text on black background, representing a secured remote server connection

The mental model: SSH is your front door. Default Ubuntu configuration leaves it wide open: root login enabled, password authentication accepted, standard port 22. You’re going to close all three.

Create a Non-Root User

Don’t do anything else until you’ve created a user account with sudo access. Logging in as root gives an attacker who gains access complete, unrestricted control.

# Run as root on your fresh VPS
adduser deploy
usermod -aG sudo deploy

Verify the group assignment:

groups deploy
# Expected output: deploy : deploy sudo

Generate an SSH Key Pair (on Your Local Machine)

Run this on your laptop or workstation, not the server:

ssh-keygen -t ed25519 -C "your-email@example.com"
# Follow the prompts; save to ~/.ssh/id_ed25519
# Set a passphrase — this matters

ED25519 keys are faster and more resistant to side-channel attacks than RSA 2048. Don’t skip the passphrase.

Copy the Public Key to Your Server

ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@YOUR_SERVER_IP
# You'll be prompted for deploy's password (last time you'll need it)

Test that key auth works before you disable passwords:

ssh -i ~/.ssh/id_ed25519 deploy@YOUR_SERVER_IP
# You should land on the server without a password prompt

Harden the SSH Daemon Configuration

Edit the SSH config on the server:

sudo nano /etc/ssh/sshd_config

Find and change these lines (or add them if missing):

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
# Optional: change the default port
# Port 2222

Restart the daemon:

sudo systemctl restart sshd
# Keep your current session open while you test in a NEW terminal window

Open a second terminal and verify you can still connect with your key. If it works, close the old session. If it fails, you still have an open session to fix it.

[PERSONAL EXPERIENCE] The first time you disable password authentication and reconnect successfully, there’s a specific moment of relief followed immediately by the realization that you’ve also locked out anyone without the key. Keep a backup access method, whether that’s your cloud provider’s console or a second authorized key, before you close that original terminal.

Citation Capsule: A University of Utah study published at USENIX NSDI 2024 analyzed 427 million SSH brute-force attempts across more than 500 honeypot servers over four years. The researchers found attacks are “rapidly growing more aggressive,” with automated tools cycling through credential lists at increasing speed and scale. (Singh et al., USENIX NSDI 2024)

For key rotation, agent forwarding, and managing access across multiple servers, see the EPT Lab SSH key management guide.


UFW: Why Deny-by-Default Is the Only Rational Starting Position

In 2025, the Verizon Data Breach Investigations Report found that vulnerability exploitation was the initial access vector in 20% of all breaches — a 34% year-over-year increase — while human error and misconfiguration drove a further quarter of incidents (Verizon DBIR 2025, Apr 2025). IBM’s Cost of a Data Breach Report 2024 puts the global average cost at $4.88 million per incident (IBM Cost of a Data Breach Report 2024, Jul 2024). A missing firewall rule is misconfiguration. It’s also one of the fastest to fix.

Multiple terminal screens showing firewall rules and network traffic logs, representing active UFW configuration on a Linux server

Run ufw status on a fresh Ubuntu server and you’ll see one word: inactive. There are no firewall rules. Every port that a running service binds to is immediately reachable from the internet.

Set Deny-by-Default and Open Only What You Need

sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (use your custom port if you changed it)
sudo ufw allow 22/tcp

# Allow web traffic if this is a web server
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

# Enable the firewall
sudo ufw enable
# You'll be asked to confirm — type y

# Verify the active rules
sudo ufw status verbose

Expected output:

Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW IN    Anywhere
80/tcp                     ALLOW IN    Anywhere
443/tcp                    ALLOW IN    Anywhere

Why Deny-by-Default Is the Only Reasonable Starting Position

Allow-by-default means every port is open until you explicitly close it. Deny-by-default means every port is closed until you explicitly open it. These aren’t equivalent starting positions; they have completely different threat profiles.

With allow-by-default, you have to know which ports to close. With deny-by-default, you only have to know which ports your application needs. The second problem is dramatically smaller and easier to audit.

[UNIQUE INSIGHT] The common mistake is treating a firewall as a list of things to block. That mental model puts you in reactive mode: you block what you’ve seen before. Deny-by-default flips this. You’re now describing what legitimate traffic looks like. Anything that doesn’t match that description is dropped, including attack patterns you’ve never seen.

Root Causes of Data Breaches (2025) Data Breaches Misconfiguration / Human Error — 26% IT Failure — 23% Credential Abuse — 22% Vulnerability Exploitation — 20% Other — 9% Source: Verizon DBIR 2025 · IBM Cost of a Data Breach Report 2024
Misconfiguration and IT failure together account for nearly half of all breaches — both directly addressable by VPS hardening.

fail2ban: What It Covers and What It Doesn’t

The same USENIX NSDI 2024 study found that fail2ban’s default configuration blocks approximately 66.1% of SSH brute-force attempts. That sounds good until you read the inverse: 34% of attempts still get through on default settings (Singh et al., USENIX NSDI 2024). fail2ban is a rate-limiter and an IP blocker. It’s not a replacement for SSH key authentication.

The mental model: fail2ban watches log files, detects repeated failed logins from a single IP, and writes a temporary block rule to iptables. It buys time. It slows attackers down. Against credential stuffing from distributed IP ranges, it’s limited. That’s why it works best as a layer on top of SSH keys, not instead of them.

Install and Configure fail2ban

sudo apt install fail2ban -y

# Never edit jail.conf directly — it gets overwritten on upgrades
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

Find the [sshd] section and configure it:

[sshd]
enabled  = true
port     = ssh
maxretry = 3
bantime  = 1h
findtime = 10m

This bans any IP that fails authentication 3 times within 10 minutes, for 1 hour.

sudo systemctl enable fail2ban
sudo systemctl start fail2ban

# Check that the SSH jail is active
sudo fail2ban-client status sshd

Expected output:

Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     0
`- Actions
   |- Currently banned: 0
   |- Total banned:     0

The Defense-in-Depth Argument

Why run fail2ban at all if 34% of attempts still get through? Because you’re not choosing between fail2ban and SSH keys. You’re running both. SSH key authentication means a brute-force attempt against password authentication fails immediately at the protocol level. fail2ban then bans the IP, reducing log noise and protecting any other services on the server.

The USENIX study also found that more sophisticated blocking strategies, like dictionary-based IP blocking, can stop 99.5% of attempts. fail2ban’s 66% is a floor, not a ceiling. You can tune it.

Citation Capsule: A University of Utah analysis of 427 million SSH brute-force attempts found that fail2ban’s default configuration blocks approximately 66.1% of attempts, while dictionary-based IP blocking blocks 99.5%. The gap exists because fail2ban operates reactively per-IP, while dictionary blocking targets known attack infrastructure proactively. (Singh et al., USENIX NSDI 2024)


Automatic Security Updates: Closing the 27-Day Patch Gap

The Verizon DBIR 2025 found that only 54% of vulnerable devices are fully remediated within a year, with a median patch time of 32 days (Verizon Data Breach Investigations Report 2025). Against a 5-day median exploitation window, 32 days isn’t a patch cadence. It’s an exposure guarantee.

The unattended-upgrades package solves the problem for security patches specifically. It won’t update major versions or break your application dependencies. It watches the security pocket of the Ubuntu repository and applies patches automatically.

sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades
# Select "Yes" at the prompt

Verify the configuration was written:

cat /etc/apt/apt.conf.d/20auto-upgrades

Expected output:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

The 1 values mean: check for updates daily, apply security upgrades daily. You can also confirm which repositories are covered:

cat /etc/apt/apt.conf.d/50unattended-upgrades | grep -A2 "Allowed-Origins"
# Should show Ubuntu security pocket enabled
The Patch Gap: Attacker vs. Defender (Days) Attacker 5 days Defender 32 days ← 27-day exposure window → Source: Rapid7 2026 Global Threat Landscape Report · Verizon DBIR 2025
Attackers exploit CVEs 6× faster than the median organization patches them — a structural gap that only automation can close.

With unattended-upgrades active, your effective patch window for security updates drops from 32 days to under 24 hours. That’s not a complete solution. But it eliminates the gap between CVE publication and your first line of defense.

Configure email alerts for upgrade events with the EPT Lab unattended-upgrades monitoring guide.


If you want to go beyond these four steps, the EPT Lab Linux Security course covers the full CIS Benchmark implementation, intrusion detection with OSSEC and AIDE, and centralized log monitoring.


What You’ve Built — and What Comes Next

Verizon’s DBIR 2025 reported that supply chain breaches doubled from 15% to 30% of incidents in a single year, and ransomware appeared in 44% of all breaches, with 88% of those targeting small and medium businesses (Verizon DBIR 2025). Layer-one hardening doesn’t stop supply chain attacks. It does eliminate the low-effort intrusions that dominate the attack landscape.

What you’ve built after these four steps:

The remaining attack surface is smaller and harder to exploit. But it still exists.

What comes after this guide? Intrusion detection with OSSEC or AIDE lets you monitor file integrity changes, which is how you catch attackers who’ve already gotten in. Centralized log shipping to a SIEM means your logs survive if the server is compromised. A full CIS Benchmark Level 2 audit will surface the remaining 100+ configuration gaps that this guide doesn’t cover.

The next step is a full CIS Benchmark Level 2 audit for Ubuntu 24.04 — covered in the EPT Lab Linux Security course.


FAQ

How long does VPS hardening actually take?

The four steps in this guide, from creating a non-root user to enabling automatic updates, take under 30 minutes on a fresh server. A full CIS Benchmark Level 2 audit takes 2 to 3 hours and requires working through 200+ individual controls. Start with the four steps here, then run the CIS audit tool (ubuntu_cis_hardening.sh) when you have a maintenance window.

Do I need all of these steps if my VPS is brand new?

Yes. “Brand new” describes the server’s age, not its security posture. The CISA Known Exploited Vulnerabilities catalog documents exploits active before their CVE was published, and Rapid7’s 2026 Threat Landscape Report found attackers weaponize vulnerabilities in 19.5 days on average (Rapid7 2026). A fresh VPS running an unpatched kernel is vulnerable to CVEs published before you provisioned it. Age provides no protection.

Is fail2ban enough to stop SSH attacks?

On its own, fail2ban blocks approximately 66% of SSH brute-force attempts on default configuration (Singh et al., USENIX NSDI 2024). The remaining 34% get through, whether from distributed IP ranges or slow-and-low attacks that stay under the rate threshold. SSH key authentication eliminates password-based brute force entirely. Run both, not one or the other.

What’s the difference between UFW and iptables?

UFW is a frontend for iptables. When you run ufw allow 22/tcp, UFW translates that into iptables rules and writes them to the kernel’s packet filtering system. You’re always using iptables underneath. UFW’s value is readability and the deny-by-default posture it enforces from the first ufw enable command. Without it, Ubuntu ships with no active iptables rules, leaving all 120+ CIS-flagged default open ports reachable (CIS Benchmarks, Oct 2024).


What You Just Learned


Sources

  1. Rapid7. 2026 Global Threat Landscape Report. Mar 18, 2026. Retrieved Jun 2026. https://www.rapid7.com/about/press-releases/rapid7-2026-global-threat-landscape-report-shows-exploited-high-and-critical-severity-vulnerabilities-surged-105-as-attack-timelines-collapsed/
  2. Verizon. Data Breach Investigations Report 2025. Apr 2025. Retrieved Jun 2026. https://www.verizon.com/business/resources/reports/dbir/
  3. NIST. National Vulnerability Database — CVE Statistics. Retrieved Jun 2026. https://nvd.nist.gov/vuln/search/statistics
  4. CISA. Known Exploited Vulnerabilities Catalog. Retrieved Jun 2026. https://www.cisa.gov/known-exploited-vulnerabilities-catalog
  5. CIS Security. CIS Benchmark for Ubuntu Linux 24.04 LTS. Oct 2024. Retrieved Jun 2026. https://www.cisecurity.org/benchmark/ubuntu_linux
  6. Singh et al., University of Utah. Where The Wild Things Are: Brute-Force SSH Attacks In The Wild And How To Stop Them. USENIX NSDI 2024. https://www.usenix.org/system/files/nsdi24-singh-singh.pdf
  7. IBM. Cost of a Data Breach Report 2024. Jul 2024. Retrieved Jun 2026. https://www.ibm.com/reports/data-breach
  8. OSSEC — Open Source Host-based IDS. https://www.ossec.net/
  9. AIDE — Advanced Intrusion Detection Environment. https://aide.github.io/