← All posts

Hardening a new Linux server: the checklist I run before deploying anything

The steps I take on every fresh Ubuntu or Debian VPS before it hosts a real product: users and SSH, firewall, updates, Docker exposure, secrets, backups and monitoring.

Author
Elie Chaaban
Published
Reading time
5 min
Topics
DevOps, Linux, Security, Servers

A fresh VPS from any provider is reachable from the whole internet within seconds of booting, and automated scanners start knocking almost immediately. If you look at the SSH log of a new server after a day, you will see login attempts from addresses all over the world. None of that is personal. It is background noise, and a basic, boring setup makes it irrelevant.

This is the checklist I run on every new Linux server before it hosts anything real, whether it is a client's website or a production platform like the ones behind NeonTask and Pro Clean+. It assumes Ubuntu or Debian, but the ideas apply anywhere.

1. Create a normal user and stop working as root

Log in once as root, create a personal user, add it to the sudo group and copy your SSH public key to it:

adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh && chmod 600 /home/deploy/.ssh/authorized_keys

Open a second terminal and confirm you can log in as that user and run sudo before you change anything else. Locking yourself out of a server is a rite of passage, but it is better skipped.

2. Lock down SSH

In /etc/ssh/sshd_config (or a file in sshd_config.d/), set:

PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes

Then reload SSH and, again, test a fresh login before closing your current session. Key-only login removes the entire class of password-guessing attacks. Changing the SSH port reduces log noise but is not a security control on its own; key-only authentication is what matters.

Use a separate key per person and per purpose. A CI/CD pipeline that deploys to the server should have its own key, restricted where possible, so you can revoke it without touching anyone else's access.

3. Turn on a firewall that denies by default

With ufw the setup is short:

ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Only the ports you actually serve should be open. Databases, caches, admin panels and metrics endpoints should never be reachable from the internet directly. If you need to reach them, use an SSH tunnel or a private network.

Many providers also offer a cloud firewall in front of the server. Using both is reasonable: the cloud firewall protects you if a local rule is misconfigured.

4. Know how Docker interacts with the firewall

This catches many people. When you publish a container port with Docker, for example -p 5432:5432, Docker inserts its own iptables rules, and on many setups that port becomes reachable from the internet even though ufw says it is blocked.

The fixes:

After deploying, check from another machine which ports are actually open. Trust the scan, not the config. I go deeper into this setup in running Docker Compose in production on a single server.

5. Automatic security updates

Enable unattended security updates so critical patches are applied without waiting for you:

apt install unattended-upgrades
dpkg-reconfigure --priority=low unattended-upgrades

Decide how you handle reboots. Kernel updates need one, and an automatic reboot at a quiet hour is often better than a server running an old kernel for months. If your services are in containers with restart policies, a reboot is usually a short, uneventful blip, but test that once deliberately rather than discovering it at 3 AM.

6. Brute-force protection

With password login disabled, SSH guessing cannot succeed, but tools like fail2ban still reduce noise and protect other services that do have logins. Configure it for SSH and for any exposed login endpoint whose logs it can read.

For web applications, rate limiting on login and password reset endpoints belongs in the app or the reverse proxy.

7. Secrets out of the repository

Database passwords, API keys and signing keys do not belong in Git. Keep them in environment files on the server with tight permissions (chmod 600, owned by the deploying user), or in your CI/CD system's secret store, injected at deploy time. Rotate anything that ever leaked into a commit; deleting the file later does not remove it from history.

8. Time, hostname and basics

Small things that save debugging time later:

9. Backups that leave the server

A backup stored on the same server is not a backup. Database dumps and uploaded files should go to separate storage, ideally with a different provider or at least a different account, on a schedule, with retention. And a backup is only real once you have restored it. I cover that in detail in Postgres backups that actually restore.

Provider snapshots are a useful extra layer for quick rollback, but they are not a substitute for application-level backups you control.

10. Monitoring and alerts from outside

At minimum:

Set Docker log rotation so container logs do not fill the disk, and prune old images on a schedule. I describe a practical setup in monitoring and alerts for a small product.

11. Keep a short runbook

Write down, in a file you can find in a stressful moment: how to SSH in, where the compose files and environment files live, how to deploy, how to roll back, where backups are and how to restore them, and who has access. When something breaks, this page saves more time than any tool.

12. Review access regularly

People leave projects. Keys get copied to new laptops. Every few months, check authorized_keys, CI/CD deploy keys, provider account users and DNS account access. Remove anything you cannot explain.

What this checklist does not cover

This is the baseline for a single server running web applications. Products handling sensitive personal or financial data need more: audit logging, intrusion detection, encryption at rest, network segmentation and a proper security review. If that is your situation, the baseline is still the starting point, not the finish line.

None of these steps is clever. That is the point. Most compromised servers I have seen were not beaten by sophisticated attacks. They had a password login, an exposed database port or an unpatched service. Doing the boring things consistently removes most of the risk.

If you want a second pair of eyes on how your servers are set up, or you need someone to set them up properly from the start, see my work and get in touch.

More from the blog

Let's work
together