Skip to content
Back to Blog
Security11 min read

Server Security Hardening 2026: 9 Fixes That Stop Breaches

Walk through nine configuration changes and access controls that defend against the attacks happening right now—from SSH brute-force to privilege escalation.

Written by Abdul AbrorTechnical Hosting Support Engineer
Server Security Hardening 2026: 9 Fixes That Stop Breaches
On this page

Breaches don't happen because attackers found a zero-day in your kernel. They happen because SSH runs on port 22 with password auth enabled, because a service account has a shell it doesn't need, or because file permissions let www-data write to your cron directory. The fixes are boring, repetitive, and they work.

I've walked customers through post-breach forensics enough times to see the patterns. The entry point is almost always a weak credential, an exposed service, or a misconfigured permission that handed an attacker exactly what they needed. What follows are the nine changes that close those doors.

1. Disable SSH password authentication entirely

Password auth is the front door attackers knock on first. Brute-force attempts hit every publicly-routed server within hours of provisioning. I've seen logs with tens of thousands of failed root login attempts in a single day.

Switch to key-based authentication and turn passwords off. Generate an ED25519 key pair on your local machine:

ssh-keygen -t ed25519 -C "[email protected]"

Copy the public key to your server:

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@your-server

Then edit /etc/ssh/sshd_config:

PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM no
PermitRootLogin prohibit-password

Restart SSH and test the connection from a second terminal before you close your current session. If you lock yourself out, you'll need console access through your hosting panel.

2. Change the default SSH port

Security through obscurity isn't a strategy, but it cuts noise. Moving SSH off port 22 drops automated scan traffic by ninety percent. Your logs stay readable and your fail2ban rules spend cycles on real threats instead of script kiddies.

Pick a high port between 10000 and 65535. Edit /etc/ssh/sshd_config:

Port 47823

Update your firewall rules to allow the new port and block 22:

ufw allow 47823/tcp
ufw delete allow 22/tcp
ufw reload

Restart SSH, then connect on the new port:

ssh -p 47823 user@your-server

Store the port in your ~/.ssh/config so you don't have to type it every time:

Host your-server
    HostName 192.0.2.10
    Port 47823
    User youruser

3. Configure fail2ban with aggressive jail times

Fail2ban watches your logs and blocks IPs that trigger too many auth failures. The default configuration is too forgiving. An attacker gets ten tries before a ten-minute ban—plenty of time to rotate IPs or walk through a credential list.

Install fail2ban:

apt install fail2ban

Create /etc/fail2ban/jail.local:

