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.
- Part 1: Securing the Base System
- Part 2: Firewall, Bans & Updates
- Part 3: Rootless Podman
- Part 4: Traefik & HTTPS
- Part 5: Deploying, and One Script for All of It
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_VPSThe 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 -yHit: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 lesssudo 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/hosts127.0.1.1 my-vpsThat 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.cfgThen the timezone, so log times are in my time and not UTC:
timedatectl set-timezone Asia/Jakarta
timedatectl set-ntp true
timedatectl status 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: activeI 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"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.pubThat 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.pubssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8kR2pQ7mZvN1xW4tYbL9cD0eF3gH5jK7lM2nP4qR6s vps-personalIf 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 adminNew password:
Retype new password:
passwd: password updated successfullyThe groups:
sudoto run admin commands.admandsystemd-journalto read system logs without typingsudoevery 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'[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/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: 1It 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_keysinstall 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_VPSLinux 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/*.confsshd 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.confPort 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.comThat’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.AllowUsersmeans 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 dossh -Lto reach a database on the server from my laptop. LogLevel VERBOSElogs 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 kexBefore restarting anything, test the file. A syntax error here and SSH won’t come back:
sudo sshd -tNo 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) 'port 22022
permitrootlogin no
passwordauthentication no
allowusers adminThat’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.socketenabledIf 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=22022The empty ListenStream= clears the default port 22. Without it, it listens on both.
sudo systemctl daemon-reload
sudo systemctl restart ssh.socketIf it said disabled, sshd opens the port itself and restarting it is enough:
sudo systemctl restart sshEither way, check what’s listening:
sudo ss -tlnp | grep 22022LISTEN 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_VPSIf 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_VPSroot@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/configHost vps
HostName IP_VPS
User admin
Port 22022
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yeschmod 600 ~/.ssh/config
ssh vpsIdentitiesOnly 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.