Secure the server in the first hour

From OVH's default `debian` login to your own admin account with a key and a sudo password, SSH on its own port with passwords and root refused, a firewall, bans on brute force, and security updates that install themselves. Every lock is tested before the old door closes.

intermediate~30 min hands-on
#ssh#ufw#fail2ban#unattended-upgrades#debian#ovh#security

Not validated end to end yet — be the first.Report a problem

Draft — not yet run end to end. This page was written but its author has not yet run it on a real machine. Commands may be wrong: read before you run, and tell us what breaks.

The gistOne way in, tested before the old one closes
Your VPS
ssh vps22, refusedkey → session
Your laptopssh vps
The internet's botsroot / passwords on 22
Firewall/tcp only
sshdkeys only · no root
Your account · sudo

Only your key reaches sshd, on port ${SSH_PORT}, through the firewall, and it opens a session as ${USERNAME}. Bots on port 22 find a closed door; the KVM console stays as the way back in.

The VPS answers on port 22 to the whole internet, as debian, with password-less sudo. This page moves you to your own account, puts SSH on its own port with keys only, turns on a firewall, bans brute force and makes security updates install themselves. The order matters: every new door is tested from a second terminal before the old one closes.

Before you start

Check that the delivered login works and that debian has sudo without a password, then log in as debian in a first terminal and stay logged in: a second terminal on your laptop tests each change from the outside.

Two terminals, and the badge on each block says which one:

  • Terminal A, badge server · debian: your first session, opened now. It is your lifeline: do not close it until this page tells you to. Every sudo command up to the switch-over runs here.
  • Terminal B, badge Mac: a terminal on your laptop, for the tests from outside.

takes over only once the new door is tested (a third session, ssh vps). The server key is the one made on page 2, ~/.ssh/id_ed25519_vps.

Check
$ssh -i ~/.ssh/id_ed25519_vps -o IdentitiesOnly=yes debian@ sudo -n true && echo OK
Expected output
OK

Update everything

Upgrade, then reboot if Debian left the flag file that says an update needs it (a new kernel, usually), and log in again as debian:

Server·debian
Sensitive command — reboots or powers off the machine. Review before running.
$sudo apt update && sudo apt full-upgrade -y
$sudo apt install -y vim
$[ -f /var/run/reboot-required ] && sudo reboot

Name and clock

Give the server its name, keep cloud-init from putting OVH's name back, and set the time zone. timedatectl must then show your zone and System clock synchronized: yes.

Server·debian
$sudo hostnamectl set-hostname
$printf 'preserve_hostname: true\nmanage_etc_hosts: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-hostname.cfg
$sudo sed -i '/^127\.0\.1\.1[[:space:]]/d' /etc/hosts
$echo "127.0.1.1 " | sudo tee -a /etc/hosts
$sudo timedatectl set-timezone
$timedatectl

Your own user

Create , in terminal A. adduser asks for the password twice: type the one saved in your password manager. Paste this command alone:

Server·debianwill ask you something
$sudo adduser --gecos ""

