Skip to content
Back to Blog
Security11 min read

Server Security Best Practices: 9 Steps to Lock Down [2026]

Nine actionable hardening steps to protect production infrastructure from current attack patterns—SSH keys, firewall rules, automated updates, and monitoring that actually works.

Written by Abdul AbrorTechnical Hosting Support Engineer
Server Security Best Practices: 9 Steps to Lock Down [2026]
On this page

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.