Secure SSH on a fresh server

Ten minutes after delivery, before anything else: a personal account, key-only login, a non-default port, a firewall, and a ban on brute force.

beginner~10 min hands-on
#ssh#linux#security#example

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 gistBefore and after, in one picture
Your server
ssh -p 22, refusedban
Your laptop~/.ssh/id_ed25519
The internet's botsroot / password…
Firewall/tcp only
sshdkeys only · no root
fail2ban3 failures → 1h ban
Your account · sudo

Only your key reaches sshd, on your port, through the firewall. Root and passwords are refused; with fail2ban, repeat offenders are banned before they even reach sshd.

A server straight from the provider answers to root with a password, on port 22, to the whole internet. Within the hour, bots are trying passwords. This page closes those doors in five steps, each one verified before the next.

Before you start

will ask you something
$ssh-keygen -t ed25519 -C "@laptop"
Check
$cat ~/.ssh/id_ed25519.pub
Expected output
ssh-ed25519 AAAA… @laptop
WhatBeforeAfter this page
Who can log inroot, anyone with the password, with a key
Port22
Wrong passwordsunlimited3, then banned for an hour

Create your own user

$ssh root@
will ask you something
$adduser
$usermod -aG sudo
$rsync --archive --chown=: ~/.ssh /home/
Check
$ssh @ sudo -n true && echo OK
Expected output
OK
If sudo asks for a password

Debian and Ubuntu grant password-less sudo to nobody by default; sudo -n then fails. Either type the password (fine), or allow the group without password:

$echo '%sudo ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/90-sudo-nopasswd

Do this only if the account is protected by a key, which is what the next step enforces.

Harden the SSH daemon

/etc/ssh/sshd_config.d/00-hardening.conf
Port
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
Check
$sudo sshd -t && echo syntax OK
Expected output
syntax OK

Firewall

Sensitive command — may cut your own access (firewall). Review before running.
$sudo apt install -y ufw
$sudo ufw allow /tcp
$sudo ufw enable
Check
$sudo ufw status
Expected output
Status: active
/tcp                  ALLOW       Anywhere

Now restart SSH and, from a second terminal, log in on the new port:

$sudo systemctl restart ssh
Check
$ssh -p  @ echo connected
Expected output
connected
If the new connection fails

Your first terminal is still logged in: nothing is lost. In order of likelihood:

  • The firewall does not list : re-run the firewall step.
  • sshd -t reports an error: fix the drop-in, restart again.
  • The provider has its own network firewall in front of the server (common at OVH, Hetzner, cloud providers): open the port there too.
  • You are testing from the first terminal by mistake: use a new one.

Ban brute force

$sudo apt install -y fail2ban
/etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
port =
maxretry = 3
bantime = 1h
$sudo systemctl enable --now fail2ban
Check
$sudo fail2ban-client status sshd | head -3
Expected output
Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
If you ban yourself

Three typos in a row from your own address and you are out for an hour. From another address (phone hotspot, the provider's console), unban yourself:

$sudo fail2ban-client set sshd unbanip

To never ban your office or home, add ignoreip = 203.0.113.0/24 to the jail.

Done

Root is locked out, passwords are refused, the port is off the beaten path, and repeat offenders are banned. From now on you connect with:

$ssh -p @

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.