[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3

[sshd]
enabled = true
port = 47823
logpath = /var/log/auth.log

Three failures in ten minutes earns an hour-long ban. For repeat offenders, enable incremental banning:

bantime.increment = true
bantime.factor = 2
bantime.maxtime = 604800

Restart fail2ban and watch the ban list grow:

systemctl restart fail2ban
fail2ban-client status sshd

4. Remove unused user shells and lock service accounts

Every user account with a login shell is a potential pivot point. Service accounts—mysql, www-data, nginx—don't need shells. An attacker who compromises a web application shouldn't be able to pop a shell as www-data and start enumerating the system.

List accounts with login shells:

grep -vE ':(false|nologin)$' /etc/passwd

For any service account in that list, lock it:

usermod -s /usr/sbin/nologin mysql
usermod -s /usr/sbin/nologin www-data
usermod -L nginx

The -L flag locks the password so the account can't authenticate even if someone discovers a hash.

5. Harden kernel parameters with sysctl

The kernel ships with network stack defaults that prioritize compatibility over security. A few sysctl tweaks block common reconnaissance and amplification attacks.

Edit /etc/sysctl.conf or create /etc/sysctl.d/99-hardening.conf:

# Disable IP forwarding
net.ipv4.ip_forward = 0
net.ipv6.conf.all.forwarding = 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
net.ipv6.conf.all.accept_source_route = 0

# Enable TCP SYN cookies
net.ipv4.tcp_syncookies = 1

# Log martian packets
net.ipv4.conf.all.log_martians = 1

# Disable ICMP echo on broadcasts
net.ipv4.icmp_echo_ignore_broadcasts = 1

# Protect against time-wait assassination
net.ipv4.tcp_rfc1337 = 1

Apply the changes:

sysctl -p

These settings won't stop a determined attacker, but they close off low-hanging reconnaissance tactics and make DDoS amplification harder.

6. Restrict file permissions on sensitive paths

World-readable configuration files leak database credentials, API keys, and session secrets. Writable directories under web roots let attackers upload PHP shells. Check the obvious places first.

Lock down common config files:

chmod 600 /root/.ssh/authorized_keys
chmod 600 /etc/ssh/sshd_config
chmod 640 /etc/mysql/my.cnf
chown root:root /etc/crontab
chmod 600 /etc/crontab

For web applications, nothing under the document root should be writable by the web server except specific upload or cache directories, and those directories should never allow script execution. In Apache, drop a .htaccess file in upload directories:

<FilesMatch "\.(php|phtml|php3|php4|php5|pl|py|jsp|asp|sh|cgi)$">
    Require all denied
</FilesMatch>

In Nginx, deny execution in the upload location block:

location /uploads {
    location ~ \.php$ {
        return 403;
    }
}

7. Enable automatic security updates

Patching is the most effective defense and the one admins skip most often. Manual update schedules slip. Kernel updates sit unapplied because a reboot needs scheduling. Unattended-upgrades fixes this.

On Debian and Ubuntu:

apt install unattended-upgrades
dpkg-reconfigure -plow 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-based systems, enable dnf-automatic:

dnf install dnf-automatic
systemctl enable --now dnf-automatic.timer

You can restrict updates to security patches only by editing /etc/dnf/automatic.conf:

upgrade_type = security

8. Use a mandatory access control system

Discretionary access controls—file permissions, user groups—rely on applications and users making correct decisions. They fail when a compromised process inherits the wrong permissions. Mandatory access control systems like AppArmor and SELinux enforce policy at the kernel level.

AppArmor ships enabled on Ubuntu. Confirm it's running:

aa-status

Most common services have profiles already. Load them:

aa-enforce /etc/apparmor.d/usr.sbin.nginx
aa-enforce /etc/apparmor.d/usr.sbin.mysqld

If a profile breaks your application, switch it to complain mode while you debug:

aa-complain /etc/apparmor.d/usr.sbin.nginx

Check /var/log/syslog for AppArmor denials and adjust the profile as needed. On RHEL systems, SELinux does the same job. Check its status:

getenforce

If it returns Permissive or Disabled, enable it by editing /etc/selinux/config:

SELINUX=enforcing

Reboot for the change to take effect. SELinux policies are complex, but the default targeted policy covers most server workloads.

9. Audit sudo access and require passwords

Sudo without a password prompt is convenient. It's also a free privilege escalation. If an attacker compromises a user account that can sudo without a password, they own root immediately.

Edit /etc/sudoers with visudo and remove NOPASSWD entries:

# Bad
admin ALL=(ALL) NOPASSWD: ALL

# Good
admin ALL=(ALL) ALL

Restrict sudo to specific commands when possible. A deployment user doesn't need full root:

deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart myapp, /bin/systemctl status myapp

Enable sudo logging to track who ran what:

Defaults logfile="/var/log/sudo.log"
Defaults log_input, log_output

Review the log regularly. Unexpected sudo invocations are an early warning.

What to check first

Start with SSH and user access. Key-based auth, fail2ban, and locked service accounts stop the majority of automated attacks. Then move to permissions and kernel hardening. Finish with automatic updates and mandatory access controls.

Hardening isn't a checklist you run once. Services get added, configs drift, and new CVEs drop. Schedule quarterly reviews and test your changes in a staging environment first—especially SSH config and firewall rules.

Common questions

Does moving SSH to a different port actually help?
It won't stop a targeted attack, but it eliminates almost all automated scanning noise. Your logs become usable again and fail2ban can focus on real threats.

Should I disable root login completely?
Yes. Use a regular user account with sudo access instead. Set PermitRootLogin no in sshd_config after you've tested sudo access.

What if AppArmor or SELinux breaks my application?
Switch the profile to complain mode and review the denial logs. Most issues are fixable by adjusting the profile. Disabling MAC entirely trades security for convenience you probably don't need.

How often should security updates run?
Daily for security patches. Kernel updates need a reboot, so schedule those during maintenance windows or enable automatic reboots at off-peak hours.

Can I automate all of this with a script?
Yes, and you should. Configuration management tools like Ansible make hardening reproducible across fleets. I keep a playbook that applies all nine fixes and run it on every new server.

Lock it down now

Most breaches exploit boring misconfigurations. SSH passwords, loose file permissions, unpatched services. The fixes don't require specialized knowledge or expensive tools—they require discipline. Apply these nine changes and you've closed the doors attackers walk through every day.