There is a moment that every developer who has just provisioned a fresh VPS eventually experiences — the quiet satisfaction of seeing that green prompt for the first time, the server responding, ready to be shaped into whatever you need it to be. It is a good feeling. What most tutorials do not tell you, however, is what is happening in parallel on the other side of that connection. Within minutes — sometimes seconds — of your server coming online, automated bots from around the world have already begun probing it. They are scanning for open ports, testing common username and password combinations, looking for outdated software with known exploits, and checking for misconfigured services that might give them a foothold.
This is not a hypothetical scenario or an exaggeration for dramatic effect. It is simply the reality of putting any IP address on the public internet in the current threat landscape. The bots are not targeting you specifically — they are running continuously against every reachable IP address, looking for the ones that did not bother with basic hardening. The good news is that a few hours of focused configuration work will harden your server against the overwhelming majority of these automated attacks. This guide walks through each of those steps in the order you should apply them, with enough context to understand why each one matters — not just what command to run.
Understanding the Threat Before You Start
Before diving into configuration, it is worth spending a moment on what you are actually defending against, because the answer shapes how you prioritise the work.
The most common attack vectors against a fresh Linux server fall into a small number of categories. Brute-force SSH attacks are by far the most frequent — automated scripts cycle through thousands of common username and password combinations against port 22, the default SSH port, hoping to find an account with weak credentials. Exploit-based attacks target known vulnerabilities in software packages; if your server is running an outdated version of a service with a published CVE, scanners will find it and attempt to exploit it. Misconfiguration attacks look for services that are running but should not be — databases with no authentication, admin panels exposed to the public internet, or debug endpoints left enabled from development.
The comforting reality is that most automated attackers are not sophisticated. They are running scripts against millions of servers and moving on quickly to the next target if they encounter any resistance. You do not need to build a fortress to protect a typical web application server — you need to raise the barrier high enough that automated tools give up and move on. That said, doing the job properly also protects you from the smaller number of more determined attackers who go beyond automated scanning, and it builds habits that will serve you well as your infrastructure grows.
Step One: Hardening SSH Access
SSH is the front door of your server, and it is where most attacks begin. The default configuration that ships with most Linux distributions was designed for convenience, not security. Changing a handful of settings in /etc/ssh/sshd_config closes off the most common attack paths before anything else is in place.
The first and most important change is disabling root login. The root account exists on every Linux server and has no login attempt limits by default, which makes it a prime target for brute-force attacks. Setting PermitRootLogin no in your SSH configuration prevents direct root login entirely. You should be working as a non-root user with sudo privileges for administrative tasks anyway — this just enforces that practice at the SSH level.
The second change is switching from password-based authentication to key-based authentication. SSH keys are cryptographically generated key pairs — a private key that stays on your local machine and a public key that lives on the server. Authentication happens through a cryptographic challenge that cannot be brute-forced in any practical sense. Generate your key pair locally with ssh-keygen -t ed25519, copy the public key to your server using ssh-copy-id, and then set PasswordAuthentication no in sshd_config. From this point forward, only machines with the corresponding private key can authenticate — password guessing becomes irrelevant.
Moving SSH to a non-standard port is a lower-impact measure than the previous two, but it is worth doing. The overwhelming majority of automated SSH scanners target port 22 specifically. Changing to a high port number (something in the 1024–65535 range that does not conflict with other services) will eliminate most automated login attempts from your logs overnight. It does not provide real security against a targeted attacker who runs a port scan first, but it dramatically reduces noise and the attack surface for automated tools. Update the Port directive in sshd_config and remember to update your firewall rules before restarting the SSH service.
After making these changes, restart the SSH daemon with sudo systemctl restart sshd. Before closing your current session, open a second terminal window and verify that you can connect with the new configuration. Locking yourself out of a freshly hardened server is a rite of passage nobody wants to go through twice.
Step Two: Installing and Configuring fail2ban
Even with key-based authentication enabled, it is worth installing fail2ban as an additional defensive layer. fail2ban watches your system logs in real time and automatically adds temporary firewall rules to block IP addresses that show patterns consistent with brute-force attacks — too many failed login attempts in too short a time window.
Install it with your package manager (sudo apt install fail2ban on Debian/Ubuntu systems), and it will begin protecting SSH immediately with sensible defaults. The default configuration bans an IP address for 10 minutes after 5 failed login attempts within 10 minutes. For most servers, this is a reasonable starting point, though you can tighten it by editing /etc/fail2ban/jail.local (always edit the .local file, never the .conf file directly, so your changes survive package updates).
fail2ban is also extensible beyond SSH. If you are running a web application with a login form, you can configure fail2ban to watch your application logs and ban IPs that are attempting to brute-force user accounts through your HTTP endpoints. If you are running a mail server, it can monitor SMTP authentication failures. The same principle — watch logs, identify patterns, block bad actors — applies across your entire stack. Once you have it running, sudo fail2ban-client status gives you a live view of active bans, and it is mildly satisfying to watch the list fill up in the first few hours after installation.
Step Three: Configuring Your Firewall with UFW
A firewall is a fundamental piece of server infrastructure, not an optional add-on. UFW (Uncomplicated Firewall) is the most accessible firewall management tool available on Ubuntu and Debian-based systems, wrapping the underlying iptables rule system in a syntax that is straightforward to reason about.
The configuration philosophy for a server firewall is the inverse of what most people expect: start by denying everything, then explicitly allow only what you need. This default-deny approach means that any service you install that opens a new port is not automatically reachable from the internet — you have to consciously decide to expose it. Here is the sequence of commands to apply this configuration:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow [your-ssh-port]/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
The order matters here: always add your SSH port allow rule before enabling the firewall, or you will lock yourself out. Once enabled, verify the rules look correct with sudo ufw status verbose. You should see incoming traffic blocked by default with explicit exceptions only for your SSH port and your web ports.
As your server's role evolves, you will add more rules. If you are running a database that needs to accept connections from an application server, allow that specific port from that specific IP address rather than from any source. If you set up a monitoring agent that sends metrics to a central collector, allow that outbound connection. The discipline of keeping your firewall rules minimal and intentional — and documenting why each rule exists — pays compounding dividends as your infrastructure becomes more complex. UFW makes this easy enough that there is no excuse for running with the default permissive configuration that most servers ship with.
Step Four: Automated Security Updates
Software vulnerabilities are discovered constantly. The window between when a vulnerability is published and when exploit code appears in the wild has compressed to hours in some cases. Running outdated packages is one of the most common reasons servers get compromised, and it is one of the most preventable. Keeping your software current is not a one-time task — it is an ongoing operational responsibility.
unattended-upgrades is a tool that automates the application of security updates on Debian and Ubuntu systems. Install it with sudo apt install unattended-upgrades and enable it with sudo dpkg-reconfigure -plow unattended-upgrades. The default configuration applies security updates automatically while leaving other package updates for you to manage manually, which is the right balance — you want security patches applied immediately, but you do not want a routine package upgrade unexpectedly changing the behaviour of a service you depend on.
For updates that require a system reboot to take effect — kernel updates, glibc updates, and certain library updates fall into this category — you can configure unattended-upgrades to automatically reboot during a maintenance window. Edit /etc/apt/apt.conf.d/50unattended-upgrades and set Unattended-Upgrade::Automatic-Reboot "true" along with your preferred reboot time. For production servers running applications where downtime matters, pair this with a process manager like systemd or Supervisor that will restart your application services cleanly after a reboot, and make sure your application is designed to handle restarts gracefully.
Enable the update-notifier-common package and periodically review /var/log/unattended-upgrades/unattended-upgrades.log to confirm that updates are being applied as expected. Automated security updates are one of those configurations that can silently stop working — a misconfigured apt source, a held package blocking updates, a disk that is too full to download packages — so periodic verification that the automation is actually running is a responsible practice.
Step Five: Periodic Security Audits with Lynis
Configuration drift is real. Servers accumulate configuration changes over time — new services get installed, settings get tweaked for expediency, dependencies get added — and the cumulative effect can quietly erode a security posture that was solid at the start. Running periodic automated audits catches drift before it becomes a problem.
Lynis is an open-source security auditing tool that scans your system and produces a detailed report of potential security issues, misconfigurations, and hardening opportunities. Install it with sudo apt install lynis and run your first audit with sudo lynis audit system. The tool will work through a comprehensive list of checks — filesystem configuration, user account security, network settings, authentication mechanisms, software versions, and much more — and produce a scored report with specific recommendations for remediation.
The first time you run Lynis on a new server, the report will likely feel overwhelming. Do not try to address every finding immediately. Instead, work through the high-priority findings first, focusing on the items flagged as warnings rather than suggestions. Many of the lower-priority suggestions are genuinely optional hardening measures that depend on your specific threat model — a server running a public web application has different requirements than an internal database server. Run Lynis monthly as part of your operational routine, and track your hardening score over time. A rising score is a tangible indicator that your security posture is improving.
Step Six: Monitoring for Signs of Compromise
All of the steps above make your server significantly harder to compromise. But security is not a state you achieve once — it is a continuous process, and detection is as important as prevention. If an attacker does find a way in despite your hardening, you want to know as quickly as possible. The longer a compromise goes undetected, the more damage it can cause and the more difficult remediation becomes.
Netdata is one of the most capable and developer-friendly monitoring tools available. It provides real-time visibility into CPU usage, memory consumption, disk I/O, network traffic, and dozens of other system metrics — all through a clean web dashboard that requires almost no configuration to be immediately useful. Install it with a single command from their official documentation, and within minutes you have a live dashboard showing everything that is happening on your server. More importantly, Netdata includes a built-in alerting system that can notify you — via email, Slack, PagerDuty, or other channels — when metrics cross thresholds that might indicate a problem. A sudden spike in CPU usage, an unusual volume of outbound network traffic, or a rapid increase in disk write activity can all be early indicators of a compromised system.
For teams with multiple servers or more sophisticated monitoring requirements, Prometheus combined with Grafana provides a more scalable solution. Prometheus is a metrics collection system that scrapes data from configured targets at regular intervals and stores it in a time-series database. Grafana provides the dashboards and alerting on top of that data. The setup requires more initial configuration than Netdata, but the result is a monitoring system that can grow with your infrastructure and provide the kind of historical data needed to understand trends and set meaningful alert thresholds.
Beyond metrics monitoring, pay attention to your system logs. Tools like logwatch or GoAccess for web logs can surface patterns in your access logs that merit investigation. Regularly reviewing /var/log/auth.log for unusual authentication activity — logins from unexpected IP addresses, sudo usage at odd hours, failed authentication attempts for accounts that should not exist — is a habit that takes minutes but can catch problems that automated monitoring might miss.
Building Security Into Your Workflow
The steps described in this guide are not a one-time checklist to complete and forget. They are the foundation of an operational security practice that needs to be maintained and built upon over time. New vulnerabilities are discovered. New services get added. Infrastructure evolves. A hardening configuration that was thorough when you applied it may have gaps six months later.
The most effective approach is to treat security hardening as part of your server provisioning workflow rather than something you do after the fact. Write your hardening steps into a provisioning script or an Ansible playbook so that every new server you deploy starts from the same hardened baseline. Review your firewall rules and Lynis reports on a regular schedule. Subscribe to security advisories for the operating system and the software packages your applications depend on. And make sure that whoever else has access to your infrastructure — other developers, contractors, operations staff — understands the security baseline and is following the same practices.
A server that has been properly hardened is not impossible to compromise, but it is significantly less attractive to the automated tools that probe the internet looking for easy targets. It is also a server that gives you visibility into what is happening, alerts you when something unusual occurs, and applies security patches without requiring you to remember to do it manually. Done properly, security hardening is not a burden on your workflow — it is the foundation that lets you build and deploy with confidence.