Ovaj tekst još uvek nije preveden na srpski — čitate englesku verziju. Otvorite original.

CYBERSECURITY

How to Harden a Linux VPS in 2026: Step-by-Step Checklist

A practitioner checklist to harden a Linux VPS: SSH keys, firewall, fail2ban, auto-updates and a Lynis audit. Copy-paste commands for Ubuntu 24.04 and Debian.

Kage System·24. jul 2026.

Hardening a Linux VPS means shrinking its attack surface: create a non-root sudo user, enforce SSH key authentication, disable root and password logins, enable a default-deny firewall, add fail2ban or CrowdSec, turn on automatic security updates, and audit the result with Lynis. This checklist walks through every step on Ubuntu 24.04 and Debian 12/13.

Close-up of an Ubuntu terminal at a shell prompt about to run a sudo command
Photo by Gabriel Heinzer on Unsplash.

Every time we spin up a new VPS for a client — usually a Hetzner Cloud box — we run the same hardening pass before anything else touches it. Below is that exact pass, in the order we actually do it, with the commands, the reasoning, and a quick way to verify each step worked. It’s written for a fresh Ubuntu 24.04 LTS or Debian 12/13 server, but almost everything applies to any modern systemd distro.

Read this first. Several steps below can lock you out if done carelessly. The golden rule: keep a second SSH session open while you work, and never disable a login method until you’ve confirmed the replacement works in that second session. If something breaks, you still have a live root shell to fix it — or your provider’s web console as a backstop.

The 10 steps at a glance

  1. Update the system and enable automatic security updates
  2. Create a non-root user with sudo
  3. Set up SSH key authentication (Ed25519)
  4. Harden sshd: disable root login and password auth
  5. Enable a default-deny firewall (UFW)
  6. Block brute-force attacks with fail2ban (or CrowdSec)
  7. Reduce the attack surface — disable services, close ports
  8. Apply kernel and network hardening (sysctl)
  9. Put admin access behind a VPN (optional but worth it)
  10. Log, audit, and verify with auditd and Lynis

What VPS hardening actually protects you from

A brand-new server doesn’t get a grace period. The moment it has a public IP, automated bots start scanning and probing it. In a classic Sophos study, a cloud honeypot was attacked within 52 seconds of going live, and the honeypot network averaged around 13 attempted attacks per minute each (Sophos, Exposed: Cyberattacks on Cloud Honeypots, 2019).

The overwhelming majority of that traffic is dumb, automated SSH brute-forcing. The SANS Internet Storm Center publishes live honeypot data, and the pattern barely changes year to year: the most-tried username is root by a huge margin, followed by admin, ubuntu, user, and test; the most-tried passwords are 123456, password, admin, and root. F5 Labs found that root is attacked roughly three times more often than any other username, and that SSH brute-forcing dwarfs most other attack types by volume.

The scale keeps climbing, too. GreyNoise’s 2026 State of the Edge report tracked nearly three billion malicious sessions from 3.8 million unique IPs, and noted that 52% of remote-code-execution attempts came from IPs with no prior history — so reputation blocklists alone won’t save you. Verizon’s 2025 Data Breach Investigations Report found that credential abuse is the single most common way breaches start, and that attacks on internet-facing edge infrastructure grew roughly eightfold.

So on day one you’re not defending against a clever human who picked your box specifically. You’re defending against constant, dumb, automated noise. Almost all of it disappears the moment you enforce key-only SSH and a default-deny firewall — which is why those two steps do more for you than anything clever further down the list.

Before you start

You’ll need:

If your provider offers a network-level firewall (Hetzner Cloud Firewall, AWS security groups, etc.), configure it first, before the VM is even reachable. It’s free defense-in-depth that sits in front of the host: allow inbound only on SSH and 80/443, drop everything else. Even if you later misconfigure the host firewall, the edge filter is still standing. On Hetzner you create it in the Cloud Console or with the hcloud CLI — see the Hetzner Cloud Firewall docs.

Step 1 — Update the system and enable automatic security updates

First, patch everything. Old, repeated CVEs are still actively exploited — GreyNoise noted attackers hammering vulnerabilities that are years, even decades, old — so an unpatched box is low-hanging fruit.

sudo apt update && sudo apt full-upgrade -y
sudo apt autoremove --purge -y
sudo reboot   # only if the kernel or libc was updated

Then make patching automatic so you don’t have to remember. Unattended-upgrades applies security updates on its own:

sudo apt install -y unattended-upgrades apt-listchanges needrestart
sudo dpkg-reconfigure -plow unattended-upgrades

