Every production server starts with the vendor's defaults. Those defaults prioritize ease of setup over security, and attackers know it. I've seen compromised boxes where a single hardening step would have blocked the entry point entirely.
This checklist walks through twelve practical measures that close the most exploited gaps on Linux servers. You can apply them in order on a fresh VPS or audit an existing production box. Each step reduces your attack surface without breaking legitimate workflows.
1. Disable root SSH login and password authentication
Root SSH access is the highest-value target for brute-force bots. When password authentication is enabled, those bots can try thousands of combinations per hour.
Open /etc/ssh/sshd_config and set these directives:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
Restart SSH:
sudo systemctl restart sshd
Before you close the session, test login from a second terminal using your SSH key. If you lock yourself out, you'll need console access through your hosting panel.
Create a non-root user with sudo privileges for daily administration. This way, you audit individual actions and rotate credentials without touching the root account.
2. Change the default SSH port
Port 22 receives constant automated traffic. Changing the port won't stop a determined attacker, but it eliminates script-kiddie noise and keeps your auth logs clean.
In /etc/ssh/sshd_config, set:
Port 2289
Pick any unused port above 1024. Update your firewall rules before restarting SSH, or you'll lock yourself out:
sudo ufw allow 2289/tcp
sudo ufw delete allow 22/tcp
sudo systemctl restart sshd
Reconnect using ssh -p 2289 user@server. This single change typically drops failed auth attempts by ninety percent or more in my experience.
3. Configure a stateful firewall with default-deny
A stateful firewall blocks all inbound traffic except the ports you explicitly allow. Most compromises I've investigated involved an exposed service the owner forgot was listening.
On Ubuntu and Debian, ufw is the simplest option:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2289/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
For CentOS or RHEL, use firewalld:
sudo firewall-cmd --set-default-zone=drop
sudo firewall-cmd --zone=public --add-service=ssh --permanent
sudo firewall-cmd --zone=public --add-service=http --permanent
sudo firewall-cmd --zone=public --add-service=https --permanent
sudo firewall-cmd --reload
List all listening ports with ss -tuln and compare against your firewall rules. Close anything you don't recognize.
4. Install and configure Fail2Ban
Fail2Ban monitors log files and temporarily bans IPs that show malicious behavior patterns—repeated auth failures, web-shell probes, or exploit attempts.
Install it:
sudo apt install fail2ban # Debian/Ubuntu
sudo yum install fail2ban # CentOS/RHEL
Create /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = 2289
Restart the service:
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Check active jails and banned IPs:
sudo fail2ban-client status sshd
In support tickets I handled, brute-force attempts typically stopped within hours once Fail2Ban was active. Attackers move on to softer targets.
5. Keep the system and all packages updated
Unpatched vulnerabilities are the second most common entry point after weak credentials. Set up automatic security updates so critical patches apply without manual intervention.
On Debian and Ubuntu, install unattended-upgrades:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Edit /etc/apt/apt.conf.d/50unattended-upgrades to enable automatic reboots if needed:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
For CentOS and RHEL, enable dnf-automatic:
sudo yum install dnf-automatic
sudo systemctl enable --now dnf-automatic.timer
Schedule a monthly review of installed packages. Remove anything you don't actively use.
6. Harden kernel parameters with sysctl
The Linux kernel ships with defaults that favor compatibility. A few sysctl tweaks can block common network-based attacks.
Add these lines to /etc/sysctl.conf:
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.default.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.tcp_syncookies = 1
kernel.dmesg_restrict = 1
Apply them:
sudo sysctl -p
These settings disable IP source routing, ICMP redirects, and broadcast pings, and enable SYN cookies to mitigate SYN flood attacks.
7. Restrict user privileges and use sudo
Never run services as root. Each daemon should run under its own unprivileged user account. If the service is compromised, the attacker inherits only that user's limited permissions.
For administrative tasks, grant specific sudo privileges instead of adding users to the wheel or sudo group with full access.
Edit /etc/sudoers with visudo:
username ALL=(ALL) /usr/bin/systemctl restart nginx, /usr/bin/systemctl reload nginx
This allows the user to restart Nginx without granting broader root access.
Audit your user list regularly:
cat /etc/passwd | grep -v nologin | grep -v false
Remove or lock any accounts that shouldn't have shell access:
sudo usermod -s /usr/sbin/nologin username
8. Enable and configure auditd
The audit daemon logs detailed system calls, file access, and privilege escalation events. When something goes wrong, these logs let you reconstruct exactly what happened.
Install auditd:
sudo apt install auditd # Debian/Ubuntu
sudo yum install audit # CentOS/RHEL
Add rules to /etc/audit/rules.d/audit.rules:
-w /etc/passwd -p wa -k passwd_changes
-w /etc/shadow -p wa -k shadow_changes
-w /etc/sudoers -p wa -k sudoers_changes
-w /var/log/auth.log -p wa -k auth_log_changes
Reload the rules:
sudo augenrules --load
Search audit logs by key:
sudo ausearch -k passwd_changes
I've traced unauthorized changes back to specific sessions using these logs more times than I can count.
9. Disable unnecessary services and remove unused packages
Every running service is a potential entry point. If you're not using a daemon, turn it off.
List active services:
sudo systemctl list-unit-files --type=service --state=enabled
Disable anything you don't recognize or need:
sudo systemctl disable servicename
sudo systemctl stop servicename
Common candidates for removal: Bluetooth, print services, Avahi, and GUI components on a headless server.
Remove orphaned packages:
sudo apt autoremove # Debian/Ubuntu
sudo yum autoremove # CentOS/RHEL
Fewerpackages mean fewer vulnerabilities to patch.
10. Secure shared memory and temporary directories
Shared memory at /run/shm can be exploited to run malicious code. Mount it with noexec to prevent execution.
Add this line to /etc/fstab:
tmpfs /run/shm tmpfs defaults,noexec,nodev,nosuid 0 0
Remount:
sudo mount -o remount /run/shm
Similarly, lock down /tmp:
tmpfs /tmp tmpfs defaults,noexec,nodev,nosuid,size=2G 0 0
Some applications expect to execute scripts from /tmp. Test thoroughly after applying these changes.
11. Set up centralized logging or log forwarding
Local logs disappear when an attacker wipes the disk or gains root. Forwarding logs to a remote syslog server or a service like Papertrail preserves the evidence.
Configure rsyslog to forward everything:
Edit /etc/rsyslog.conf and add:
*.* @@remote-log-server:514
Restart rsyslog:
sudo systemctl restart rsyslog
If you manage multiple servers, centralized logging also simplifies monitoring and correlation across systems.
12. Enable mandatory access control with AppArmor or SELinux
MAC frameworks enforce policies that limit what processes can do, even if they run as root. AppArmor is easier to start with; SELinux offers finer-grained control.
On Ubuntu and Debian, AppArmor is usually active by default. Check status:
sudo aa-status
Ensure profiles are in enforce mode, not complain mode. To enforce a profile:
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx
On CentOS and RHEL, SELinux is the standard. Verify it's enforcing:
getenforce
If it returns Permissive or Disabled, edit /etc/selinux/config:
SELINUX=enforcing
Reboot to apply.
When I see a compromised web server, SELinux or AppArmor in enforce mode usually contained the breach to a single process instead of letting it spread.
How often should you repeat this checklist?
Run through the full checklist when you provision a new server. After that, the work shifts to maintenance: reviewing firewall rules monthly, auditing users quarterly, and keeping packages updated continuously.
Automation helps. Tools like Ansible, Puppet, or simple shell scripts let you apply the same hardening baseline to every new instance without manual repetition. I keep a baseline playbook for common server roles and adjust it as new threats emerge.
Some steps, like kernel parameters and MAC profiles, rarely change once set. Others, like user accounts and firewall rules, need regular attention because your infrastructure evolves.
What if my application breaks after hardening?
Start by checking logs—/var/log/syslog, /var/log/audit/audit.log, and application-specific logs usually point to the culprit. Common issues include firewall rules blocking a required port, SELinux denying a file access, or noexec on /tmp stopping a script.
Rollback the most recent change and test incrementally. You can temporarily set SELinux to permissive mode or disable a firewall rule to isolate the problem, but don't leave it that way in production.
Do I need all twelve steps on every server?
The first six steps apply universally: SSH lockdown, firewall, automatic updates, Fail2Ban, kernel hardening, and sudo policies. Steps seven through twelve depend on your threat model and compliance requirements.
A public-facing web server benefits from everything. An internal database server behind a VPN might skip Fail2Ban and focus on access control and logging instead.
Can I harden a cPanel or Plesk server the same way?
Most of these steps work on managed control panels, but read the vendor docs first. cPanel manages SSH configuration through WHM, and changing /etc/ssh/sshd_config directly may get overwritten during updates. Use the built-in security center instead.
Plesk and cPanel both include firewall interfaces that wrap iptables or firewalld. Use those to avoid conflicts with the panel's own rules.
How do I test that hardening actually worked?
Run a port scan from an external IP using nmap to confirm only the intended ports are open. Attempt SSH login with a wrong password to verify Fail2Ban triggers. Check sudo systemctl status for all services to confirm unnecessary ones are stopped.
Schedule quarterly penetration tests or vulnerability scans if the server handles sensitive data. Many small issues only show up under active probing.
Where to tighten first
If you can only do three things today, start with these: disable root SSH login and enforce key authentication, configure a default-deny firewall, and enable automatic security updates. Those three steps close the majority of opportunistic attacks without requiring deep system knowledge.
Once you have breathing room, work through the rest of the checklist in order. Each additional layer makes the next breach harder, and attackers usually move on when a target requires more effort than it's worth.
