Most breaches start with weak server fundamentals. A stolen SSH password, an open MySQL port, or root running a web app—small misconfigurations that attackers scan for at scale. I've seen production databases exposed because someone forgot to bind MySQL to localhost. The fix took two minutes; the breach investigation took two weeks.
Hardening your server means reducing the attack surface before someone finds it. The steps below lock down common entry points and limit what an attacker can do even if they get in.
1. Disable password authentication and harden SSH
Password-based SSH is the top vector I see in support tickets. Bots try thousands of root login attempts daily. If your password is weak or reused, they will get in.
Switch to key-based authentication. 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-ip
Then edit /etc/ssh/sshd_config and enforce these directives:
PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
Port 2222
Changing the default port to something above 1024 drops automated scans by over ninety percent in my experience. Restart SSH after the change:
sudo systemctl restart sshd
Test your key login from a second terminal before closing the original session. If something breaks, you still have a way back in.
Also disable empty passwords and challenge-response auth:
PermitEmptyPasswords no
ChallengeResponseAuthentication no
Finally, limit which users can SSH in by adding an AllowUsers line:
AllowUsers deploy admin
Only those two accounts will be able to authenticate.
2. Configure a host-based firewall and close unused ports
Every open port is a potential entry point. Default installs often expose services you never use—SMTP on port 25, MySQL on 3306, or a Redis instance with no password.
Use ufw on Ubuntu/Debian or firewalld on CentOS/RHEL. Start by denying all incoming traffic, then whitelist only what you need.
For ufw:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2222/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
sudo ufw enable
Check the active rules:
sudo ufw status numbered
If you run MySQL or PostgreSQL, bind them to 127.0.0.1 in their config files so they only accept local connections. For MySQL, edit /etc/mysql/mysql.conf.d/mysqld.cnf:
bind-address = 127.0.0.1
Restart the service:
sudo systemctl restart mysql
Scan your own server from the outside to confirm:
nmap -sV your-server-ip
You should see only SSH, HTTP, and HTTPS. Anything else needs a documented reason or it should be closed.
3. Run services with least privilege and separate users
Running everything as root is the fastest way to turn a small compromise into full system access. A vulnerable PHP script running as root can read /etc/shadow, install a kernel module, or wipe your disks.
Create dedicated users for each service:
sudo useradd -r -s /usr/sbin/nologin -d /var/www/html webuser
The -r flag makes it a system user with no home directory under /home. The -s /usr/sbin/nologin prevents interactive login.
For Nginx or Apache, set the user directive in the config. Nginx example in /etc/nginx/nginx.conf:
user webuser;
For application code, run it under a service account with no shell access. If you use systemd, specify the user in the unit file:
[Service]
User=appuser
Group=appuser
ExecStart=/usr/bin/node /opt/app/server.js
Never give service accounts sudo privileges. If your app needs to restart a service or read a log file, use capabilities or sudoers rules that whitelist a single command.
Check which processes are running as root:
ps aux | grep root
If you see your web server or database in that list, you have a problem.
4. Store secrets in environment variables or a vault, never in code
Hardcoded credentials in config files or committed to version control are the number one way internal credentials leak. A developer pushes a .env file to GitHub, a contractor forks the repo, or someone screenshots code for a support ticket.
Store secrets in environment variables loaded at runtime. For systemd services, use an EnvironmentFile:
[Service]
EnvironmentFile=/etc/app/secrets.env
ExecStart=/usr/bin/node /opt/app/server.js
The file /etc/app/secrets.env should be:
DB_PASSWORD=your_secure_password
API_KEY=your_api_key
Set restrictive permissions:
sudo chmod 600 /etc/app/secrets.env
sudo chown appuser:appuser /etc/app/secrets.env
For larger teams, use a secrets manager like HashiCorp Vault or AWS Secrets Manager. Your app fetches credentials at boot and never writes them to disk.
Audit your codebase for secrets:
grep -r "password" /var/www/
grep -r "api_key" /var/www/
If you find any, rotate them immediately and move them to environment variables.
Also scan your Git history. Tools like gitleaks or trufflehog will find committed secrets even if you deleted them in a later commit. The history is still there.
5. Enable automatic security updates and patch regularly
Unpatched systems are low-hanging fruit. Attackers scan for known CVEs in web servers, kernels, and libraries. If you're running an Nginx version with a public exploit, you will be found.
On Ubuntu/Debian, enable unattended-upgrades:
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Edit /etc/apt/apt.conf.d/50unattended-upgrades and ensure security updates are enabled:
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
};
For CentOS/RHEL, use dnf-automatic:
sudo dnf install dnf-automatic
sudo systemctl enable --now dnf-automatic.timer
Edit /etc/dnf/automatic.conf and set:
apply_updates = yes
Check for available updates manually once a week:
sudo apt update && sudo apt list --upgradable
or
sudo dnf check-update
Kernel updates require a reboot. Schedule them during low-traffic windows.
Subscribe to security mailing lists for your distribution and any critical software you run. When a zero-day drops, you want to know within hours, not weeks.
6. Centralize logs and monitor for anomalies
Logs are your early warning system. A spike in failed SSH attempts, a sudden outbound connection to an unknown IP, or a user account created at 3 AM—these are signals that something is wrong.
By default, logs sit in /var/log/ on each server. If an attacker gains root, the first thing they do is erase those logs. Ship them off-box in real time.
Use rsyslog or journald forwarding to send logs to a central server or a SaaS like Papertrail, Logtail, or Datadog. Example rsyslog config to forward everything:
*.* @@logserver.example.com:514
The @@ means TCP. Restart rsyslog:
sudo systemctl restart rsyslog
Monitor these patterns:
- Failed SSH login attempts (auth.log)
- Sudo command usage (auth.log)
- Service start/stop events (syslog)
- Outbound connections on unexpected ports (firewall logs)
- File changes in /etc or /usr/bin (use auditd)
Set up alerts for thresholds: more than ten failed SSH attempts in five minutes, any sudo command from a service account, or a new user added to /etc/passwd.
For intrusion detection, install fail2ban:
sudo apt install fail2ban
It watches logs and automatically bans IPs after repeated failed logins. The default config protects SSH, but you can add jails for Nginx, WordPress, or anything that writes to syslog.
Check current bans:
sudo fail2ban-client status sshd
What happens after you harden?
Hardening is not a one-time task. New services get installed, developers need temporary access, and software updates introduce new config directives. I audit server configs quarterly and after any major change.
Run a port scan from outside your network every month. Use nmap or an external scanner like Shodan or SecurityHeaders.com. Compare the results to your documented baseline. Any new open port is a red flag.
Test your SSH config by trying to log in with a password from a different machine. It should fail. Try logging in as root—also fail. If either succeeds, roll back and check your sshd_config.
Review your firewall rules and remove anything you added for a temporary project six months ago. Check /var/log/ufw.log or /var/log/firewalld for blocked attempts. A spike in blocks targeting a specific port means someone is scanning for a service you might have exposed in the past.
Rotate secrets and API keys on a schedule. Even if they haven't leaked, rotation limits the blast radius if they do.
Where to focus first
If you can only do three things today, start with SSH keys, close your database ports, and enable automatic security updates. Those three changes will block the majority of automated attacks I see in hosting support. The rest you can layer on over the next few weeks.
Hardening is about making the attacker's job harder than it's worth. Every step raises the bar. Most will move on to easier targets.
![Prevent Data Breach: 6 Server Hardening Steps [2026]](/images/blog/prevent-data-breach-6-server-hardening-steps-2026.jpg)