Open /etc/apt/apt.conf.d/50unattended-upgrades and enable the two lines most people leave commented out — automatic reboot at a quiet hour (so a patched kernel actually takes effect) and a failure notification:

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Mail "root";
Unattended-Upgrade::MailReport "on-change";

needrestart is the companion piece: after a library like libssl is patched, services still running the old code in memory need restarting. needrestart flags (or restarts) them so the fix is real, not just on disk.

Verify it worked:

sudo unattended-upgrades --dry-run --debug
# example output
Checking if system is running on battery is skipped. Please install powermgmt-base ...
Initial blacklist:
Initial whitelist (not strict):
Starting unattended upgrades script
Allowed origins are: origin=Ubuntu,archive=noble-security, ...

If you’re on Ubuntu, the free Ubuntu Pro tier (up to five machines) adds Livepatch, which applies kernel CVE fixes without a reboot — turn it on with sudo pro attach <TOKEN> and sudo pro enable livepatch.

Step 2 — Create a non-root user with sudo

Operating as root all day is a mistake: every process runs with full privileges, and root is the one account every bot on earth is trying to brute-force. Make a normal user and grant it sudo.

adduser deploy
usermod -aG sudo deploy

Copy your SSH setup over so you can log in as the new user (we’ll set up the key itself in the next step; if root already has an authorized_keys, this carries it across):

rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy/

Verify it worked — and do this before you touch SSH config. Open a second terminal on your own machine and confirm the new user can log in and escalate:

ssh deploy@YOUR_SERVER_IP
sudo whoami
# example output
root

If that prints root, you’re safe to proceed. If it doesn’t, fix it now while your original root session is still open.

Step 3 — Set up SSH key authentication

Keys replace passwords with a cryptographic pair: a public key on the server, a private key that never leaves your machine. This alone makes password brute-forcing — the single most common attack against your box — structurally impossible. Ed25519 is the modern default: short, fast, and strong.

Generate the key on your own machine, not the server:

ssh-keygen -t ed25519 -a 100 -C "deploy@your-laptop"

Copy the public key to the server:

ssh-copy-id deploy@YOUR_SERVER_IP
# or, if ssh-copy-id isn't available, paste ~/.ssh/id_ed25519.pub into
# /home/deploy/.ssh/authorized_keys on the server (one key per line)

Test it before going further: open a new session and confirm you log in without being asked for the account password (you may be asked for your key’s passphrase, which is different):

ssh deploy@YOUR_SERVER_IP

Keys, passwords, or passkeys?

For most servers an Ed25519 key with a strong passphrase is plenty. If you’re securing infrastructure that matters, consider a hardware-backed FIDO2 key (a YubiKey or similar). The private key physically lives on the token and every login requires a touch, so a compromised laptop still can’t authenticate:

ssh-keygen -t ed25519-sk -O verify-required -C "deploy@yubikey"

This needs OpenSSH 8.4+ on both ends. One gotcha: macOS’s bundled SSH isn’t built with FIDO2 support, so run brew install openssh if you use -sk keys from a Mac.

Step 4 — Harden sshd: disable root login and passwords

Now lock the front door. On Ubuntu 24.04 and Debian 12+, don’t edit /etc/ssh/sshd_config directly — put your changes in a drop-in file under /etc/ssh/sshd_config.d/. Drop-ins survive package upgrades, and the main config pulls them in right at the top.

Here’s the gotcha that catches people, and it caught us: for any given option, the first value wins, and the files are read in alphabetical order. Cloud images — Hetzner’s included — usually ship a 50-cloud-init.conf in that directory that already sets things like PasswordAuthentication. If your file sorts after it, your setting is silently ignored and you’ll swear you disabled password auth when you didn’t. So we name ours to sort first (00-), and we always confirm with sshd -T at the end rather than trusting the file.

sudo tee /etc/ssh/sshd_config.d/00-hardening.conf >/dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
AuthenticationMethods publickey
MaxAuthTries 3
MaxSessions 4
LoginGraceTime 20
AllowUsers deploy
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
GSSAPIAuthentication no
ClientAliveInterval 300
ClientAliveCountMax 2
LogLevel VERBOSE
EOF

What each of the important ones buys you:

Always validate the config before reloading, or a typo can lock you out:

sudo sshd -t && sudo systemctl reload ssh

Verify it worked — check that the running daemon reflects your settings, and confirm root and password logins are actually refused:

sudo sshd -T | grep -Ei 'permitroot|passwordauth|pubkey|maxauth|logingrace|allowusers'
# example output
permitrootlogin no
pubkeyauthentication yes
passwordauthentication no
maxauthtries 3
logingracetime 20
allowusers deploy

Keep that second session open until you’ve opened a fresh one and confirmed you can still get in as deploy.

