FR
live

10 commands turn an Ubuntu VPS from soft target into fortress — here are the only ones that matter

A freshly provisioned Ubuntu VPS on Hetzner, OVH, or DigitalOcean receives its first SSH brute-force attempt within 12 minutes. These 10 commands — AppArmor, firewalld, fail2ban, auditd, AIDE, kernel hardening, Lynis, and unattended-upgrades — lock it down in 5 minutes flat.

10 commands to harden an Ubuntu server — AppArmor, firewalld, fail2ban, auditd, AIDE, kernel hardening, Lynis, unattended-upgrades — ETTAYEB illustration

Ubuntu 26.04 LTS shipped on April 24, 2026, the CIS Benchmark for Ubuntu 24.04 lists 212 hardening recommendations, and Shodan currently indexes over 4 million servers with port 22 exposed and no brute-force protection. The reality is brutal: a Ubuntu VPS provisioned with default settings gets scanned by bots inside 12 minutes, and a weak SSH password falls to a dictionary attack in an average of 90 seconds.

This isn’t a Linux security lecture. It’s an emergency prescription — 10 commands, 5 minutes, zero exotic dependencies. Every command has one job, and every job addresses a documented attack surface. Copy them, paste them, and your server graduates from easy target to hard pass. Tested on Ubuntu 24.04 LTS and Ubuntu 26.04 LTS.

AppArmor — the MAC that’s already there, active but underused

AppArmor ships enabled by default on Ubuntu — it has since 7.10 Gutsy Gibbon in 2007. The problem isn’t the absence of AppArmor. It’s the number of profiles running in complain mode (log only) instead of enforce mode (actually blocking).

SELinux is the historical alternative, maintained by the NSA and Red Hat since 2000. It assigns security labels to every object in the system — files, sockets, processes — and enforces policy at the kernel level. The model is more granular, but the learning curve is vertical: every sysadmin knows the setenforce 0 reflex that follows three hours of debugging an incomprehensible denied.

AppArmor takes the opposite path: it binds profiles to file paths (/usr/sbin/nginx, /usr/bin/mysqld), which makes them readable and editable inside five minutes. The trade-off is real — an attacker who manages to relocate a binary potentially escapes its profile — but the probability that an AppArmor profile gets deployed and maintained in production is objectively higher than that of a correct SELinux policy.

The command that changes everything:

bash
# Check the current state — the complain/enforce ratio is the tell
sudo aa-status

