Attack surface keeps growing. Every port you leave open, every default config you skip, every service running as root—it's a handhold for someone scanning your IP block. I've rebuilt servers after ransomware got in through an SSH brute-force that succeeded because fail2ban wasn't running. That ticket took three days to resolve and cost the client their staging environment.
This isn't theory. These nine steps are what I run on every production VPS I provision, and what I check first when a client calls about suspicious traffic.
1. Disable Root Login and Enforce SSH Keys
Password authentication over SSH is the easiest way in. Botnets hammer port 22 around the clock trying common passwords. I've seen auth logs with ten thousand failed attempts in a weekend.
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 that key-based login works before you lock out passwords. Open /etc/ssh/sshd_config and set:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
Restart SSH:
systemctl restart sshd
Now brute-force attacks hit a wall. No password means nothing to guess.
2. Change the Default SSH Port
Port 22 gets hammered. Moving SSH to a non-standard port won't stop a determined attacker, but it drops the noise floor dramatically. Automated scanners mostly hit common ports and move on.
Pick a port above 1024 and not used by another service—something like 2849 or 5122. Edit /etc/ssh/sshd_config:
Port 2849
If you're running a firewall (you should be), open the new port before restarting SSH:
ufw allow 2849/tcp
systemctl restart sshd
Reconnect with:
ssh -p 2849 deployuser@your-server-ip
Check your auth logs a day later. The difference is stark.
3. Configure a Firewall with Default-Deny Rules
A firewall should block everything by default and allow only what you need. UFW makes this simple on Ubuntu and Debian.
Install and enable UFW:
apt install ufw
ufw default deny incoming
ufw default allow outgoing
Allow your SSH port (use the custom port you set):
ufw allow 2849/tcp
If you're running a web server:
ufw allow 80/tcp
ufw allow 443/tcp
Enable the firewall:
ufw enable
Check the rules:
ufw status verbose
On CentOS or RHEL, firewalld is the default. The syntax differs but the principle is the same—block everything, open specific ports.
For more granular control, write iptables rules directly. But UFW handles most production scenarios without the complexity.
4. Install Fail2Ban to Block Brute-Force Attempts
Even with SSH keys, services like email and control panels accept passwords. Fail2ban watches log files and bans IPs after repeated failures.
Install it:
apt install fail2ban
Copy the default config:
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local. Under [sshd], set:
enabled = true
port = 2849
maxretry = 3
bantime = 3600
Restart fail2ban:
systemctl restart fail2ban
Check active jails:
fail2ban-client status
After a week, look at banned IPs:
fail2ban-client status sshd
You'll see dozens of addresses blocked. Each one represents an attack that stopped at the door.
5. Keep the Kernel and Packages Updated
Unpatched vulnerabilities are low-hanging fruit. I've handled incidents where a six-month-old kernel flaw gave an attacker root because automatic updates were disabled.
On Ubuntu and Debian, enable unattended-upgrades:
apt install unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
Edit /etc/apt/apt.conf.d/50unattended-upgrades to include security updates and optionally all updates:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
"${distro_id}:${distro_codename}-updates";
};
On CentOS, use dnf-automatic:
dnf install dnf-automatic
systemctl enable --now dnf-automatic.timer
Schedule a monthly manual review for kernel updates that require a reboot. Check for pending updates:
apt list --upgradable
Security patches need to land fast. Automate what you can.
6. Disable Unused Services and Remove Unnecessary Software
Every service is a potential entry point. If you're not using it, turn it off.
List running services:
systemctl list-units --type=service --state=running
Disable anything you don't recognize or need. For example, if you're not using Bluetooth:
systemctl disable bluetooth.service
systemctl stop bluetooth.service
List installed packages and remove bloat:
apt list --installed
apt remove package-name
On a fresh VPS, I typically disable snapd (if not needed), ModemManager, and any desktop-related services. A lean system has fewer bugs to patch and less surface area to attack.
What about services you do need? Run them with minimal privileges.
7. Run Services as Non-Root Users with Minimal Privileges
If a service gets compromised, you don't want the attacker landing in a root shell. Most daemons can run as dedicated users with restricted permissions.
For example, Nginx and Apache create their own users during installation. Verify with:
ps aux | grep nginx
You should see www-data or nginx as the user, not root.
For custom applications, create a dedicated user:
useradd -r -s /bin/false appuser
Set file ownership:
chown -R appuser:appuser /opt/yourapp
Use systemd to run the service as that user. In your service file:
[Service]
User=appuser
Group=appuser
Capabilities and AppArmor profiles add another layer, but even basic user isolation stops a lot of privilege escalation attempts.
8. Harden Kernel Parameters with sysctl
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/99-custom.conf:
# Disable IP forwarding
net.ipv4.ip_forward = 0
# Enable SYN cookies to prevent SYN flood attacks
net.ipv4.tcp_syncookies = 1
# 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
# Log martian packets
net.ipv4.conf.all.log_martians = 1
# Ignore ICMP ping requests (optional, breaks some monitoring)
net.ipv4.icmp_echo_ignore_all = 1
Apply the changes:
sysctl -p
These settings won't stop every attack, but they make certain techniques harder and noisier.
9. Set Up Centralized Logging and Intrusion Detection
You can't respond to what you can't see. Logs tell you when something's wrong, but only if you're watching them.
Ship logs to a central location—either a dedicated logging server or a service like Papertrail or Logtail. If an attacker wipes the local logs, you still have a record.
For intrusion detection, install AIDE (Advanced Intrusion Detection Environment):
apt install aide
aideinit
AIDE creates a database of file checksums. Run periodic checks:
aide --check
It reports any changed binaries or config files. Pair this with a cron job and email alerts.
For real-time monitoring, auditd tracks system calls and file access:
apt install auditd
systemctl enable auditd
Define rules in /etc/audit/rules.d/audit.rules. For example, watch /etc/passwd:
-w /etc/passwd -p wa -k passwd_changes
Query the audit log:
ausearch -k passwd_changes
Log aggregation and IDS won't prevent breaches, but they cut detection time from days to minutes.
What Hardening Can't Fix
No checklist is perfect. Hardening the OS won't help if your application has SQL injection flaws or if developers commit AWS keys to GitHub. Physical security, supply chain risks, and human error sit outside the scope of server config.
That said, these nine steps close the most common gaps I see in support tickets. An attacker who finds your SSH port will hit key-only auth. Botnets scanning port 22 will miss you entirely. Services running as unprivileged users limit the blast radius of a compromise.
Stack these layers. Each one makes the next attack harder.
Frequently Asked Questions
Q: Should I disable IPv6 for security?
No. Disabling IPv6 doesn't meaningfully improve security and breaks some services. Instead, apply the same firewall rules to IPv6 as IPv4.
Q: How often should I rotate SSH keys?
Rotate keys when someone with access leaves the team or if you suspect a private key was exposed. There's no need to rotate on a schedule otherwise.
Q: Is it safe to allow password auth for emergency access?
If you do, restrict it to specific IPs with firewall rules and set a very strong password. Key-only auth is still better.
Q: What's the difference between UFW and iptables?
UFW is a front-end for iptables that simplifies rule syntax. Under the hood, it writes iptables rules. Use whichever you're comfortable managing.
Q: Can fail2ban block legitimate users?
Yes, if they mistype passwords repeatedly. Whitelist your own IP or office network in /etc/fail2ban/jail.local under ignoreip.
What to Check First After Hardening
Verify you can still log in. Test SSH from a new terminal session before closing your current one. Confirm the firewall allows your services—run curl http://your-server-ip from another machine to check HTTP.
Check that fail2ban is watching the right logs and banning IPs. Look at /var/log/fail2ban.log for activity. Run aide --check after a day to confirm AIDE is tracking file changes.
Schedule a calendar reminder to review logs weekly and check for package updates monthly. Security is a maintenance task, not a one-time setup. But these nine steps give you a hardened baseline that stands up to the current threat landscape.
![Server Security Best Practices: 9 Steps to Lock Down [2026]](/images/blog/server-security-best-practices-9-steps-to-lock-down-2026-3.jpg)