A note on changing the SSH port. Moving SSH off port 22 is a popular “tip,” but be honest about what it does: it cuts log noise, because the ~99% of bots that only scan port 22 stop hitting you. It is not a security control — a full port scan finds your SSH daemon in seconds regardless. Treat it as optional log hygiene, never as protection. And if you do it on Ubuntu 24.04, note that ssh.socket now owns the listening port — a Port line in sshd_config is silently ignored. You have to edit the socket unit (or disable socket activation), and open the new port in both your host and network firewalls before you disconnect.

Step 5 — Enable a default-deny firewall (UFW)

Deny everything inbound by default, then open only the ports you actually serve. On Ubuntu and Debian we reach for UFW — a thin, readable front-end over the kernel’s nftables, and the right level of abstraction for a single server.

sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp     # allow SSH BEFORE enabling — or you lock yourself out
sudo ufw limit 22/tcp     # rate-limit repeated SSH connections
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

The single most common self-inflicted lockout is running ufw enable before allowing SSH. Don’t. ufw limit is a nice extra: it temporarily blocks any IP that makes six or more connections in 30 seconds, which knocks down casual brute-forcing on its own.

Verify it worked:

sudo ufw status verbose
# example output
Status: active
Default: deny (incoming), allow (outgoing), disabled (routed)

To                         Action      From
--                         ------      ----
22/tcp                     LIMIT       Anywhere
80/tcp                     ALLOW       Anywhere
443/tcp                    ALLOW       Anywhere
22/tcp (v6)                LIMIT       Anywhere (v6)

Don’t forget IPv6 — if your box has a v6 address, confirm the rules cover it (they show as (v6) above). Use UFW or raw nftables, never both at once, or you’ll fight conflicting rulesets.

Step 6 — Block brute-force attacks with fail2ban

Key-only auth already stops the logins, but bots will still hammer the port, filling your logs and burning a little CPU. A ban tool watches the auth log and firewalls off IPs that misbehave.

Install fail2ban and create a local jail config (never edit the packaged jail.conf directly):

sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
[DEFAULT]
backend = systemd
banaction = nftables-multiport
bantime  = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 YOUR_HOME_IP

[sshd]
enabled = true
EOF
sudo systemctl enable --now fail2ban

Set ignoreip to include your own address so you never ban yourself during a deploy loop.

Verify it worked:

sudo fail2ban-client status sshd
# example output
Status for the jail: sshd
|- Filter
|  |- Currently failed: 3
|  |- Total failed:     418
|  `- Journal matches:  _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
   |- Currently banned: 6
   |- Total banned:     51
   `- Banned IP list:   203.0.113.7 198.51.100.42 203.0.113.91 ...

(The IPs above are documentation-range placeholders — yours will be real ones caught in the act.)

fail2ban or CrowdSec?

fail2ban is the reliable, battle-tested default. The newer alternative, CrowdSec, adds crowd-sourced threat intelligence: it detects locally and shares signals across a community, so you can block IPs that are attacking other people before they reach you.

fail2ban CrowdSec
Model Local log-parse → local ban Local detection + crowd-sourced blocklist
Maturity Since 2004, everywhere Since 2020, fast-growing
Intelligence Only what your box sees Millions of contributors
Enforcement Built in Separate “bouncer” component
Setup One config file Agent + bouncer + collections

For a single box where you want dead-simple and local, fail2ban is perfect. For internet-facing servers where the community blocklist earns its keep, CrowdSec is the more modern choice. There’s no urgency to migrate a working fail2ban install — pick one and move on.

Step 7 — Reduce the attack surface

You can’t attack a service that isn’t running. See what’s actually listening on the network:

ss -tlnp
# example output
State   Recv-Q  Send-Q   Local Address:Port    Peer Address:Port  Process
LISTEN  0       128            0.0.0.0:22           0.0.0.0:*      users:(("sshd",...))
LISTEN  0       511            0.0.0.0:80           0.0.0.0:*      users:(("nginx",...))
LISTEN  0       4096         127.0.0.1:5432         0.0.0.0:*      users:(("postgres",...))

Anything listening on 0.0.0.0 (all interfaces) that doesn’t need to face the internet should either be disabled or bound to 127.0.0.1. Databases are the classic offender — Postgres and Redis should listen only on localhost. Then disable anything you don’t need; a print server or an mDNS responder has no business on a headless VPS:

systemctl list-unit-files --state=enabled
# disable whichever of these actually exist on your box (the redirect
# just swallows "unit not found" for the ones that were never installed):
sudo systemctl disable --now avahi-daemon cups rpcbind 2>/dev/null

The goal state: nothing on a public interface except SSH and the services you deliberately serve.

