SECURITY

How to secure a Linux server: the first hour checklist

By , FounderUpdated 9 min read

Before you start: update and keep a session open

These steps are written for a fresh Ubuntu 24.04 LTS server where your provider gave you either a root password or a root SSH key. Most of it applies to Debian too. Do it before you deploy anything: a server with a public IP gets automated login attempts very soon after it boots.

Log in as root and bring the system up to date first:

terminal (as root)
apt update && apt full-upgrade -y
# If this file exists, a reboot is needed (usually a new kernel)
[ -f /var/run/reboot-required ] && reboot

Several steps below change how you log in. Keep one SSH session open the whole time and test every change from a second terminal. If the new login fails, you still have the first session to undo it. Close the original session only after the new login works.

Step 1: Create a non-root sudo user

Working as root means every typo runs with full power, and every attacker knows the username. Create your own user and give it sudo rights. Replace deploy with whatever name you like.

terminal (as root)
adduser deploy
usermod -aG sudo deploy

adduser asks for a password. Set a strong one: you won't use it to log in over SSH, but sudo will ask for it.

Step 2: Log in with an SSH key

On your own computer (not the server), create an ed25519 key if you don't already have one. Give it a passphrase so a stolen laptop doesn't mean a stolen server.

your computer
ssh-keygen -t ed25519 -C "you@your-laptop"

Copy the public key to the new user. On Linux or macOS:

your computer
ssh-copy-id deploy@SERVER_IP

Windows doesn't ship ssh-copy-id. In PowerShell, this does the same job:

PowerShell
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh deploy@SERVER_IP "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

If your provider only allows key login for root, copy root's authorized keys to the new user from your existing root session instead:

terminal (as root)
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

Now, in a second terminal, check that ssh deploy@SERVER_IP logs you in without a password prompt and that sudo whoami prints root. Don't continue until both work.

Step 3: Turn off root and password login

On Ubuntu 24.04, /etc/ssh/sshd_config starts with Include /etc/ssh/sshd_config.d/*.conf. Put your changes in a drop-in file there rather than editing the main file, so package updates don't fight with you. For most settings sshd uses the first value it reads, and files are read in name order, so a name starting with 00- wins over files like 50-cloud-init.conf that some providers add with PasswordAuthentication yes.

/etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes

Check the syntax, confirm the settings sshd will actually use, then restart it:

terminal
sudo sshd -t
sudo sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication)'
sudo systemctl restart ssh

On Ubuntu the service is called ssh, not sshd. Restarting it doesn't drop sessions that are already open. Now test from a second terminal:

  • ssh deploy@SERVER_IP should still log in with your key.
  • ssh root@SERVER_IP should fail with Permission denied (publickey).
  • ssh -o PubkeyAuthentication=no deploy@SERVER_IP should fail without ever asking for a password.

Only when all three behave as expected should you close your original root session.

Step 4: Close every port you don't need

Ubuntu ships with ufw, a simple front end to the kernel firewall. Allow SSH before you enable it, or you will lock yourself out.

terminal
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
# Only if this server serves websites:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Then check what is actually listening on the network:

terminal
sudo ss -tulpn

Anything bound to 0.0.0.0 or [::] listens on every interface. Databases such as MySQL, PostgreSQL and Redis should listen on 127.0.0.1 unless another server genuinely needs to reach them, and then only that server's IP should be allowed through. Your Node.js or Python app should sit behind Nginx on a local port, not face the internet directly.

If your provider also offers a network firewall in its control panel, use it as a second layer with the same rules. It blocks traffic before it reaches the server, and it still protects you if a package or a script ever changes the rules on the machine itself. Keep both lists in sync, and write down why each open port is open, so the next person (or you, in six months) knows what is safe to close.

Docker publishes container ports by writing its own firewall rules, which bypass ufw. A container started with -p 3000:3000 is reachable from the internet even if ufw says port 3000 is closed. Publish to localhost instead (-p 127.0.0.1:3000:3000) and let Nginx proxy to it. Our guide to Docker in production covers this in more detail.

Step 5: Automatic security updates and fail2ban

Unattended upgrades

Ubuntu Server normally ships with unattended-upgrades enabled for security updates, but images vary between providers, so check rather than assume:

terminal
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades
sudo unattended-upgrade --dry-run --debug

20auto-upgrades should contain APT::Periodic::Update-Package-Lists "1"; and APT::Periodic::Unattended-Upgrade "1";. Security updates will then install daily. Kernel updates still need a reboot. You can either reboot by hand when /var/run/reboot-required appears, or set Unattended-Upgrade::Automatic-Reboot "true"; and an Automatic-Reboot-Time in /etc/apt/apt.conf.d/50unattended-upgrades if a short nightly outage is acceptable.

fail2ban

fail2ban watches logs and temporarily bans IPs that keep failing to log in. With key-only SSH, brute force is already unlikely to succeed, but fail2ban cuts the noise and slows down scanners.

terminal
sudo apt install fail2ban

Never edit jail.conf; put your settings in jail.local so updates don't overwrite them:

/etc/fail2ban/jail.local
[sshd]
enabled  = true
maxretry = 5
findtime = 10m
bantime  = 1h
terminal
sudo systemctl enable --now fail2ban
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd

Step 6: Backups you have restored, and a look at the logs

Backups

A backup stored on the same server disappears with the server. Send backups somewhere else: another server, object storage or a backup service. Dump databases with mysqldump or pg_dump first, because copying a running database's files is not a reliable backup. Restic, available from Ubuntu's repositories, handles encrypted, deduplicated backups to an SFTP server or object storage:

terminal (as root, e.g. after sudo -i)
apt install restic
export RESTIC_REPOSITORY="sftp:backup@BACKUP_HOST:/srv/restic/web1"
export RESTIC_PASSWORD_FILE=/root/.restic-password   # chmod 600, and keep a copy off the server
restic init                                          # once
restic backup /etc /var/www /home/deploy/db-dumps
restic snapshots
# The part people skip: prove you can get the data back
restic restore latest --target /tmp/restore-test

Run the backup from a systemd timer or cron, and put a restore test in your calendar. If you have never restored from a backup, you don't know that you have one.

Monitoring and logs

Set up an external uptime check that alerts your phone, then get into the habit of a quick look:

terminal
df -h                                # disk space
free -h                              # memory
sudo journalctl -p err -b            # errors since the last boot
sudo journalctl -u ssh --since today # SSH logins and failures
sudo fail2ban-client status sshd     # who is being banned
systemctl --failed                   # services that have crashed

Full disks, crashed services and failed certificate renewals are what usually take a small server down, and all three show up here before your visitors notice. If you plan to put the server behind a CDN, our guide to using Cloudflare with your own server explains how to lock the web ports to Cloudflare as well.

Where NevaraCloud fits

This checklist is the baseline for any server you run yourself, and keeping it up is ongoing work. If you'd rather not own that work, our managed hosting takes care of patching, firewalls and SSL for you. We don't sell VPSs; if you aren't sure which way to go, read managed hosting vs a VPS.

If you already run your own server and want a second pair of eyes on it, or something has broken, we can harden or fix an existing setup, quoted after we look at it.

Rather we handled it?

Managed hosting billed in LKR, Docker hosting, or hands-on deployment help. Tell us what you're working on.

Ask us on WhatsApp