# Move all profiles to enforce
sudo aa-enforce /etc/apparmor.d/*

After this, the kernel actually blocks unauthorized syscalls for every profiled service. If a profile breaks a legitimate service, aa-logprof scans the logs and suggests the rules to add — no denied without a fix.

The verdict is pragmatic: use AppArmor on Ubuntu and Debian, use SELinux on RHEL and derivatives. This isn’t about technical superiority — it’s about native support. The CIS Benchmark for Ubuntu documents 18 AppArmor-specific rules; porting them to SELinux would require a translation effort nobody will perform.

firewalld — three zones, zero unnecessary open ports

firewalld replaced iptables as the front-end for nftables starting with RHEL 7 (2014). On Ubuntu, it isn’t installed by default — ufw fills that slot. The difference: ufw is a static configuration script, while firewalld is a daemon that applies changes dynamically without flushing existing rules. If you manage more than one server, the second option prevents accidental lockouts.

bash
# Install and start immediately
sudo apt install firewalld -y
sudo systemctl enable --now firewalld

# Default to the restrictive 'public' zone
sudo firewall-cmd --set-default-zone=public

# Open ONLY necessary ports — never --add-service without thinking
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload

The zone model is firewalld’s reason for existing. The public zone blocks everything except what you explicitly allow. The internal zone trusts the local network. The drop zone rejects packets silently — no response at all. Best practice: every network interface gets its own zone, and public is the fallback.

Classic trap: --add-service=ssh opens port 22 to everyone. If your SSH runs on a non-standard port, use --add-port=2222/tcp. If you have a fixed management IP, use --add-rich-rule=’rule family="ipv4" source address="203.0.113.5" service name="ssh" accept’ — least privilege applies to firewalls too.

fail2ban — the automated response to brute-force attacks

fail2ban is the highest-ROI tool on this list. Its principle is dead simple: it tails logs, detects failed authentication patterns, and temporarily bans the source IP via firewalld. The numbers speak for themselves: an unprotected server receives 800 to 2,000 SSH brute-force attempts per day; with fail2ban, that number drops to zero after the first three attempts.

bash
sudo apt install fail2ban -y
sudo systemctl enable --now fail2ban

# Local configuration — NEVER edit jail.conf directly
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

The default configuration is reasonable but insufficient. In jail.local, three parameters tip the scale:

ini
[DEFAULT]
bantime = 3600           # 1 hour instead of 10 minutes
findtime = 600           # 10-minute observation window
maxretry = 3             # 3 failures = ban

[sshd]
enabled = true
mode = aggressive        # Also catches key-based attacks

The mode = aggressive parameter is critical: normal mode only detects password failures; aggressive mode also catches invalid key authentication attempts, malformed SSH negotiations, and ssh-keyscan attacks. The false-positive cost is near zero — nobody fumbles their SSH key three times in ten minutes.

auditd — the black box that speaks after the breach

If the previous layers fail, the only thing left is logging. auditd is the Linux kernel audit subsystem: it records syscalls, file accesses, and command executions at the kernel level, not the application level. An attacker who wipes /var/log/auth.log can’t touch audit logs without disabling the kernel subsystem — which itself generates a log entry.

bash
sudo apt install auditd -y
sudo systemctl enable --now auditd

Installing isn’t enough — without rules, auditd runs idle. The four minimum rules:

bash
# Watch /etc/shadow and /etc/passwd for write/attribute changes
sudo auditctl -w /etc/shadow -p wa -k identity_changes
sudo auditctl -w /etc/passwd -p wa -k identity_changes

# Log all privileged command executions
sudo auditctl -a always,exit -F arch=b64 -S execve -F euid=0 -k root_commands

# Persist rules across reboots
echo "-w /etc/shadow -p wa -k identity_changes" | sudo tee -a /etc/audit/rules.d/custom.rules
echo "-w /etc/passwd -p wa -k identity_changes" | sudo tee -a /etc/audit/rules.d/custom.rules
echo "-a always,exit -F arch=b64 -S execve -F euid=0 -k root_commands" | sudo tee -a /etc/audit/rules.d/custom.rules

Query the logs with ausearch — for example, ausearch -k root_commands --start today shows every root command executed today. The log volume is substantial (several MB per day on an active server); logrotate is essential, but the auditd package enables it by default.

AIDE — the watchdog that barks when a file changes

AIDE (Advanced Intrusion Detection Environment) is a file integrity checker. It builds a database of SHA-256 hashes for every binary, library, and configuration file in critical paths, then verifies it on a schedule. If a rootkit replaces /usr/bin/sshd or an attacker modifies /etc/pam.d/common-auth, AIDE detects it.

bash
sudo apt install aide -y

# Initialize the database (takes 2 to 5 minutes)
sudo aideinit
sudo mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db

# First verification
sudo aide.wrapper --check

AIDE’s default configuration on Ubuntu is conservative — it monitors /bin, /sbin, /usr, /etc, and /lib, while ignoring /var/log, /home, and /tmp. That’s the right compromise: monitoring user directories generates constant noise, and logs are volatile by nature.

bash
# Daily automatic verification via cron
echo "0 3 * * * root /usr/bin/aide.wrapper --check | mail -s 'AIDE Report' [email protected]" | sudo tee /etc/cron.d/aide-check

AIDE’s main weakness is the lack of real-time monitoring — an attacker can modify a file and restore it between checks. For continuous surveillance, auditd with file watch rules fills the gap.

Kernel hardening — the sysctls that close blind spots

The Linux kernel exposes hundreds of tunables through sysctl. Ten of them eliminate entire classes of attack. The file /etc/sysctl.d/99-hardening.conf:

ini
# ASLR — full address space randomization
kernel.randomize_va_space = 2

# Hide kernel pointers in /proc
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1

# Block CPU register access from user space
kernel.perf_event_paranoid = 3

# TCP SYN flood protection
net.ipv4.tcp_syncookies = 1

# Ignore ICMP redirects (route hijacking)
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0

# Block IP source routing (IP spoofing)
net.ipv4.conf.all.accept_source_route = 0

# Ignore ICMP broadcasts (Smurf amplification)
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Disable SysRq magic keys
kernel.sysrq = 0

# Restrict ptrace (process injection)
kernel.yama.ptrace_scope = 2

Apply with sudo sysctl --system. These parameters break nothing on a headless server — they remove debugging and diagnostic features that have no place in production.

Two additional parameters for servers doing routing or forwarding:

ini
# Martian packet protection (invalid source IP)
net.ipv4.conf.all.rp_filter = 1

# Disable packet forwarding between interfaces
net.ipv4.ip_forward = 0

unattended-upgrades — autopilot for security patches

The most exploited vulnerability on Linux servers isn’t a zero-day — it’s a patch published six months ago that never got applied. unattended-upgrades automates security-only updates without touching functional upgrades.

bash
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

The file /etc/apt/apt.conf.d/50unattended-upgrades deserves two adjustments:

ini
// Enable all security sources
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}";
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};

// Reboot automatically if needed (servers only)
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";

// Clean up obsolete packages
Unattended-Upgrade::Remove-Unused-Dependencies "true";

Automatic reboot is controversial but necessary — a patched but unreloaded openssl is a vulnerable openssl. The 3 AM window is the industry standard for servers without 24/7 uptime requirements.

Lynis — the auditor that finds what you forgot

Lynis is an open-source security auditing tool maintained by CISOfy. It scans your system, compares its configuration against hundreds of best practices, and produces a report with a numeric hardening index. Think of it as the chapter test — anything that comes back red is a fix waiting to happen.

bash
# Install from the official repository
sudo apt install lynis -y

# Full audit
sudo lynis audit system

# Summarize warnings and suggestions
sudo grep -E "^(warning|suggestion)" /var/log/lynis-report.dat

A score of 70+ is respectable on the first pass; 85+ is production-ready; 95+ is hardened for sensitive environments. The report is actionable — every warning and suggestion includes a description and a fix.

Lynis covers what the previous commands don’t: file permissions, passwordless user accounts, unnecessary services, login banners, password expiration, PAM configuration. It’s the safety net that catches everything you missed.

The complete script in 10 commands

Here’s the exact sequence, executable in under 5 minutes on a freshly provisioned Ubuntu 24.04 or 26.04. Every command is self-contained and idempotent — running the script again won’t break anything.

bash
#!/bin/bash
# hardening-ubuntu.sh — 10 commands, 5 minutes, one hardened server
# Tested on Ubuntu 24.04 LTS and Ubuntu 26.04 LTS
set -e

echo "[1/10] Updating packages..."
sudo apt update && sudo apt upgrade -y

echo "[2/10] AppArmor — enforcing all profiles..."
sudo aa-enforce /etc/apparmor.d/*

echo "[3/10] firewalld — public zone, SSH only..."
sudo apt install firewalld -y
sudo systemctl enable --now firewalld
sudo firewall-cmd --set-default-zone=public
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload

echo "[4/10] fail2ban — 3 failures = 1 hour ban..."
sudo apt install fail2ban -y
sudo systemctl enable --now fail2ban
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo sed -i 's/^bantime  = 10m/bantime  = 1h/' /etc/fail2ban/jail.local
sudo sed -i 's/^maxretry = 5/maxretry = 3/' /etc/fail2ban/jail.local
sudo systemctl reload fail2ban

echo "[5/10] auditd — minimum monitoring rules..."
sudo apt install auditd -y
sudo systemctl enable --now auditd
sudo auditctl -w /etc/shadow -p wa -k identity_changes
sudo auditctl -w /etc/passwd -p wa -k identity_changes
sudo auditctl -a always,exit -F arch=b64 -S execve -F euid=0 -k root_commands

echo "[6/10] AIDE — initial integrity database..."
sudo apt install aide -y
sudo aideinit -y 2>/dev/null || sudo aideinit
sudo cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db 2>/dev/null || true

echo "[7/10] Kernel hardening — sysctl..."
sudo tee /etc/sysctl.d/99-hardening.conf > /dev/null <<'SYSCTL'
kernel.randomize_va_space = 2
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
kernel.perf_event_paranoid = 3
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
kernel.sysrq = 0
kernel.yama.ptrace_scope = 2
SYSCTL
sudo sysctl --system

echo "[8/10] unattended-upgrades — automatic patches..."
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

echo "[9/10] Lynis — initial audit..."
sudo apt install lynis -y
sudo lynis audit system --quick 2>&1 | tail -20

echo "[10/10] Removing unnecessary services..."
sudo systemctl disable --now avahi-daemon cups 2>/dev/null || true
sudo systemctl mask avahi-daemon cups 2>/dev/null || true

echo ""
echo "Hardening complete."
echo "Next steps:"
echo "  - lynis audit system (full audit)"
echo "  - Review the CIS Ubuntu Benchmark: https://www.cisecurity.org/benchmark/ubuntu_linux"
echo "  - Check logs: sudo ausearch -k root_commands --start today"

Verdict

If you’re running a single VPS, this script covers 80% of the CIS Ubuntu Benchmark recommendations in 5 minutes — run it, follow up with lynis audit system, fix the warnings, and sleep better. If you’re managing a fleet of servers, the same logic gets automated with Ansible or cloud-init so every machine is born hardened from its first second of life.

The question isn’t whether these 10 commands can stop a state actor — they can’t. The question is whether your current VPS can survive an automated Shodan scan. If you have no firewall, no fail2ban, no auditd, and no AIDE, the answer is no. And the fix costs 5 minutes.

References

The cyber brief, every Tuesday

The flaws that matter and the patches to apply, in a ten-minute read.

No spam. One-click unsubscribe.
read next

On the same topic

KVM/libvirt powers AWS, GCP, and Azure — your homelab should run it too

KVM delivers 97% of bare-metal CPU performance, costs zero in licensing, and is the hypervisor behind the three largest public clouds. With libvirt, virt-manager, and Cockpit, this datacenter-grade stack is ready on any Linux server within twenty minutes.

← Back to the feed

Type at least two characters.

navigate open esc dismiss