Step 8 — Kernel and network hardening (sysctl)

A set of kernel tunables closes off spoofing, redirect attacks, and information leaks. Many are already sane defaults on Ubuntu 24.04, but setting them explicitly documents intent and satisfies audit tools:

sudo tee /etc/sysctl.d/99-hardening.conf >/dev/null <<'EOF'
net.ipv4.conf.all.rp_filter = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.log_martians = 1
kernel.randomize_va_space = 2
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
EOF
sudo sysctl --system

One thing to watch: strict reverse-path filtering (rp_filter = 1) can drop legitimate traffic on hosts with asymmetric routing or multiple interfaces. If that’s you, use 2 (loose mode). Change one value at a time and test — over-tightening the kernel is a great way to create a mysterious outage you end up debugging at 2 a.m.

Step 9 — Put admin access behind a VPN (optional)

The strongest move for management access is to not expose SSH to the whole internet at all. Instead, run a WireGuard VPN and allow SSH only from the VPN’s internal subnet (via AllowUsers/Match Address in sshd and a tightened firewall rule). Now brute-force bots literally cannot reach your SSH port — they can’t route to it. This is more setup than most single-box projects need, but for anything holding real data it’s the highest-leverage step after key-only auth. WireGuard is fast, modern, and small enough to audit.

Step 10 — Log, audit, and verify

You want a before-and-after number you can actually point to. Two tools give you one.

auditd records security-relevant events — changes to user accounts, sudoers, and SSH config:

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

A minimal starting ruleset in /etc/audit/rules.d/hardening.rules:

-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k scope
-w /etc/ssh/sshd_config -p wa -k sshd

Load it with sudo augenrules --load, and review activity later with sudo aureport --auth.

Lynis gives you a single number to track progress. Run it before and after hardening and watch the score move:

sudo apt install -y lynis
sudo lynis audit system
# example output
  Hardening index : 67 [############        ]
  Tests performed : 258
  ...
  Lynis security scan details:
  Suggestions:
  - Configure password hashing rounds in /etc/login.defs [AUTH-9230]
  - Install a file integrity tool to monitor changes [FINT-4350]

A fresh, unhardened server typically lands somewhere around 55–65; after a pass like this one you should be comfortably into the 75+ range. Treat the Hardening Index as a progress metric between your own runs, not an absolute grade — chasing 100 usually means breaking something useful. Re-run it monthly and after any big change.

Ubuntu 24.04 vs Debian: what differs

The checklist is the same; a few details aren’t:

Everything else — the sudo user, keys, sshd drop-ins, UFW, fail2ban, sysctl — is identical.

The complete Linux VPS hardening checklist

Copy this into your runbook:

Frequently asked questions

Is a VPS secure by default? No. Default images typically ship with root SSH enabled, no firewall enforcement, and packages that may already have known CVEs. A fresh VPS is exposed to automated attacks within minutes of getting a public IP, so hardening is step one, not an optional extra.

How often should I update my VPS? Security patches should apply as soon as they’re available — automate this with unattended-upgrades. Do a manual review of the full system weekly, and a broader audit (including a Lynis re-scan) monthly. Reboot promptly after kernel updates, or use Livepatch to avoid the reboot.

Do I need fail2ban if I already use SSH keys? Keys stop unauthorized logins, but they don’t stop bots from hammering the port, filling your logs, and burning resources. fail2ban (or CrowdSec) bans the offending IPs, which keeps logs readable and cuts noise. They solve different problems, so run both.

Should I change the default SSH port? It’s optional log hygiene, not security. Moving off port 22 hides you from the ~99% of bots that only scan 22, so your logs get quieter — but a real attacker’s port scan finds SSH anywhere in seconds. Do it if you like clean logs; never rely on it as protection.

How do I know if my VPS has been hacked? Warning signs include unfamiliar processes or high load, logins from unknown IPs in your auth log (journalctl -u ssh, or /var/log/auth.log where it exists), user accounts you didn’t create, unexpected outbound network traffic, and changed system binaries. auditd, a Lynis scan, and periodic rkhunter/chkrootkit checks help surface these early.

Can hardening lock me out of my VPS? Yes — the two classic mistakes are enabling the firewall before allowing SSH, and disabling password auth before confirming key login works. Both are avoidable: keep a second SSH session open while you work, validate sshd config with sudo sshd -t before reloading, and know where your provider’s web console is as a backstop.


This walkthrough reflects the hardening pass we run on new Linux servers at Kage System. Commands were written against Ubuntu 24.04 LTS and Debian 12/13; the terminal outputs shown are representative examples using documentation-range IP addresses, not captures of any specific customer system. Verify each step in your own environment, and when in doubt, keep that second session open.