Skip to content

How to Set Up a VPS for Beginners, from a Beginner (Part 1: Securing the Base System)

Part 1 of setting up my first VPS on Debian 13. Updating, naming the server, one user with SSH keys and sudo, and an SSH config that only lets that user in.

A 5-part series.

Introduction

This is my first VPS. It’s a small Debian 13 machine, and I want to run my own projects on it, starting with this website.

A fresh server has nothing configured, so nothing is protecting it either. I wanted to understand every step before trusting the box with anything, so I did all of it by hand and wrote it down as I went. That’s this series.

At the end I turned everything into one script, vps-setup.sh. If you only want the result, Part 5 shows how to run it. If you want to know what it’s doing to your server, start here.

First Access

The provider gives you an IP and a root login. Connect:

ssh root@IP_VPS
Output
The authenticity of host 'IP_VPS (IP_VPS)' can't be established.
ED25519 key fingerprint is SHA256:qL3nR8vT1wX5yZ0aB4cD7eF9gH2jK6mN8pQ1rS3tU5v.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes

Linux vps 6.12.48+deb13-amd64 #1 SMP Debian 6.12.48-1 x86_64
root@vps:~#

The fingerprint question only comes up the first time. If it ever shows up again for the same server, something changed on the other end, and I’d want to know what before typing yes.

I’m going to use root for the next few minutes and then never log in as root again.

Updating

The server was created from the provider’s image, and that image is never fully up to date:

apt update && apt full-upgrade -y
Output
Hit:1 http://deb.debian.org/debian trixie InRelease
Get:2 http://security.debian.org/debian-security trixie-security InRelease [48.0 kB]
...
14 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

I use full-upgrade instead of upgrade because it’s allowed to install or remove packages when an update needs it, like a new kernel package. Plain upgrade holds those back.

Then a few basics:

apt install -y sudo curl ca-certificates nano less

sudo comes first for a reason. Debian’s minimal images don’t include it, but the sudo group exists anyway, so adding a user to it works and prints nothing. If you then disable root login without the sudo command installed, there’s no account left that can fix anything.

Each later part installs what it needs when it needs it.

Name and Time

Two small things. The hostname shows up in the prompt and in every log line, and vps or debian won’t mean anything to me later:

hostnamectl set-hostname my-vps
nano /etc/hosts
127.0.1.1	my-vps

That line in /etc/hosts should match the new name, otherwise sudo complains that it can’t resolve the host.

Some providers set the hostname on every boot with cloud-init. If yours does, this stops it from changing the name back:

echo 'preserve_hostname: true' > /etc/cloud/cloud.cfg.d/99-hostname.cfg

Then the timezone, so log times are in my time and not UTC:

timedatectl set-timezone Asia/Jakarta
timedatectl set-ntp true
timedatectl status
Output
               Local time: Wed 2026-05-20 16:20:33 WIB
           Universal time: Wed 2026-05-20 09:20:33 UTC
                 Time zone: Asia/Jakarta (WIB, +0700)
System clock synchronized: yes
              NTP service: active

I want System clock synchronized: yes. A wrong clock breaks HTTPS certificates in confusing ways. If NTP service says n/a, the image came without a time client and apt install -y systemd-timesyncd fixes it.

SSH Keys

Passwords can be guessed. A key can’t, at least not in any practical sense. So before anything else, I made a key on my laptop (not on the server):

ssh-keygen -t ed25519 -C "vps-personal"
Output
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/you/.ssh/id_ed25519):
Enter passphrase for "/home/you/.ssh/id_ed25519" (empty for no passphrase):
Your identification has been saved in /home/you/.ssh/id_ed25519
Your public key has been saved in /home/you/.ssh/id_ed25519.pub

That gives two files. id_ed25519 is private and never leaves the laptop. id_ed25519.pub is the one I give to servers:

cat ~/.ssh/id_ed25519.pub
Output
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8kR2pQ7mZvN1xW4tYbL9cD0eF3gH5jK7lM2nP4qR6s vps-personal

If what you see starts with -----BEGIN OPENSSH PRIVATE KEY-----, that’s the wrong file.

I went with Ed25519 because the keys are short and fast and every modern OpenSSH supports them. RSA works too, but it has to be at least 2048 bits to be worth anything.

One User for Everything

I don’t want to work as root. I also don’t want a pile of accounts. So the server gets exactly one normal user, and that user will do everything: log in, use sudo, and later own every container.

Back on the server, as root:

useradd --create-home --shell /bin/bash admin
usermod -aG sudo,adm,systemd-journal admin
passwd admin
Output
New password:
Retype new password:
passwd: password updated successfully

The groups:

  • sudo to run admin commands.
  • adm and systemd-journal to read system logs without typing sudo every time.

The password is only for sudo. Logging in will be key-only in a few minutes.

Check that sudo actually works for the new user before relying on it:

su - admin -c 'sudo -v'
Output
[sudo] password for admin:

If it asks for the password and accepts it, good. sudo: command not found means the package is missing, so go back and install it while root still works.

Giving the User Your Key

The public key has to go into ~/.ssh/authorized_keys of the new user. From the laptop:

ssh-copy-id admin@IP_VPS
Output
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
admin@IP_VPS's password:

Number of key(s) added: 1

It logs in with the password one last time and sets the right permissions on the files.

If your provider’s image already has password logins turned off, that won’t work. In that case, copy root’s keys over on the server instead:

install -d -m 700 -o admin -g admin /home/admin/.ssh
install -m 600 -o admin -g admin /root/.ssh/authorized_keys /home/admin/.ssh/authorized_keys

install creates the files with the right owner and mode in one go. A plain cp would leave them owned by root, and SSH silently ignores a key file that the user doesn’t own.

Test it from the laptop:

ssh admin@IP_VPS
Output
Linux my-vps 6.12.48+deb13-amd64 #1 SMP Debian 6.12.48-1 x86_64
admin@my-vps:~$

No password prompt, and the prompt ends in $, not #. That’s a normal user logged in with a key.

Locking Down SSH

Now the actual hardening. Debian’s main config is /etc/ssh/sshd_config, but I didn’t edit it. At the top of that file is this line:

Include /etc/ssh/sshd_config.d/*.conf

sshd uses the first value it finds for each setting, and the included files are read first, in alphabetical order. Lots of VPS images ship a 50-cloud-init.conf in there with PasswordAuthentication yes. Any change you make further down in the main file loses to it.

So I put my settings in a file that sorts before everything else:

sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
Port 22022
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
PubkeyAuthentication yes
AuthenticationMethods publickey
AllowUsers admin

MaxAuthTries 3
MaxSessions 4
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2

PermitUserEnvironment no
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding local
AllowStreamLocalForwarding no
GatewayPorts no
PermitTunnel no
UseDNS no
LogLevel VERBOSE

HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,curve25519-sha256
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com

That’s a lot, so briefly:

  • Port 22022. Not real security, but bots hammer port 22 all day and this cuts the noise in the logs.
  • No root, no passwords, keys only, and only admin. These are the lines that matter most. AllowUsers means even if another account appears on the server someday, it can’t log in over SSH.
  • MaxAuthTries, LoginGraceTime. Give up quickly on connections that aren’t getting anywhere.
  • Forwarding. I turned off everything except AllowTcpForwarding local, so I can still do ssh -L to reach a database on the server from my laptop.
  • LogLevel VERBOSE logs the fingerprint of the key used for each login, so I can see which key got in.
  • Crypto. Only modern key exchange, ciphers and MACs. The first key exchange, mlkem768x25519-sha256, is post-quantum and new in OpenSSH 10, which Debian 13 ships. I dropped the ECDSA host key and kept Ed25519 plus RSA for older clients.

If you’re on a different OpenSSH version, check what it supports before copying those last lines:

ssh -Q kex

Before restarting anything, test the file. A syntax error here and SSH won’t come back:

sudo sshd -t

No output means it’s fine. Then ask sshd what it will actually use, after all the included files:

sudo sshd -T | grep -Ei '^(port|permitrootlogin|passwordauthentication|allowusers) '
Output
port 22022
permitrootlogin no
passwordauthentication no
allowusers admin

That’s the effective config, and it’s the only view of it I trust. If passwordauthentication says yes here, some other file is winning.

The Port Has to Change in Two Places

This one got me. Debian 13 can start SSH through a systemd socket, and when it does, systemd opens the port, not sshd. The Port line above gets ignored. Mine was set up that way:

systemctl is-enabled ssh.socket
Output
enabled

If you see enabled too, tell the socket about the new port:

sudo mkdir -p /etc/systemd/system/ssh.socket.d
sudo nano /etc/systemd/system/ssh.socket.d/port.conf
[Socket]
ListenStream=
ListenStream=22022

The empty ListenStream= clears the default port 22. Without it, it listens on both.

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket

If it said disabled, sshd opens the port itself and restarting it is enough:

sudo systemctl restart ssh

Either way, check what’s listening:

sudo ss -tlnp | grep 22022
Output
LISTEN 0  4096   0.0.0.0:22022  0.0.0.0:*  users:(("systemd",pid=1,fd=52))
LISTEN 0  4096      [::]:22022     [::]:*  users:(("systemd",pid=1,fd=53))

Test Before You Close Anything

Don’t close the session you’re in. Open a second terminal on the laptop and connect on the new port:

ssh -p 22022 admin@IP_VPS

If that works, you’re done. If it doesn’t, the first session is still open and you can fix it from there. If you close it and the new config is broken, the only way back in is your provider’s web console.

And to confirm root really is locked out:

ssh -p 22022 root@IP_VPS
Output
root@IP_VPS: Permission denied (publickey).

Making the Connection Shorter

Typing ssh -p 22022 admin@IP_VPS every time gets old fast. SSH has a config file on the laptop side for this:

nano ~/.ssh/config
Host vps
	HostName IP_VPS
	User admin
	Port 22022
	IdentityFile ~/.ssh/id_ed25519
	IdentitiesOnly yes
chmod 600 ~/.ssh/config
ssh vps

IdentitiesOnly yes makes SSH offer only that one key. Without it, it tries every key in your agent, and with MaxAuthTries 3 on the server you can get rejected before it reaches the right one.

The nice part is that everything else that uses SSH reads this file too. scp file vps:~/ and rsync ./dist/ vps:~/site/ just work, and so does VS Code’s Remote-SSH.

Next Part

So far: the system is updated, there’s one user with a key and sudo, root can’t log in, and SSH only accepts that one user’s key on a non-default port.

That only covers the login. Every other port is still open, nothing watches for repeated failures, and updates only happen when I remember. Part 2 deals with that: a firewall, fail2ban, automatic updates, and some tuning.