A fresh Linux server ships with conveniences that attackers love. Root login over SSH. Password authentication. Services you'll never use listening on ports you've never checked. I've restored too many compromised servers where the initial entry point was something trivial—a default config line nobody changed.
This checklist walks you through twelve hardening steps that close the easy doors. You won't need to be a security specialist to apply them, and you won't break a working server if you follow the order. Each step takes five to thirty minutes depending on your setup.
1. Disable root SSH login and enforce key-based auth
Password authentication over SSH is the most common attack vector I see in support tickets. Bots hammer port 22 with credential lists around the clock.
Edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
Before you restart sshd, verify that:
- You have a non-root sudo user.
- That user's ~/.ssh/authorized_keys contains your public key.
- You can log in as that user with your key in a separate terminal session.
Only then run systemctl restart sshd. Test immediately. If you lock yourself out, most VPS control panels offer a console or rescue mode.
2. Change the default SSH port
This is security through obscurity, but it cuts brute-force noise by ninety percent. Pick a port above 1024 and below 65535 that doesn't conflict with anything else you run.
In /etc/ssh/sshd_config:
Port 2849
Restart sshd, then update your firewall rules (step 3) to allow the new port before you close your current session. Connect on the new port in a second terminal to confirm it works.
3. Configure a firewall with default-deny
Use iptables with iptables-persistent, nftables, or ufw depending on your distribution. I'll show ufw because it's the simplest for this purpose.
apt install ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 2849/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw enable
Add only the ports your services actually need. A web server needs 80 and 443. A mail server needs 25, 465, 587, 993, and maybe 995. Everything else stays closed.
Check active rules with ufw status numbered. If you use a control panel like cPanel or Plesk, check their documentation—they may manage firewall rules themselves and conflict with manual ufw changes.
4. Install and configure fail2ban
Fail2ban watches logs for repeated failed login attempts and temporarily bans the source IP. It works for SSH, HTTP auth, mail servers, and dozens of other services.
apt install fail2ban
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = 2849
Restart with systemctl restart fail2ban. Check active jails with fail2ban-client status and banned IPs with fail2ban-client status sshd. You'll see bans accumulate within hours if your server is internet-facing.
5. Keep packages updated automatically
Unpatched systems get owned. You need a process that applies security updates without waiting for you to remember.
On Debian and Ubuntu, install unattended-upgrades:
apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Edit /etc/apt/apt.conf.d/50unattended-upgrades to enable automatic security updates and configure email notifications if something breaks. For RHEL-based systems, enable dnf-automatic instead.
Check /var/log/unattended-upgrades/ periodically to confirm updates are running. A stale log means the service stopped and you didn't notice.
6. Remove or disable unnecessary services
Every running service is a potential attack surface. List what's listening:
ss -tulnp
You'll see everything bound to a port. For each one, ask: do I actually use this? If you don't recognize a service, check its systemd unit with systemctl status <service-name>.
Disable what you don't need:
systemctl disable --now <service-name>
Common candidates: rpcbind, avahi-daemon, cups (unless you're printing from this server, which you aren't). On a typical VPS, you should see SSH, your web server, maybe a database, and not much else.
7. Restrict user permissions and implement least privilege
Don't run applications as root. Create dedicated service accounts with no login shell and no home directory:
useradd -r -s /usr/sbin/nologin -d /nonexistent appuser
Set ownership so the app can read its files but not write to binaries:
chown -R appuser:appuser /var/www/app
chmod -R 750 /var/www/app
For sudo access, use /etc/sudoers.d/ drop-in files instead of editing sudoers directly. Grant only the specific commands each user needs, not blanket ALL=(ALL) ALL.
8. Enable and configure SELinux or AppArmor
Mandatory access control (MAC) systems enforce policies even if file permissions are misconfigured. SELinux is standard on RHEL-based distributions; AppArmor ships with Debian and Ubuntu.
On RHEL/CentOS/AlmaLinux, check the current mode:
getenforce
If it says Disabled, edit /etc/selinux/config and set SELINUX=enforcing, then reboot. Expect some services to break initially—check /var/log/audit/audit.log for denials and write policies or adjust booleans as needed.
On Ubuntu, AppArmor is usually active. Check with:
aa-status
Most distributions ship profiles for common daemons. If you install something custom, you may need to write a profile or set the service to complain mode while you tune it.
This is the step that takes the most iteration. Don't skip it.
9. Configure kernel hardening parameters
Edit /etc/sysctl.conf or create /etc/sysctl.d/99-hardening.conf:
# IP forwarding off unless you're routing
net.ipv4.ip_forward = 0
# Ignore ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Ignore source-routed packets
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# SYN flood protection
net.ipv4.tcp_syncookies = 1
# Ignore ICMP ping
net.ipv4.icmp_echo_ignore_all = 1
# Log suspicious packets
net.ipv4.conf.all.log_martians = 1
Apply with sysctl -p. Reboot to confirm the settings persist.
10. Enable audit logging with auditd
The audit daemon records security-relevant events: who ran sudo, which files were accessed, failed login attempts, permission changes.
apt install auditd audispd-plugins
systemctl enable --now auditd
Add rules in /etc/audit/rules.d/audit.rules. Track sudo usage:
-w /usr/bin/sudo -p x -k sudo_exec
Watch sensitive files:
-w /etc/passwd -p wa -k passwd_changes
-w /etc/shadow -p wa -k shadow_changes
-w /etc/ssh/sshd_config -p wa -k sshd_config
Reload rules with augenrules --load. Search logs with ausearch -k <key> or dump everything with aureport.
The logs live in /var/log/audit/audit.log. Rotate them or they'll fill your disk.
11. Set up file integrity monitoring
File integrity monitoring (FIM) detects unauthorized changes to system binaries, configs, and application code. AIDE and Tripwire are the standard tools.
Install AIDE:
apt install aide
aideinit
This creates a baseline database of file hashes. It takes several minutes on a large filesystem. When it finishes, copy the database:
cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
Run checks with aide --check. Schedule it in cron:
0 3 * * * /usr/bin/aide --check | mail -s "AIDE report" [email protected]
Every time you update packages or change a config intentionally, regenerate the baseline with aideinit and copy the new database. Otherwise you'll get false positives forever.
12. Deploy intrusion detection with OSSEC or Wazuh
Host-based intrusion detection systems (HIDS) correlate logs, detect rootkits, and alert on policy violations. OSSEC is lightweight and easy to deploy. Wazuh is a maintained fork with a better web interface.
Install OSSEC in local mode:
wget https://github.com/ossec/ossec-hids/archive/refs/tags/3.7.0.tar.gz
tar -xzf 3.7.0.tar.gz
cd ossec-hids-3.7.0
./install.sh
Choose "local" installation when prompted. Edit /var/ossec/etc/ossec.conf to enable rootkit detection, log monitoring, and email alerts. Start with:
/var/ossec/bin/ossec-control start
Check alerts in /var/ossec/logs/alerts/alerts.log. You'll get noise at first—tune rules in /var/ossec/rules/local_rules.xml to reduce false positives.
For a multi-server setup, run Wazuh with a manager node and agents on each host. The manager aggregates alerts and provides a web dashboard.
What about cloud-specific hardening?
If you're on AWS, Azure, or GCP, apply these steps first. Then layer on platform-specific controls: security groups instead of relying only on host firewalls, IAM roles with least privilege, instance metadata service hardening (IMDSv2 on AWS), and disk encryption.
The principles don't change. Defense in depth.
Start with SSH and firewall, then layer up
You don't have to implement all twelve steps in one afternoon. Start with SSH hardening and the firewall—those two changes block the majority of automated attacks. Add fail2ban next, then work through the rest over a few weeks.
Document what you enable and why. Six months from now, when a service breaks and you need to troubleshoot, you'll want to know which hardening rule might be the cause. A checklist you actually followed beats a perfect security policy that stayed in a Google Doc.