Then put it in sudo and sshusers, and write your public key (the value from the panel, not a copy of debian's file) into its authorized_keys:

Server·debian
$sudo groupadd -f sshusers
$sudo usermod -aG sudo,sshusers
$sudo install -d -m 700 -o -g /home//.ssh
$echo '' | sudo tee /home//.ssh/authorized_keys >/dev/null
$sudo chown : /home//.ssh/authorized_keys
$sudo chmod 600 /home//.ssh/authorized_keys

Check the key before going on: the file must hold a valid Ed25519 key.

Check
$sudo ssh-keygen -lf /home//.ssh/authorized_keys | grep -o '(ED25519)'
Expected output
(ED25519)

From terminal B (Mac), log in as the new user with the key only (still on port 22) and read its groups. The options forbid the password: if this passes, the key works.

Check
$ssh -i ~/.ssh/id_ed25519_vps -o IdentitiesOnly=yes -o PasswordAuthentication=no -o BatchMode=yes @ 'id -nG | grep -qw sudo && id -nG | grep -qw sshusers && echo groups OK'
Expected output
groups OK

Harden SSH

Write the drop-in: copy the command and paste it in terminal A.

Server·debianwrites a file/etc/ssh/sshd_config.d/00-hardening.conf
Port
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowGroups sshusers

Test the syntax, then read the effective values. sshd -t prints nothing when the syntax is fine; the second command must print exactly one port line with , no for the three next settings, and allowgroups sshusers.

Server·debian
$sudo sshd -t
$sudo sshd -T | grep -Ei '^(port|permitrootlogin|passwordauthentication|kbdinteractiveauthentication|allowgroups) '

Firewall and switch-over

Install ufw, refuse everything inbound, and open the new port and 22 for now. ufw enable warns that it may disrupt SSH connections; answer y. 22 stays open so the session you are typing in survives, and so a rollback (delete the drop-in, restart) brings you back to a port the firewall accepts.

Server·debian
Sensitive command — may cut your own access (firewall). Review before running.
$sudo apt install -y ufw
$sudo ufw default deny incoming && sudo ufw default allow outgoing
$sudo ufw allow /tcp && sudo ufw allow 22/tcp
$sudo ufw enable

Before the restart, give your laptop the vps shortcut, with the server key, in terminal B. If ~/.ssh/config already has a Host vps block, do not add a second one: edit it with ${EDITOR} ~/.ssh/config so it has the same lines (ssh uses the first match).

Mac
$touch ~/.ssh/config && chmod 600 ~/.ssh/config
$grep -q '^Host vps$' ~/.ssh/config || printf '\nHost vps\n HostName \n Port \n User \n IdentityFile ~/.ssh/id_ed25519_vps\n IdentitiesOnly yes\n AddKeysToAgent yes\n UseKeychain yes\n' >> ~/.ssh/config
Server·debian
$sudo systemctl restart ssh

In terminal B, test the new door with the shortcut. ssh asks again to confirm the host key: it is the same key, stored under a new name ([IP]:port), so answer yes.

Check
$ssh -o BatchMode=yes vps echo connected
Expected output
connected
If the new connection fails

Your first terminal is still logged in: nothing is lost. Look, in this order:

  • sudo sshd -T | grep '^port ': must say .
  • sudo ss -tlnp | grep sshd: sshd must listen on , for IPv4 (0.0.0.0) and IPv6 ([::]).
  • sudo ufw status: /tcp must be ALLOW.
  • systemctl is-active ssh.socket: if it says active, the socket unit holds the port, not sshd; run sudo systemctl daemon-reload && sudo systemctl restart ssh.socket. Debian's default is ssh.service (Ubuntu uses the socket): confirm with systemctl is-enabled ssh.socket ssh.service.
  • OVH's Network Firewall (control panel → your IP → Network Firewall), if you enabled it: it filters before the packets reach the VPS; add a rule for .
  • To roll back in ten seconds: sudo rm /etc/ssh/sshd_config.d/00-hardening.conf && sudo systemctl restart ssh. You are back on port 22, which is still open.
  • First session gone too: log in on the KVM console as with your password and run the same commands.

The new port works: close 22, in terminal A.

Server·debian
$sudo ufw delete allow 22/tcp

Retire the debian account

Open your own session from the Mac and check that sudo accepts your password:

Mac
$ssh vps
Server·will ask you something
$sudo true && echo SUDO OK

Once SUDO OK came back, close terminal A (exit). Then, in the ssh vps session. userdel prints mail spool (/var/mail/debian) not found: that is normal.

Server·
Sensitive command — recursive or forced deletion (rm). Review before running.
$sudo pkill -KILL -u debian
$sudo userdel -r debian
$sudo rm -f /etc/sudoers.d/90-cloud-init-users

From your laptop, check that debian is refused; the answer must end with Permission denied (publickey).

Mac
$ssh -p -o BatchMode=yes debian@ true

Ban brute force

With passwords refused, guessing is useless, but bots still knock. fail2ban reads the SSH log and bans an address after three failures.

Server·
$sudo apt install -y fail2ban python3-systemd

Then the jail, in one file:

Server·writes a file/etc/fail2ban/jail.d/sshd.local
[DEFAULT]
bantime.increment = true
[sshd]
enabled = true
port =
backend = systemd
maxretry = 3
findtime = 10m
bantime = 1h
Server·
$sudo systemctl enable fail2ban && sudo systemctl restart fail2ban
Check
$sudo fail2ban-client status sshd | head -1
Expected output
Status for the jail: sshd
If you ban yourself

Three typos from your own address and you are out for an hour. To never ban a fixed home address, add ignoreip = 127.0.0.1/8 ::1 followed by your address () under [DEFAULT] with sudo ${EDITOR} /etc/fail2ban/jail.d/sshd.local, and restart fail2ban. To unban, from another connection (phone hotspot) or the KVM console:

Server·
$sudo fail2ban-client set sshd unbanip

Automatic security updates

Server·
$sudo apt install -y unattended-upgrades apt-listchanges
$printf 'APT::Periodic::Update-Package-Lists "1";\nAPT::Periodic::Unattended-Upgrade "1";\n' | sudo tee /etc/apt/apt.conf.d/20auto-upgrades

Allow a reboot at when an update needs one:

Server·writes a file/etc/apt/apt.conf.d/52unattended-upgrades-local
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "";
Check
$apt-config shell UU APT::Periodic::Unattended-Upgrade
Expected output
UU='1'

Check IPv6

The next page publishes the IPv6 address in DNS; a dead one sends IPv6 visitors into timeouts. On the server, the global address must be and must reach the internet:

Server·
$ip -6 addr show scope global
$ping -6 -c 3 2001:4860:4860::8888

Note the result: if ping -6 answers, page 4 publishes the IPv6 address (AAAA record); if not, it publishes none.

Snapshot the clean state (optional, paid)

In the OVH control panel, open your VPS: on its home tab, the Snapshot option offers to order it; once active, take a snapshot from the same place. Without the option, you still have the included automated backup (one day of retention). Menu labels move: OVH's guide "Using snapshots on a VPS" on docs.ovhcloud.com has the current path.

Done

WhatBeforeAfter this page
Who logs indebian, sudo without password, key only, sudo with password
SSH port22
Root, passwordsroot off, passwords possibly on (cloud-init)both refused by your own drop-in
Inbound trafficeverything/tcp only
Brute forceunlimited tries3 tries, then banned
Security updatesby handdaily, automatic, reboot at

From now on, you connect with ssh vps. Next page: Point the domain at the server (Infomaniak DNS). It publishes and in the Infomaniak DNS zone of , so the name reaches this machine before Caddy asks for a certificate.

Did everything work?

If you followed this page to the end on a real machine, say so. Your validation is dated and records your stack, so the next reader on the same path knows it still works.

This copy is read-only. To report that it works, or that it does not, open an issue

Only your stack choices are recorded, never your values. The pseudonym stays on this browser.