Servers facing the public internet last minutes without basic hardening. Botnets scan ports constantly, brute-force attacks run around the clock, and zero-day exploits target unpatched systems before most admins finish their coffee.
This checklist walks through nine hardening steps I've used across hundreds of production servers—Linux and Windows—to cut off the easiest attack paths. Every step is reversible if something breaks, but each one closes a door attackers routinely walk through.
1. Disable Root SSH and Enforce Key-Based Authentication
Password authentication over SSH is the single biggest liability on most servers. Brute-force bots hit port 22 thousands of times per hour trying common usernames and passwords.
First, create a non-root user with sudo privileges:
adduser deployuser
usermod -aG sudo deployuser
Generate an SSH key pair on your local machine if you haven't already:
ssh-keygen -t ed25519 -C "[email protected]"
Copy the public key to the server:
ssh-copy-id deployuser@your_server_ip
Test logging in as the new user with the key. Once confirmed, edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
Restart SSH:
systemctl restart sshd
This change alone drops failed login attempts by over 99% in my experience. Attackers move on to softer targets.
For Windows Server, disable password authentication in OpenSSH config at C:\ProgramData\ssh\sshd_config with the same directives, then restart the OpenSSH service.
2. Configure a Host-Based Firewall and Close Unnecessary Ports
Every open port is a potential entry point.
On Linux, use ufw (Ubuntu/Debian) or firewalld (RHEL/CentOS). Start by listing what's currently listening:
ss -tulnp
For a typical web server, you need ports 22 (SSH), 80 (HTTP), and 443 (HTTPS). Close everything else:
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Check the result:
ufw status verbose
If you run mail services, database servers, or custom applications, add only the specific ports those services require. Avoid opening ranges unless you've documented why each port is needed.
On Windows Server, use the built-in firewall. Open PowerShell as Administrator:
New-NetFirewallRule -DisplayName "Allow HTTP" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow
New-NetFirewallRule -DisplayName "Allow HTTPS" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow
Set the default inbound policy to Block:
Set-NetFirewallProfile -Profile Domain,Public,Private -DefaultInboundAction Block
3. Implement Automated Security Patching
Unpatched systems are the easiest targets. Most breaches exploit vulnerabilities that had patches available for weeks or months.
On Debian/Ubuntu, enable unattended-upgrades:
apt install unattended-upgrades
dpkg-reconfigure --priority=low unattended-upgrades
Edit /etc/apt/apt.conf.d/50unattended-upgrades to enable automatic reboots for kernel updates:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
On RHEL/CentOS, use dnf-automatic:
dnf install dnf-automatic
systemctl enable --now dnf-automatic.timer
Configure /etc/dnf/automatic.conf to apply updates automatically:
apply_updates = yes
Windows Server supports automatic updates through Group Policy or the Settings app. Set active hours to avoid disruption, and configure automatic restarts for outside business hours.
I recommend staging updates in a test environment first if you run critical applications, but automatic security patches are non-negotiable for smaller deployments.
4. Harden SSH Configuration Beyond Key-Only Auth
Disabling passwords is step one. Additional SSH hardening closes more subtle attack vectors.
Edit /etc/ssh/sshd_config again:
Port 2222
PermitEmptyPasswords no
MaxAuthTries 3
MaxSessions 2
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers deployuser
Protocol 2
Changing the default port to something non-standard (like 2222) drops automated scans to nearly zero. It's security through obscurity, but it works against low-effort attacks. Remember to update your firewall rules:
ufw allow 2222/tcp
ufw delete allow 22/tcp
MaxAuthTries 3 locks out after three failed attempts, and AllowUsers restricts SSH access to specific accounts.
Restart SSH and test from a new terminal before closing your current session. If something breaks, you'll still have access through your existing connection.
5. Set Up Fail2Ban or Similar Intrusion Prevention
Fail2Ban monitors log files for repeated failed login attempts and temporarily bans the offending IP addresses.
Install it:
apt install fail2ban
Create a local config file at /etc/fail2ban/jail.local:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
[sshd]
enabled = true
port = 2222
logpath = /var/log/auth.log
Start the service:
systemctl enable --now fail2ban
Check active jails:
fail2ban-client status sshd
You'll see IPs that have been banned after exceeding the retry limit. Fail2Ban works for other services too—HTTP auth, FTP, mail servers—by monitoring their respective logs.
On Windows, consider using tools like IPBan or Windows Firewall with Advanced Security rules that respond to event logs.
6. Enforce Strong Password Policies and Disable Unused Accounts
Even with SSH keys, local accounts still need strong passwords in case someone gains console access or uses privilege escalation.
On Linux, install and configure libpam-pwquality:
apt install libpam-pwquality
Edit /etc/security/pwquality.conf:
minlen = 14
minclass = 3
maxrepeat = 2
This enforces 14-character minimum passwords with at least three character classes (upper, lower, digit, special).
List all user accounts:
cat /etc/passwd
Lock or delete accounts that shouldn't log in:
usermod -L olduser
# or
userdel -r olduser
For Windows Server, configure password policies through Group Policy Editor (gpedit.msc) under Computer Configuration > Windows Settings > Security Settings > Account Policies > Password Policy. Set minimum length to 14 characters, enable complexity requirements, and set maximum password age.
Disable or remove unused local accounts through Computer Management > Local Users and Groups.
7. Enable and Configure SELinux or AppArmor
Mandatory access control systems limit what processes can do, even if an attacker compromises one.
SELinux is standard on RHEL/CentOS. Check its status:
getenforce
If it returns "Disabled" or "Permissive," enable it:
setenforce 1
Make it persistent by editing /etc/selinux/config:
SELINUX=enforcing
AppArmor is default on Ubuntu. Check loaded profiles:
aa-status
Most distributions ship with profiles for common services like Apache, Nginx, and MySQL. If a service breaks after enabling enforcement, check logs:
autail -f /var/log/audit/audit.log | grep denied
You'll see what got blocked. You can then adjust the policy or create exceptions, but avoid disabling enforcement entirely.
Windows doesn't have a direct equivalent, but Application Control policies and Windows Defender Application Guard provide similar protections.
8. Disable Unnecessary Services and Remove Unused Software
Every installed package is potential attack surface.
List running services on Linux:
systemctl list-units --type=service --state=running
Disable anything you don't need:
systemctl disable --now cups.service
Common candidates for disabling: print services, Bluetooth, Avahi, remote desktop services.
List installed packages and remove unused ones:
apt list --installed
apt purge package-name
On Windows Server, use the Server Manager to remove roles and features you don't use. Open PowerShell and list installed features:
Get-WindowsFeature | Where-Object {$_.InstallState -eq 'Installed'}
Remove unneeded ones:
Uninstall-WindowsFeature -Name Feature-Name
Fewer services mean fewer things to patch and fewer potential entry points.
9. Configure Kernel-Level Protections and System Limits
Kernel parameters control low-level behavior that affects security.
On Linux, edit /etc/sysctl.conf and add:
# IP Forwarding (disable if not 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
# Enable TCP SYN cookies (protection against SYN floods)
net.ipv4.tcp_syncookies = 1
# Log suspicious packets
net.ipv4.conf.all.log_martians = 1
# Disable IPv6 if not used
net.ipv6.conf.all.disable_ipv6 = 1
Apply changes:
sysctl -p
Set resource limits in /etc/security/limits.conf to prevent fork bombs and runaway processes:
* hard nproc 1024
* soft nproc 512
* hard nofile 4096
On Windows, equivalent protections come from Group Policy security options and registry tweaks. Disable unnecessary network protocols (NetBIOS, LLMNR) if not required:
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" -Name "EnableMulticast" -Value 0
What happens after you lock it down?
Log review becomes part of your routine. Check failed login attempts weekly:
grep "Failed password" /var/log/auth.log | tail -50
Watch for unusual port scans in your firewall logs. Monitor disk and CPU usage for signs of cryptominers or unauthorized processes.
Set up basic alerting for critical events—SSH root login attempts, service failures, disk space over 85%. Even a daily cron job that emails you a summary beats flying blind.
Re-audit your hardening every quarter. New services get added, configurations drift, and attack techniques evolve.
How do I test if hardening broke something?
Test each change incrementally. After modifying SSH config, open a new terminal and try connecting before closing your existing session. After enabling the firewall, confirm your application still serves traffic. After enabling SELinux or AppArmor, monitor logs for denials and test all critical workflows.
Keep a rollback plan. Document every change and know how to reverse it.
Should I change the SSH port if I already use key-only auth?
Key-only auth is the real security. Changing the port just reduces log noise and bot traffic. It won't stop a determined attacker, but it makes your logs cleaner and drops CPU load from constant brute-force attempts. Do it if you want quieter logs; skip it if you prefer standard ports for simplicity.
How often should I reboot after kernel updates?
Kernel updates require a reboot to take effect. Schedule them during your maintenance window—weekly or monthly depending on your uptime requirements. Use needrestart on Debian/Ubuntu to check if services need restarting after library updates:
apt install needrestart
needrestart
Can I automate all of this with Ansible or similar tools?
Yes. Every step here can be scripted and deployed with Ansible, Puppet, or Chef. Write playbooks that enforce your hardening baseline, and run them against new servers automatically. Configuration management tools also let you audit drift—when someone changes a setting manually, your next run detects and corrects it.
First step tomorrow morning
If you do one thing, disable password authentication on SSH and enforce key-only access. That single change blocks the majority of automated attacks. From there, add the firewall, then automated patching, then work through the rest of the list as time allows.
Hardening isn't a one-time checklist. It's a baseline you maintain and improve as your environment changes and new threats emerge. Start with the basics, test thoroughly, and build from there.
