What you will set up
A new server is found within hours. Our test server logged its first password guess less than three hours after it came online, and 1,636 in its first day, from 11 addresses. Most of them tried to log in as root.
These steps take about 15 minutes, and every other guide here builds on a server set up like this:
- you log in with a key instead of a password,
- the server stops accepting passwords over SSH, so guessing one gets nobody in,
- security updates install themselves every day, and
- a firewall, ufw, keeps every port closed that you have not opened.
Every step below was run on a fresh Melonslab server with Debian 13, and checked again after a reboot.
Before you start
You need a server with Debian 13 and its root password. If you have not logged in to it yet, the connect guide shows how. The examples use 203.0.113.10 for your server's IPv4 address. Replace it with your own throughout.
If something goes wrong, the console gets you back in: in the client area, open your server and choose Open console on the Server tab. It shows the server's screen as if you were standing at it, and works without SSH or the network.
1. Log in with a key
On your own computer, in a terminal (on Windows, in PowerShell), create a key:
ssh-keygen -t ed25519
Press Enter to keep the suggested file, and choose a passphrase: it protects the key if someone copies it from your computer. This makes two files: id_ed25519 is the private key, which never leaves your computer, and id_ed25519.pub is the public key, which goes on the server.
Copy the public key to the server. This is the last time you type the server's root password:
ssh-copy-id root@203.0.113.10
Windows has no ssh-copy-id. In PowerShell, run this instead:
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh root@203.0.113.10 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
Now log in:
ssh root@203.0.113.10
If it asks for the key's passphrase and not the server's password, the key works.
2. Turn off password logins
Keep this session open until the end of the step. If a change goes wrong, you can still undo it here.
Create /etc/ssh/sshd_config.d/10-keys-only.conf:
# Log in with keys only. Named 10- so it is read before 50-cloud-init.conf,
# whose PasswordAuthentication yes would otherwise win.
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
The folder already has a file, 50-cloud-init.conf, that turns password logins on. SSH reads the files in name order and keeps the first value it finds for each setting, so a name starting with 10 comes first and wins. PermitRootLogin prohibit-password still lets root in with a key.
Check the configuration and apply it:
sshd -t && systemctl reload ssh
sshd -t only prints something if there is a mistake, and SSH is only reloaded when there is none. To see the settings SSH now uses:
sshd -T | grep -E '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin) '
permitrootlogin without-password
passwordauthentication no
kbdinteractiveauthentication no
without-password is an older name for prohibit-password. Now open a second terminal and log in again with ssh root@203.0.113.10. When that works, you can close the first session.
To check that passwords are refused, ask SSH not to use your key:
ssh -o PubkeyAuthentication=no root@203.0.113.10
It answers Permission denied (publickey). without asking for a password.
You do not need Fail2ban for SSH now. Guesses can no longer succeed, they only fill the log.
3. Install security updates automatically
apt update
apt install -y unattended-upgrades curl
This also installs curl, which many of our guides use to download files and test web servers: a fresh server does not have it. Installing unattended-upgrades does not switch it on. Create /etc/apt/apt.conf.d/20auto-upgrades:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
The server now fetches the package lists and installs updates from Debian and Debian's security team once a day, in the morning. To see what it would do, without installing anything:
unattended-upgrade --dry-run --debug 2>&1 | grep -E 'Allowed origins|No packages'
The first line lists label=Debian-Security among the sources it installs from. systemctl list-timers apt-daily-upgrade.timer shows when it runs next.
A new kernel only takes effect after a restart. To let the server restart by itself at 04:00 when an update needs it, create /etc/apt/apt.conf.d/52unattended-upgrades-local:
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Apps set up as in our other guides start again by themselves after a restart. If you would rather restart yourself, leave this file out: the file /run/reboot-required appears when an update is waiting for a restart.
4. Open only the ports you use
Debian starts without a firewall, so every program that listens on a port can be reached from the internet. ufw, short for "uncomplicated firewall", blocks everything that comes in except what you allow, for both IPv4 and IPv6.
Install it, and allow SSH before you switch it on, or you lock yourself out:
apt install -y ufw
ufw allow 22/tcp
If the server will serve websites, allow HTTP and HTTPS too. HTTPS uses UDP as well as TCP for HTTP/3:
ufw allow 80,443/tcp
ufw allow 443/udp
Then switch it on. It warns that it may disrupt SSH connections: answer y. The rule for port 22 keeps yours open.
ufw enable
ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
80,443/tcp ALLOW IN Anywhere
443/udp ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)
80,443/tcp (v6) ALLOW IN Anywhere (v6)
443/udp (v6) ALLOW IN Anywhere (v6)
ufw starts again after a reboot with the same rules. To remove one, give the same rule after ufw delete, for example ufw delete allow 443/udp.
Rules for our other guides
| Guide | Run |
|---|---|
| WireGuard VPN | ufw allow 51820/udp and ufw route allow in on wg0 out on eth0 |
| Unbound DNS, for VPN devices | ufw allow in on wg0 |
| Matrix calls | ufw allow 7881/tcp and ufw allow 7882/udp, and for coturn ufw allow 3478 and ufw allow 49152:65535/udp |
| Stalwart mail server | ufw allow 25,465,993,995/tcp |
| Forgejo Git server | ufw allow 2222/tcp |
| Headscale | ufw allow 3478/udp |
| Zabbix | ufw allow from 198.51.100.30 to any port 10051 proto tcp, once for each server you watch |
| FreeRADIUS | ufw allow from 198.51.100.20 to any port 1812:1813 proto udp, with your office's address |
| Postfix mail server | ufw allow 25,80,587,993/tcp, with 80 for certificate renewal |
| Minecraft server | ufw allow 25565/tcp |
| Asterisk | ufw allow 5061/tcp and ufw allow 10000:20000/udp |
| Pelican and Pterodactyl | ufw allow 8080,2022/tcp, for Wings |
| Jitsi Meet | ufw allow 10000/udp, ufw allow 3478/udp and ufw allow 5349/tcp |
| AdGuard Home | ufw allow 853/tcp and ufw allow in on wg0 |
The WireGuard rules are two: one lets devices connect, and the route rule lets their traffic pass through the server to the internet, which ufw blocks by default.
Rootless Podman containers, as in the Podman guide, follow the same rules as any other program: a port they publish to the internet stays closed until you allow it. Ports published only on 127.0.0.1, as our app guides do behind Caddy, are never reachable from outside.
5. Check what is listening
ss -tulpn
This lists every program that listens on a port. A local address of 0.0.0.0, [::] or * means every address, which ufw keeps closed unless you allowed the port. 127.0.0.1 and [::1] can only be reached from the server itself. If you find something you do not use, remove it rather than leaving it to the firewall.
Troubleshooting
You are locked out. Choose Open console in the client area and log in as root with the password. From there, ufw disable switches off the firewall, and deleting /etc/ssh/sshd_config.d/10-keys-only.conf and running systemctl reload ssh turns password logins back on. If you have lost the root password too, the Root password section on the same tab sets a new one.
Permission denied (publickey) from your own computer. The server did not accept your key. Check that you log in as root from the computer where you made the key in step 1. If you use another computer, add its public key to /root/.ssh/authorized_keys from a session that works, or from the console.
Devices on the VPN lose the internet after ufw enable. The route rule for WireGuard is missing. If they lose DNS but not the internet, the rule for Unbound is. Both are in the table above.
A website or app stops answering. Its port is not allowed. ss -tulpn shows which port it listens on.