Harden a Linux Cloud Server in 10 Minutes: SSH, UFW, Fail2ban, Updates, and Auditd

Published on April 4, 2026

Every cloud provider hands you a freshly provisioned instance with a public IP and root access. That box is being scanned within minutes of going live. The window between “first boot” and “first brute-force attempt” is shrinking — Shodan and Censys crawlers will index your open ports before you finish your coffee.

This guide is a single-pass hardening workflow you can run on Ubuntu 22.04–24.04 LTS or Debian 12 cloud instances. Every step includes a copy-paste command and a verification check. The whole sequence takes roughly ten minutes.

Before you start

  • Have a non-root user with sudo privileges ready. If your provider only gives you root, create one first:
adduser deployer
usermod -aG sudo deployer
su - deployer
  • Copy your SSH public key to the new user before disabling password auth:
# From your local machine
ssh-copy-id -i ~/.ssh/id_ed25519.pub deployer@YOUR_SERVER_IP
  • Keep a second terminal connected as root until you confirm SSH access with the new user works.

1. Lock down SSH (2 minutes)

SSH is your only door in. Make it a narrow one. Drop a hardening config into the sshd_config.d directory so your changes survive package upgrades:

sudo tee /etc/ssh/sshd_config.d/hardening.conf <<'EOF'
# Disable root login and password auth
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey

# Limit authentication window
MaxAuthTries 3
LoginGraceTime 20
MaxSessions 3
MaxStartups 3:50:10

# Disable unused features
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no

# Use only strong key exchange and ciphers
KexAlgorithms [email protected],curve25519-sha256,[email protected]
Ciphers [email protected],[email protected],[email protected]
MACs [email protected],[email protected]

# Log more detail
LogLevel VERBOSE
EOF

Validate and restart:

sudo sshd -t && sudo systemctl restart sshd

Verify: Open a new terminal and confirm you can still SSH in with your key before closing the current session.

# Should fail
ssh root@YOUR_SERVER_IP

# Should succeed
ssh deployer@YOUR_SERVER_IP

2. Configure UFW firewall (2 minutes)

UFW ships on Ubuntu but is disabled by default. Enable it with a deny-all-inbound baseline, then punch holes only for what you need:

# Reset to clean state
sudo ufw --force reset

# Set defaults: deny inbound, allow outbound
sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (change port if you moved it)
sudo ufw allow 22/tcp comment 'SSH'

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

# Enable rate limiting on SSH (blocks IPs with 6+ attempts in 30s)
sudo ufw limit 22/tcp

# Enable the firewall
sudo ufw --force enable

Verify:

sudo ufw status verbose

Expected output should show Status: active with your allowed ports listed and default deny for incoming.

For servers behind a cloud firewall (AWS Security Groups, GCP firewall rules), UFW is still worth enabling as defense-in-depth. Cloud firewalls protect the network; UFW protects the host.

3. Install and configure Fail2ban (2 minutes)

Fail2ban watches log files and bans IPs that show malicious patterns. It complements UFW’s rate limiting with smarter, pattern-based blocking:

sudo apt update && sudo apt install -y fail2ban

Create a local config (never edit jail.conf directly — it gets overwritten on updates):

sudo tee /etc/fail2ban/jail.local <<'EOF'
[DEFAULT]
# Ban for 1 hour after 3 failures within 10 minutes
bantime  = 3600
findtime = 600
maxretry = 3
banaction = ufw

# Email notifications (optional, requires mailutils)
# destemail = [email protected]
# action = %(action_mwl)s

[sshd]
enabled  = true
port     = ssh
filter   = sshd
logpath  = /var/log/auth.log
maxretry = 3
EOF
sudo systemctl enable --now fail2ban

Verify it is running and the SSH jail is active:

sudo fail2ban-client status sshd

You should see Status for the jail: sshd with filter and action details. After a few hours on a public IP, check Currently banned — you will almost certainly see entries.

4. Enable automatic security updates (2 minutes)

Unpatched software is the number one cause of server compromises. Automatic security updates close the gap between “advisory published” and “patch applied” from days to hours:

sudo apt install -y unattended-upgrades apt-listchanges

Enable and configure:

sudo tee /etc/apt/apt.conf.d/50unattended-upgrades <<'EOF'
Unattended-Upgrade::Allowed-Origins {
    "${distro_id}:${distro_codename}-security";
    "${distro_id}ESMApps:${distro_codename}-apps-security";
    "${distro_id}ESM:${distro_codename}-infra-security";
};

// Remove unused kernel packages and dependencies
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";

// Auto-reboot at 3 AM if a kernel update requires it
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";

// Log to syslog
Unattended-Upgrade::SyslogEnable "true";
EOF
sudo tee /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
EOF

Verify the configuration:

sudo unattended-upgrades --dry-run --debug 2>&1 | head -20

