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
sudoprivileges 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";
EOFsudo 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
oscapor 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.
This work is licensed under a Creative Commons Attribution-NonCommercial 2.5 License .