Production servers face constant probing from automated scanners, brute-force bots, and opportunistic attackers. A single misconfigured SSH port or outdated kernel can turn a healthy VPS into a spam relay or cryptominer within hours. The hardening steps below cover the foundational controls I apply to every server before it touches real traffic.
1. Lock Down SSH Access
SSH is the first target. Default port 22 gets hammered by bots scanning entire IPv4 ranges.
Disable password authentication
Password-based auth is the weakest link. Even strong passwords fall to credential stuffing and brute-force attacks over time. Switch to public-key authentication and turn off password login entirely.
Edit /etc/ssh/sshd_config:
PasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin prohibit-password
That last line blocks root login via password but still allows key-based root access. If you want to disable root SSH entirely, set it to no instead.
Restart the service:
sudo systemctl restart sshd
Before you close your current session, open a second terminal and test key-based login. If it fails, you still have the original session to fix the config.
Change the default port
Moving SSH off port 22 won't stop a determined attacker, but it eliminates 99% of automated scan traffic. Pick a high port above 1024 that isn't used by other services.
In /etc/ssh/sshd_config:
Port 2849
Restart SSH, then update your firewall rules to allow the new port and block 22. Test the new port before closing your existing session.
Restrict by IP when possible
If your admin team connects from known IP ranges, whitelist them in the firewall and drop everything else. That's not always practical for remote teams or dynamic IPs, but when you can do it, do it.
Using UFW:
sudo ufw allow from 203.0.113.0/24 to any port 2849
sudo ufw deny 2849
Order matters. The allow rule for your subnet must come before the general deny.
2. Configure a Host-Based Firewall
Cloud providers often give you security groups or network firewalls, but a host-based firewall adds a second layer. If someone compromises a service running as root, network-level rules may not help.
UFW (Uncomplicated Firewall) is the easiest starting point on Ubuntu and Debian.
Set default deny
sudo ufw default deny incoming
sudo ufw default allow outgoing
That blocks all inbound traffic by default. Now explicitly allow only what you need.
Allow necessary services
For a typical web server:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 2849/tcp # your SSH port
If you run a mail server, allow ports 25, 587, 993, etc. Be specific. Don't open port ranges unless you have a clear reason.
Enable the firewall:
sudo ufw enable
Check the status:
sudo ufw status verbose
Review rules regularly
Every time you decommission a service, close its port. Old rules accumulate over months and create unnecessary attack surface. I schedule a quarterly firewall audit on every production server.
3. Automate Patch Management
Unpatched systems are the number one entry point in support incidents I've handled. A critical kernel vulnerability announced on Monday will be exploited by Wednesday.
Enable automatic security updates
On Debian and Ubuntu, install unattended-upgrades:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
That enables automatic installation of security patches. The system will apply updates from the security repository without manual intervention.
Edit /etc/apt/apt.conf.d/50unattended-upgrades to control what gets updated:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
};
On RHEL and CentOS, use dnf-automatic:
sudo dnf install dnf-automatic
sudo systemctl enable --now dnf-automatic.timer
Configure /etc/dnf/automatic.conf to apply security updates:
[commands]
apply_updates = yes
Monitor update logs
Automatic updates can break things. Application compatibility issues, kernel panics, and service failures happen. Set up monitoring so you know when an update runs and whether it caused problems.
Check logs after each update cycle:
sudo journalctl -u unattended-upgrades
For critical production systems, test updates in staging first, then apply them manually during maintenance windows. Automation is a tradeoff between speed and control.
4. Harden Kernel Parameters
The kernel exposes dozens of tunables that affect network security. A few sysctl tweaks close common attack vectors.
Edit /etc/sysctl.conf or create a new file in /etc/sysctl.d/:
# Prevent IP spoofing
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Disable ICMP redirect acceptance
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
# Disable source packet routing
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# Enable SYN flood protection
net.ipv4.tcp_syncookies = 1
# Log suspicious packets
net.ipv4.conf.all.log_martians = 1
Apply the changes:
sudo sysctl -p
These settings won't stop sophisticated attacks, but they block trivial spoofing and redirect exploits that still work against default configurations.
5. Implement Fail2Ban for Brute-Force Protection
Even with key-based SSH and a non-standard port, some services (like webmail or cPanel) still need password auth. Fail2Ban watches logs and bans IPs after repeated failed attempts.
Install it:
sudo apt install fail2ban
Create a local config file at /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = 2849
logpath = /var/log/auth.log
That bans an IP for one hour after five failed SSH attempts within ten minutes. Adjust the thresholds based on your tolerance for false positives.
Restart Fail2Ban:
sudo systemctl restart fail2ban
Check active bans:
sudo fail2ban-client status sshd
Add jails for other services
Fail2Ban ships with pre-built filters for Apache, Nginx, Postfix, and Dovecot. Enable them in jail.local:
[nginx-http-auth]
enabled = true
[postfix-sasl]
enabled = true
Review your logs first to confirm Fail2Ban can parse them. Custom log formats sometimes need custom regex filters.
6. Adopt Zero-Trust Principles
Zero-trust means never assume a service or user is safe just because it's inside your network. Verify every connection.
Require TLS everywhere
Unencrypted protocols (HTTP, FTP, plain SMTP, Telnet) leak credentials and session tokens. Replace them with encrypted alternatives. Use HTTPS for all web traffic, SFTP or SCP instead of FTP, and SMTP with STARTTLS for email.
If you still need legacy protocols for specific clients, isolate them on a separate VLAN or VM and restrict access by IP.
Separate privileges
Don't run application code as root. Don't give sudo access to service accounts. Create dedicated users with the minimum permissions needed for each service.
For a web application:
sudo useradd -r -s /bin/false webapp
sudo chown -R webapp:webapp /var/www/myapp
The -r flag creates a system user with no login shell. The application runs as webapp, so even if an attacker compromises it, they can't escalate to root without exploiting a second vulnerability.
Audit access regularly
Who has SSH keys installed? Who has sudo privileges? Which service accounts exist? Every few months, audit user lists and revoke access that's no longer needed.
List all users with sudo:
grep -Po '^sudo.+:\K.*$' /etc/group
List all SSH authorized keys:
find /home -name authorized_keys -exec cat {} \;
Remove keys for former employees immediately.
7. Enable and Monitor Audit Logs
You can't secure what you can't see. Comprehensive logging is the only way to detect intrusions, debug incidents, and prove compliance.
Install auditd
The Linux audit framework logs system calls, file access, and command execution.
sudo apt install auditd
sudo systemctl enable auditd
Create audit rules in /etc/audit/rules.d/audit.rules. Watch critical files:
-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
The -w flag watches a file, -p wa logs writes and attribute changes, and -k assigns a key for filtering.
Reload the rules:
sudo augenrules --load
Centralize logs
Local logs disappear if an attacker wipes the disk. Send logs to a remote syslog server or a managed logging service. That preserves evidence even if the server is compromised.
Configure rsyslog to forward to a remote host. Edit /etc/rsyslog.conf:
*.* @@remote-log-server:514
The @@ prefix uses TCP instead of UDP for reliability.
Set up alerting
Parsing logs manually is unrealistic. Use a tool like Logwatch for daily summaries or integrate with a SIEM for real-time alerting on suspicious patterns (repeated failed logins, unexpected privilege escalations, changes to system binaries).
What should you check first?
If you're inheriting an unknown server or haven't reviewed security in a while, start here:
- Check SSH config for password auth and root login.
- Review firewall rules and close unnecessary ports.
- Verify automatic security updates are enabled.
- Audit user accounts and remove stale access.
- Confirm critical services use TLS.
Hardening isn't a one-time task. Threat models evolve, new vulnerabilities emerge, and configurations drift. Schedule quarterly reviews to keep production systems locked down. The steps above won't stop nation-state actors, but they'll block the vast majority of opportunistic attacks that turn neglected servers into spam relays and botnets.
Before you walk away
Server hardening isn't about achieving perfect security. That doesn't exist. It's about raising the cost of compromise high enough that attackers move on to easier targets. The seven steps above do exactly that.
Start with SSH and firewall rules today. Those two changes alone block the majority of automated attacks. Then layer in automated patching, audit logging, and zero-trust controls over the next few weeks. Each step compounds the previous one.