For Ubuntu Pro subscribers, Livepatch can apply kernel fixes without rebooting — useful for servers where uptime matters:

sudo pro attach YOUR_TOKEN
sudo pro enable livepatch

5. Set up basic audit logging with auditd (2 minutes)

The Linux Audit Framework logs security-relevant system calls. When something goes wrong, audit logs tell you exactly what happened and when. Install and configure a baseline ruleset:

sudo apt install -y auditd audispd-plugins

Add baseline rules that cover the most critical events:

sudo tee /etc/audit/rules.d/hardening.rules <<'EOF'
# Log all authentication events
-w /etc/pam.d/ -p wa -k pam_changes
-w /etc/shadow -p wa -k shadow_changes
-w /var/log/auth.log -p r -k auth_log_read

# Log sudo usage
-w /etc/sudoers -p wa -k sudoers_changes
-w /etc/sudoers.d/ -p wa -k sudoers_changes
-w /var/log/sudo.log -p wa -k sudo_log

# Log SSH config changes
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /etc/ssh/sshd_config.d/ -p wa -k sshd_config

# Log user and group changes
-w /etc/passwd -p wa -k passwd_changes
-w /etc/group -p wa -k group_changes

# Log firewall changes
-w /etc/ufw/ -p wa -k firewall_changes

# Log cron changes
-w /etc/crontab -p wa -k cron_changes
-w /etc/cron.d/ -p wa -k cron_changes
-w /var/spool/cron/ -p wa -k cron_changes

# Log kernel module loading
-w /sbin/insmod -p x -k kernel_modules
-w /sbin/modprobe -p x -k kernel_modules
-w /sbin/rmmod -p x -k kernel_modules

# Make the configuration immutable (requires reboot to change)
-e 2
EOF
sudo systemctl enable --now auditd
sudo augenrules --load

Verify rules are loaded:

sudo auditctl -l

Search audit logs with ausearch:

# Recent authentication events
sudo ausearch -k auth_log_read -ts recent

# Who touched sudoers?
sudo ausearch -k sudoers_changes -ts today

# Summary report
sudo aureport --summary

6. Quick wins: kernel and network hardening

Apply sysctl parameters that harden the network stack and restrict kernel information leaks:

sudo tee /etc/sysctl.d/99-hardening.conf <<'EOF'
# Disable IP forwarding (unless this is a router/VPN)
net.ipv4.ip_forward = 0

# Ignore ICMP redirects (prevent MITM routing attacks)
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0

# Don't send ICMP redirects
net.ipv4.conf.all.send_redirects = 0

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

# Log suspicious packets (martians)
net.ipv4.conf.all.log_martians = 1
net.ipv4.conf.default.log_martians = 1

# Ignore ICMP broadcast requests
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Restrict kernel pointer leaks
kernel.kptr_restrict = 2

# Restrict dmesg to root
kernel.dmesg_restrict = 1

# Restrict kernel profiling
kernel.perf_event_paranoid = 3

# Harden BPF JIT
net.core.bpf_jit_harden = 2

# Restrict ptrace to parent processes
kernel.yama.ptrace_scope = 1
EOF

sudo sysctl --system

Post-hardening verification checklist

Run these checks after completing the steps above to confirm everything is in place:

# 1. SSH: root login disabled, password auth off
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication'

# 2. Firewall: active with deny-incoming default
sudo ufw status verbose | head -6

# 3. Fail2ban: sshd jail active
sudo fail2ban-client status sshd

# 4. Auto-updates: enabled
apt-config dump | grep -i unattended

# 5. Auditd: rules loaded
sudo auditctl -l | wc -l

# 6. Listening ports: only expected services
sudo ss -tlnp

# 7. No passwordless sudo for non-admin users
sudo grep -r 'NOPASSWD' /etc/sudoers /etc/sudoers.d/ 2>/dev/null

# 8. Sysctl hardening applied
sudo sysctl net.ipv4.tcp_syncookies kernel.kptr_restrict kernel.dmesg_restrict

What this does not cover

This checklist gets a cloud instance from “default” to “hardened baseline” in ten minutes. For production workloads, you will also want:

  • AppArmor profiles for your application services (Ubuntu enables AppArmor by default since 24.04, but custom profiles need tuning)
  • Centralized log shipping to a SIEM or log aggregator — local logs are useless if an attacker wipes them
  • File integrity monitoring with AIDE or OSSEC to detect unauthorized changes
  • Disk encryption with LUKS for data-at-rest protection
  • Network segmentation with VLANs or VPC subnets to limit lateral movement
  • CIS Benchmark scanning with oscap or Lynis for comprehensive compliance checks

Run a quick Lynis scan to see where you stand after applying this checklist:

sudo apt install -y lynis
sudo lynis audit system --quick

Hardening is not a one-time event. Schedule a monthly review of sudo aureport --summary, check fail2ban-client status, and re-run Lynis. The ten minutes you spend now saves hours of incident response later.