A freshly installed Linux server is a machine on the public internet with a login prompt. On a busy network that prompt is found by scanners within minutes. The work of turning it into something you can leave running unattended takes an hour or two, and almost all of it is a small number of decisions made in a specific order.
This guide covers the baseline for a new KVM VPS: key-based SSH, a second non-root account, a default-deny firewall, automatic security updates, and a backup you have actually restored. It assumes Ubuntu 24.04 or Debian 12 with apt, and notes where AlmaLinux 9 or Rocky 9 with dnf differ. The commands are deliberately plain; adjust names and ports to match your own conventions.
One rule matters more than any command here: never close the session you are working in until you have opened a second one and logged in successfully with the new settings. Almost every lockout story is someone who changed sshd_config, reloaded, and disconnected.
Before you connect
Know three things before you touch the machine: the IPv4 address, the root password or key you were given, and which operating system image was installed. On this catalogue the connection details appear in the customer dashboard under the service once the order is marked ready, and the VPS catalogue lists the images and the hardware each configuration includes.
The allowlisted images are Ubuntu 24.04, Debian 12, AlmaLinux 9 and Rocky 9, plus Windows Server 2022 and a custom ISO option. The steps below assume one of the four Linux images and a server with no data on it yet. If the image supports reinstalling from your panel, note that a reinstall is a clean slate, not a repair.
Step 1: create your own key pair
Generate the key on the machine you will connect from, not on the server:
ssh-keygen -t ed25519 -a 100 -C "laptop-to-offshore"
ed25519 is the current default choice for new keys: short, fast and not dependent on the strength of a random prime. The -a 100 flag sets key-derivation rounds for the passphrase, which is worth the fraction of a second it costs at login. Give the passphrase somewhere safe; a key with no passphrase on a stolen laptop is an open door.
Copy the public key to the server. If the image has ssh-copy-id available on your side:
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@SERVER_IP
If not, append the contents of ~/.ssh/id_ed25519.pub to /root/.ssh/authorized_keys on the server, then fix the permissions, which is where most failures come from:
mkdir -p /root/.ssh && chmod 700 /root/.ssh
chmod 600 /root/.ssh/authorized_keys
Open a new session and confirm you get in without a password prompt. Only then continue.
Step 2: create a non-root account
Running daily work as root is the habit that turns a small mistake into a rebuild. Create a personal account with sudo:
On Ubuntu or Debian:
adduser deploy
usermod -aG sudo deploy
On AlmaLinux or Rocky:
useradd -m -s /bin/bash deploy
passwd deploy
usermod -aG wheel deploy
Copy the same public key into the new account so it can log in the same way:
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
Test it: ssh deploy@SERVER_IP, then sudo -v. If both work, you no longer need root logins for normal administration.
Step 3: harden the SSH daemon
Modern OpenSSH reads a drop-in directory, so put your changes in a single file instead of editing the distribution's configuration. Create /etc/ssh/sshd_config.d/10-hardening.conf with:
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 20
X11Forwarding no
ClientAliveInterval 300
ClientAliveCountMax 2
A few notes on the choices:
PermitRootLogin prohibit-passwordallows the root account by key only. Once yourdeployaccount is comfortable,nois stricter still, and is the better end state.PasswordAuthentication nois the single highest-value line in the file. It removes online password guessing entirely as an attack path, which is what most of the noise in your authentication log is.KbdInteractiveAuthentication nocloses the PAM path that some distributions still leave open for password logins even whenPasswordAuthenticationis off.X11Forwarding noremoves a service you almost certainly do not need on a server.
Validate the configuration before reloading. This step catches syntax errors that would otherwise leave you with a daemon that refuses to start:
sshd -t
Then reload rather than restart, so existing sessions survive:
systemctl reload ssh
On AlmaLinux and Rocky the unit is called sshd:
systemctl reload sshd
Open a second terminal and log in as deploy with the key before you close the first one.
Step 4: a default-deny firewall
The principle is simple: drop inbound traffic that nothing asked for, allow the services you actually run, and allow SSH before you enable anything.
On Ubuntu and Debian, ufw is the least error-prone option:
ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw enable
ufw status verbose
If you have already moved SSH to a non-standard port, allow that port specifically and keep the rule for 22 until you have verified the new one.
On AlmaLinux and Rocky, firewalld is already present:
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
firewall-cmd --list-all
When you add a web server later, open exactly what it needs — ufw allow 80,443/tcp or firewall-cmd --permanent --add-service=http --add-service=https — rather than disabling the firewall when something does not connect. A database port opened to the world "for a minute" is how most compromised servers begin.
Step 5: automatic security updates
Unpatched software is the most common way a server is lost, and the fix is boring. On Ubuntu and Debian, install the unattended upgrade service:
apt update
apt install unattended-upgrades
dpkg-reconfigure --priority=low unattended-upgrades
That writes /etc/apt/apt.conf.d/20auto-upgrades with the periodic settings. Confirm it contains:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Then check what is allowed to install automatically in /etc/apt/apt.conf.d/50unattended-upgrades. By default the security pocket is enabled on Ubuntu, which is the right scope: security fixes apply themselves, and feature upgrades stay a decision you make. Keep automatic reboots off unless you have planned for them; instead, watch for the reboot flag and schedule a window:
ls /var/run/reboot-required 2>/dev/null && cat /var/run/reboot-required
On AlmaLinux and Rocky, dnf-automatic does the same job:
dnf install -y dnf-automatic
In /etc/dnf/automatic.conf set upgrade_type = security and apply_updates = yes, then enable the timer:
systemctl enable --now dnf-automatic.timer
systemctl list-timers dnf-automatic.timer
Check the results later rather than assuming they happened. journalctl -u unattended-upgrades --since "7 days ago" or /var/log/dnf-automatic.log will tell you whether updates are actually being applied, and a silent failure is common enough that the log is worth a look in the first week.
Step 6: time, swap and a look at what is listening
Two things that are not security features but cause security-shaped problems:
Time. Certificates, TOTP codes and logs all assume a correct clock. timedatectl set-timezone UTC and timedatectl status will confirm System clock synchronized: yes. A server with drifting time produces log entries you cannot correlate and TLS handshakes that fail for no visible reason.
Swap. A 2 GB server running a build can hit the OOM killer and take a database with it. A small swap file is not a performance feature; it is a cushion:
fallocate -l 2G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
Then look at what is actually listening:
ss -tulpn
Every line is a service reachable from somewhere. Anything bound to 0.0.0.0 or :: is reachable from the internet unless the firewall says otherwise. A database listening on all interfaces with a weak password is the single most common finding in a post-incident review.
Step 7: a backup that has been restored
A backup you have never restored is a hypothesis. The minimum that holds up:
- Move it off the machine. A snapshot on the same provider, in the same account, is not a backup against a billing problem or an account compromise.
- Encrypt it before it leaves.
resticandborgboth encrypt client-side, so the storage provider sees ciphertext. - Include the configuration, not only the data.
/etc, the crontabs, the web server vhosts and the database dumps. - Test the restore on a second machine, at least once.
A workable pattern with restic against an SFTP target or S3-compatible storage:
restic init --repo sftp:backup@backup-host:/srv/restic/server1
restic backup --repo sftp:backup@backup-host:/srv/restic/server1 /etc /home /var/www
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
For databases, dump to a file first so the backup holds a consistent snapshot rather than open files:
mysqldump --single-transaction --routines --triggers --default-character-set=utf8mb4 app > /var/backups/app-$(date +%F).sql
Then schedule it. A cron entry that runs at 03:15 and logs its exit status is enough to start with; an alert when the job fails is what makes it a backup system rather than a script.
The mistakes that undo all of this
- Disabling password authentication before the key works. Test with a second session, always.
- Enabling the firewall over the only SSH connection. Allow SSH first, then enable.
chmod -R 777to fix a permission error. It creates the next problem while hiding this one. Fix ownership instead.- Running the application as root. One remote-code-execution bug then becomes a full server compromise rather than a contained one.
- Exposing the database "just for the import". Use an SSH tunnel;
ssh -L 3306:127.0.0.1:3306 deploy@SERVER_IPcosts nothing. - Trusting a backup that has never been restored. The failure is discovered on the worst possible day.
- Treating a reinstall as a repair. Reinstalling destroys the evidence you would need to understand how the server was compromised.
A short verification pass
Run this after the steps above and keep the output somewhere:
sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication'
ufw status verbose
systemctl is-enabled unattended-upgrades
ss -tulpn
If permitrootlogin says prohibit-password or no, passwordauthentication says no, the firewall is active with a short allow list, and nothing unexpected is listening, you have the baseline. From there the work is ordinary: keep the application patched, watch the logs, and rehearse the restore.
For the next decision — whether this workload belongs on a VPS or on a whole machine — see VPS vs dedicated server. If the reason you are on this provider is jurisdiction rather than hardware, the offshore hosting primer explains what the location does and does not change, and the dedicated server catalogue covers the bare-metal alternative.