Securing a new Linux server: what attackers try first

The first things that hit a fresh server on the internet, and the handful of changes that stop most of them.

Security and DDoS22 min readUpdated

What arrivesfrom the internetNetwork filterupstreamfloodsof trafficFirewall:default denyports younever openedfail2banslows repeatguessersSSH: keysonlypasswordguessingUpdatesbugs thathave a fixYourserverA flood of junk trafficA knock on port 5432root / 123456, againslowedAn exploit for old sshdYou, with your keyWhat eachlayer stopsWatchingss shows what is listening, the journal who triedRecoveringa backup you have restored at least onceFrom the internet, outermost layer firstNetwork filter (upstream)Stops floods of trafficA flood of junk trafficFirewall: default denyStops ports you never openedA knock on port 5432fail2banSlows addresses that keep failingroot / 123456, slowedSSH: keys onlyStops password guessingroot / 123456, refusedUpdatesStops bugs that already have a fixAn exploit for old sshdYour serverYou, with your key: let inWatchingss: what listens. The journal: who tried.RecoveringA backup you have restored once

In short

  • Attacks on a new server are mostly automated and dull: programs try usernames such as root and admin with passwords such as 123456 against SSH, and scan for any other service that answers.
  • Logging in with a key and turning SSH passwords off ends the guessing outright, because the server stops asking for anything that can be guessed. fail2ban and rate limits only slow it down.
  • A firewall whose default is to drop keeps services you never meant to publish off the internet, and updates fix the bugs that no setting can, including bugs in the SSH server itself.
  • Make the changes in an order that always leaves a way back in: your own user and key first, sshd checked with sshd -t, the SSH rule added before the firewall goes on, and the provider console open in a tab.
On this page
  1. What arrives first: robots with a word list
  2. Your own user, and a key instead of a password
  3. Telling sshd to stop asking for passwords
  4. A firewall that says no unless you said yes
  5. fail2ban and rate limits: slower guessing, not an end to it
  6. Updates: the only fix for a bug in the lock itself
  7. What is listening, and who has been knocking
  8. Beliefs that leave the door open
  9. The order that does not lock you out
  10. Questions
  11. Sources

Put a server on the internet and something that is not you will try to log in to it. It will not be a person, and it will not know anything about you. It will be a program working through a list: the username root with the password 123456, then the next pair, then the next, on port 22 of your address and of every other address it can reach.

That is good news. A robot that tries the same things everywhere is beaten by the same few changes everywhere. Each step in this guide takes away one thing it needs: a password to guess, a root account to guess it for, a port to reach, the time to keep trying, or a bug that has already been fixed. Knowing which step removes which thing lets you ignore the folklore, and doing them in the right order keeps you from locking yourself out halfway.

What arrives first: robots with a word list

The SANS Internet Storm Center collects login attempts from honeypots: machines that offer SSH and Telnet, hold nothing of value, and record everything that tries to get in. Volunteers run the sensors, and logs from Cowrie, an SSH and Telnet honeypot, are accepted too.1 Nobody has a reason to target these machines in particular, so what they record is the background every server on the internet receives, whether it is a production VPS or a staging copy someone set up on a Friday.

On 28 September 2026, the day's running totals showed 420,479 attempts to log in as root, more than four times the next name, admin, at 94,904. Then came ubuntu, user, system, enable, deploy, test and guest: names that exist on many servers by default or by habit. The most-tried password was 123456, ahead of strings such as 1234, 123, Password and admin.1 The totals climb through the day, but in two snapshots taken hours apart, root, admin and ubuntu led both times.

Attempts at the username root on one day, SANS ISC sensors (28 September 2026, running total)
420,479
How far root led the next name, admin (420,479 / 94,904)
4.4x
The most-tried password the same day
123456

MITRE ATT&CK, a public catalogue of attacker techniques, files this under password guessing: an attacker with no valid credentials trying passwords systematically. SSH on TCP port 22 is the first entry in its list of commonly targeted services, followed by Telnet, FTP, SMB, RDP and others, including databases such as MSSQL, Oracle and MySQL on 3306.2

Guessing is the loudest thing that arrives, not the only one. The same robots look for any other port that answers, such as a database, a cache or a forgotten test page, and some traffic carries exploits for known bugs in whatever is listening, which work however good your password is. Guessing, exposure and old bugs are stopped by different layers, which is why the drawing at the top has more than one.

Your own user, and a key instead of a password

The commands in this guide are for Debian and Ubuntu; other distributions have the same pieces under slightly different names. Before you change anything, open your provider's browser console in a tab and keep this SSH session open until a fresh login works; the full lock-out-safe order is at the end of the guide. Start with an account of your own. On a default Ubuntu install, root has a password hash that matches no possible value, so root cannot log in directly, and administrators work through sudo instead, which leaves a record of who did what.12 If your server came with a root password or a root key instead, the first job is to make the account you will use from now on. On Debian and Ubuntu, adduser creates it, and adding it to the sudo group gives it full root access through sudo.12 Debian's installer adds the sudo package when it is given no root password,24 so a server set up with one may not have it: install it first.

apt install sudoadduser aliceadduser alice sudo

Give the account a strong password even though SSH is about to stop accepting passwords. sudo will ask for it, and so will the provider's console on the day you need to log in without the network.

How a key login works

A password is a secret you send so the server can compare it with the one it stored. Anything that is sent can be guessed and sent again, which is all the robots do. SSH's public key method never sends the secret. The SSH standard makes holding the private key the proof: the client sends a signature made with its private key, and the server checks both that the key is allowed for that user and that the signature is valid.3 The private key stays on your computer. The server keeps only the public half, in the account's ~/.ssh/authorized_keys file, one key per line.6

For the robot, that removes the target. With passwords off there is no prompt to answer, so a list of every password ever leaked is worth nothing. It also covers the accounts you forgot about: the test user a colleague made with a weak password is as safe as root, because no account accepts a password over SSH at all.

Making a key and installing it takes two commands on your own computer. ssh-keygen with no arguments generates an Ed25519 key,7 the type Ubuntu's documentation recommends for its shorter keys and lower computational cost,11 and ssh-copy-id puts your public key into the account on the server so that the server will accept it.8Give the key a passphrase when ssh-keygen asks. The private key is the one copy of your identity, and a passphrase means a stolen laptop does not hand it over.

ssh-keygen -t ed25519ssh-copy-id alice@203.0.113.10ssh alice@203.0.113.10

ssh-copy-id logs in with alice's password to do its work, so password logins have to still be on when you run it.8 If the server already accepts only keys, because you logged in as root with one, copy root's key file to the new account instead, as root on the server:

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

The last command should log you in without the account's password; a passphrase prompt, if any, is your key's own and is checked on your machine. Once in, sudo whoami should print root, and from then on root is not needed for logging in.

Telling sshd to stop asking for passwords

Having a key does not turn passwords off. Until the server's configuration changes, the robots can still try, and every account with a password is still a candidate. Four settings in sshd_config decide what they can try, and you change three of them.

SettingOpenSSH defaultSet it toWhat that takes away
PasswordAuthenticationyesnoPassword logins, for every account
KbdInteractiveAuthenticationyesnoThe other way sshd can prompt for a secret, often the same password
PermitRootLoginprohibit-passwordnoLogging in as root at all, even with a key
PubkeyAuthenticationyesyesNothing: this is the way in you keep

The defaults in the table come from the sshd_config manual.4 Keyboard-interactive is the one people miss: a separate way for sshd to ask for a secret, often the account password, so turn both off. And a server image or an installer can change any default, so check the value in force rather than the one in the manual.

Why an edit sometimes does nothing

The most common surprise has a simple cause. For each keyword, sshd uses the first value it reads and ignores any later one.4 On Ubuntu the first line of sshd_config is an Include for every file in /etc/ssh/sshd_config.d/, so a setting in one of those files beats the same setting further down the main file,11 and the included files are read in the order their names sort.4 A line added at the bottom of sshd_config loses to any snippet that set the same keyword first.

A line added at the bottomreading order/etc/ssh/sshd_configInclude sshd_config.d/*.confsshd_config.d/50-image.confPasswordAuthentication yeskept... the rest of the file ...PasswordAuthentication noignoredEffective value: yesPasswords still workA snippet of your own, read firstreading order/etc/ssh/sshd_configInclude sshd_config.d/*.confsshd_config.d/10-hardening.confPasswordAuthentication nokeptKbdInteractiveAuthentication nokeptsshd_config.d/50-image.confPasswordAuthentication yesignoredEffective value: noConfirm it with sudo sshd -T
A line added at the bottomreading order/etc/ssh/sshd_configInclude sshd_config.d/*.confsshd_config.d/50-image.confPasswordAuthentication yeskept... the rest of the file ...PasswordAuthentication noignoredEffective value: yesPasswords still workA snippet of your own, read firstreading order/etc/ssh/sshd_configInclude sshd_config.d/*.confsshd_config.d/10-hardening.confPasswordAuthentication nokeptKbdInteractiveAuthentication nokeptsshd_config.d/50-image.confPasswordAuthentication yesignoredEffective value: noConfirm it with sudo sshd -T
sshd keeps the first value it reads. Left: a snippet shipped with the server image says yes before the main file's own line is reached, so passwords still work. Right: a snippet of your own, named to sort first, wins. Illustration; 50-image.conf is an example name.

So put your settings in a file of your own in that directory, named to sort before anything already there, and let sshd tell you what it will really do. sshd -t checks the configuration file and keys without starting anything,6 and Ubuntu's documentation puts it before every restart, since losing SSH can mean losing your way to the server.11 sshd -T goes further and prints the effective configuration: every keyword with the value that won.6

PasswordAuthentication noKbdInteractiveAuthentication noPermitRootLogin no
sudo sshd -tsudo sshd -T | grep -E '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|pubkeyauthentication) 'permitrootlogin nopubkeyauthentication yespasswordauthentication nokbdinteractiveauthentication nosudo systemctl restart ssh

No output from sshd -t means no errors. Restarting is safe for the session you are typing in: sshd forks a separate process for each connection,6 so yours carries on while the listener restarts with the new settings. Before you close it, open a second terminal and log in fresh. If that works, the change is good. If it does not, you still have the first session to put things back.

Other settings that shape what a guesser gets

MaxAuthTries allows 6 authentication attempts per connection by default and starts logging failures once half of that is used. LoginGraceTime drops a connection that has not logged in within 120 seconds. AllowUsers, when set, lets only the named users log in at all, whatever else they present.4 OpenSSH 9.8 added PerSourcePenalties, on by default, which makes sshd refuse connections for a while from addresses that keep failing to authenticate;9 Debian 13's manual gives 5 seconds as the default penalty for a client that disconnects after failing.5Run sudo sshd -T | grep persourcepenalties to see whether your version has it. An empty result means it does not.

A firewall that says no unless you said yes

sshd can only look after SSH. Anything else listening on a public address meets the same robots, and plenty of software was written for a trusted network: a database that answers anyone on the network, a cache with no password, a dashboard started for a test. A host firewall deals with every port at once. In nftables, each base chain has a default policy, accept or drop, which decides what happens to a packet that no rule matched.13Set the input policy to drop and a port is closed unless a rule opens it. The list of what the internet can reach becomes the list you wrote, not the list of whatever happens to be running today. One common exception: ports published by Docker are routed before they reach the chains ufw manages, so a container published on 0.0.0.0 is reachable whatever ufw says.27 Publish such ports on 127.0.0.1, and check with ss.

The nftables wiki's example ruleset for a server shows the whole pattern in a few lines.14Its input chain drops by default. It accepts packets that belong to connections already established, or related to them, and drops invalid ones. It accepts everything on the loopback interface, then opens the few ports the server offers, 22, 80 and 443 in the example. The output chain is left undefined, so outgoing traffic is allowed. The established rule is why a default-deny server can still install updates: the mirror's reply belongs to a connection the server opened.

Reply to a connectionthe server startedFrom the server itself(loopback)New connection toport 22, 80 or 443Nothing matched:the default policyReply froma mirror(apt update)acceptNew:port 22(SSH)acceptNew:port 443(HTTPS)acceptNew: 5432from outside(database)dropYour appto 5432(same server)acceptno match, so on to the next rule
A reply toa connection theserver startedFrom the serveritself (loopback)New connection to22, 80 or 443Nothing matched:default policyReplyfrom amirroracceptNew:port 22acceptNew:port 5432outsidedropno match, so on to the next ruleThe first rule that matches decides.
Example packets meet a default-deny input chain. Each is checked against the rules from the top and the first match decides. A new connection to a port nobody opened, such as a database on 5432, matches nothing and gets the policy: drop. Illustration, following the nftables wiki's server ruleset.

ufw: the same pattern with less typing

ufw is Ubuntu's default firewall configuration tool, and it starts disabled.15It keeps one default policy per direction, and on installation the incoming policy is deny and the outgoing one is allow,16 so switching it on with a handful of allow rules gives you the pattern above. The trap is in the switching on. Turning ufw on rebuilds its chains from scratch, and that can cut the SSH session you are typing in; rules can be added while it is still off, so write the SSH rule before you enable it.16

sudo ufw default deny incomingsudo ufw default allow outgoingsudo ufw limit 22/tcpsudo ufw allow 80/tcpsudo ufw allow 443/tcpsudo ufw show addedAdded user rules (see 'ufw status' for running firewall):ufw limit 22/tcpufw allow 80/tcpufw allow 443/tcpsudo ufw enableCommand may disrupt existing ssh connections. Proceed with operation (y|n)? yFirewall is active and enabled on system startup

ufw show added lists the rules as you typed them, which is the list to read before you switch the firewall on, and adding --dry-run to a ufw command shows the changes it would make without making them.1615The SSH rule above uses limit rather than allow: connections get through until one address has started six within half a minute, and then that address is refused.16 The limit applies to you too: a deploy script or a burst of new terminals that opens six connections in half a minute is refused for a while, so use allow if you automate logins, or allow your own fixed address above it. If you always connect from a fixed address, such as an office or a VPN, you can go further and allow port 22 from that address only, as Ubuntu's firewall guide shows.15 Then the robots never reach sshd at all, and a change in your own address locks you out, so keep the console in mind.

Many providers also filter traffic before it reaches the server, but that filtering is aimed at floods, judged by volume and shape, and one address trying a password now and then looks like any other visitor to it. How DDoS protection works covers that layer. Everything in this guide happens on the server, and is yours to do.

fail2ban and rate limits: slower guessing, not an end to it

fail2ban watches login failures, in log files or the systemd journal, and when an address fails too many times it updates the firewall to reject new connections from it for a set time.17 Its shipped jail.conf bans an address for 10 minutes once it has failed 5 times within 10 minutes, and asks you not to edit it: your settings belong in jail.local or a file in jail.d, because an update can overwrite jail.conf.18 Upstream, every jail starts off. Debian's package switches the sshd jail on in jail.d/defaults-debian.conf and has it read the journal,26 which matters because Debian 12 and later no longer install rsyslog by default, so there may be no /var/log/auth.log to read.25 If you write the jail yourself, set the backend the same way; the systemd backend reads the journal.18

[sshd]enabled = truebackend = systemd
sudo apt install fail2bansudo systemctl restart fail2bansudo fail2ban-client status sshdStatus for the jail: sshd|- Filter|  |- Currently failed: 1|  |- Total failed:     38|  `- Journal matches:  _SYSTEMD_UNIT=ssh.service + _COMM=sshd`- Actions   |- Currently banned: 2   |- Total banned:     9   `- Banned IP list:   203.0.113.45 198.51.100.23

The arithmetic for one address

Take one address guessing passwords against SSH, under four setups.

  • Nothing in front. sshd allows 6 attempts per connection.4 On OpenSSH before 9.8 nothing in sshd caps the rate: the robot opens another connection whenever it likes. From 9.8, PerSourcePenalties refuses an address for 5 seconds after each failed connection once 15 seconds of penalty have built up,49 roughly one failed connection every 5 seconds: 720 an hour at 6 tries each, about 4,300 guesses. A ceiling close to ufw's, and still far from zero.
  • A ufw limit on port 22. An address that opens 6 or more connections in 30 seconds is denied, so it gets at most 5 connections per 30 seconds. At 6 tries each, that is 30 guesses per 30 seconds, and an hour holds 120 such windows: 30 × 120 = 3,600 guesses an hour, at most.
  • fail2ban's defaults. After 5 failures the address is banned for 10 minutes. When the ban lifts, those failures are more than 10 minutes old and no longer count, so it gets another 5. That is about six rounds of 5 an hour, roughly 30 guesses, or 30 × 24 × 365 = 262,800 a year from that one address.
  • Keys only. Zero, exactly: the server never asks for a password, so there is nothing for a guess to match.
Passwords on, OpenSSH before 9.8no fixed ceilingPasswords on, OpenSSH 9.8 or laterabout 4,300Passwords on, ufw limit on port 223,600 at mostPasswords on, fail2ban defaultsabout 30Passwords off: keys only0 that can work1101001,00010,000guesses per hour from one address, log scale
Password guesses one address can make against SSH in an hour. Rate limits and bans cut the number; only turning passwords off takes it to zero, which a log scale cannot draw, so it sits as a dot at the start. Derived from the defaults of sshd, ufw and fail2ban, as worked out above. Upper bounds, not measurements.

Is 262,800 guesses a year a lot? Against a long random password, no. Twelve characters drawn from upper- and lower-case letters and digits give 6212 combinations, about 3.2 × 1021; even at 3,600 guesses an hour, trying them all would take around 9 × 1017 hours. Against 123456, or a password that matches the account name, a year is far more than enough. And a robot network has many addresses, each with its own allowance. The fail2ban project is blunt about it in its own README: it makes wrong guesses rarer, but a weak password stays weak, and it points you to keys or two-factor login instead.17

Updates: the only fix for a bug in the lock itself

Every layer so far assumes the software doing the checking works. OpenSSH 9.8 fixed a race condition in sshd, present in Portable OpenSSH from version 8.5p1 to 9.7p1, that could allow arbitrary code execution as root. It had been demonstrated on 32-bit Linux systems with glibc, and in the lab an attack took 6 to 8 hours of continuous connections on average.9 Ubuntu's notice for the bug, CVE-2024-6387, describes an unauthenticated remote attacker triggering it by failing to authenticate within a set time.10No password was guessed and no key was needed. A server that accepted only keys was exposed until it was updated, because triggering the bug did not require logging in at all.

That is the job only updates can do. On Ubuntu, security updates are applied automatically by unattended-upgrades, which is installed by default and runs once a day, started by the apt-daily and apt-daily-upgrade timers with a random delay; /etc/apt/apt.conf.d/20auto-upgrades turns it on and off.19 Debian's unattended-upgrade does the same job, installing security upgrades from the configured package sources and logging to /var/log/unattended-upgrades/, and its --dry-run option simulates a run without installing anything.20 Whether a particular image has it switched on is worth checking rather than assuming.

systemctl list-timers 'apt-daily*'cat /etc/apt/apt.conf.d/20auto-upgradesAPT::Periodic::Update-Package-Lists "1";APT::Periodic::Unattended-Upgrade "1";sudo unattended-upgrade --dry-run

If the file is missing, install the package with sudo apt install unattended-upgrades and set both values in 20auto-upgrades to "1"; a 0 switches that action off.19

Installed is not the same as running. A service keeps using the old copy of an updated library in memory until it restarts. From Ubuntu 24.04, unattended-upgrades hands that to needrestart, which restarts most affected services by itself, but automatic reboots are off by default.19 On Debian and Ubuntu, an update that needs a reboot leaves the file /var/run/reboot-required behind. A new kernel only runs after that reboot. Check for the file after updates and reboot at a quiet moment, or set Unattended-Upgrade::Automatic-Reboot to true with Automatic-Reboot-Time at an hour when nobody is using the server.

What is listening, and who has been knocking

A firewall can be switched off by mistake, so know what is behind it. ss lists sockets: -l shows only the listening ones, -t and -u pick TCP and UDP, -n prints numbers rather than service names, and -p shows the process behind each socket.21 Put together, they answer the question "what on this server accepts connections, and which program is it?"

sudo ss -tulpnNetid State  Recv-Q Send-Q Local Address:Port Peer Address:Port Processudp   UNCONN 0      0         127.0.0.54:53        0.0.0.0:*    users:(("systemd-resolve",pid=512,fd=16))tcp   LISTEN 0      4096         0.0.0.0:22        0.0.0.0:*    users:(("sshd",pid=801,fd=3))tcp   LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",pid=1204,fd=6))tcp   LISTEN 0      200        127.0.0.1:5432      0.0.0.0:*    users:(("postgres",pid=990,fd=7))tcp   LISTEN 0      511          0.0.0.0:6379      0.0.0.0:*    users:(("redis-server",pid=1110,fd=6))

Read the Local Address column. 127.0.0.1 (or [::1]) means only programs on this machine can connect, so the database line is fine. 0.0.0.0, [::] or * means every address the server has, the public one included. SSH and the web server are meant to be there. The cache on 6379 is the line to act on: stop it with sudo systemctl disable --now and its unit name, or tell it to listen on 127.0.0.1 in its own configuration. The firewall would have covered it; closing it at the source covers you on the day the firewall is off.

Reading the SSH log

On a server running systemd, the SSH log is in the journal. journalctl -u shows one unit, --since limits the time range, -f follows new entries as they arrive, and -g filters messages with a regular expression, ignoring case when the pattern is all lower case.22On Ubuntu the unit is called ssh, the name its documentation restarts;11 if that finds nothing on your distribution, try sshd.

sudo journalctl -u ssh --since today -g 'failed|invalid'Sep 28 09:14:02 web1 sshd[48211]: Invalid user admin from 203.0.113.45 port 51122Sep 28 09:14:05 web1 sshd[48213]: Failed password for root from 203.0.113.45 port 51130 ssh2sudo journalctl -u ssh --since today -g acceptedSep 28 10:02:41 web1 sshd[49870]: Accepted publickey for alice from 198.51.100.7 port 50214 ssh2: ED25519 SHA256:(fingerprint)

On OpenSSH 9.8 and later the process name in these lines is sshd-session rather than sshd.9 Failures are the background noise this guide started with, and once passwords are off they stop mattering. The line that matters is Accepted: every successful login, with the account, the method and the address. Any you do not recognise is worth acting on straight away. While you test a change, keep journalctl -u ssh -f running in one window and log in from another, and you will see exactly why a login was refused.

Beliefs that leave the door open

The beliefWhat actually happensInstead
"Moving SSH off port 22 secures it."Scanners that try every port still find it. It cuts log noise, nothing more.Turn passwords off. Move the port later only for quieter logs.
"I set PasswordAuthentication no."A snippet read earlier can still say yes, keyboard-interactive can still prompt, and nothing changes until sshd restarts.Believe sshd -T, not the file.
"fail2ban protects SSH."It slows each address. Its own README says it cannot fix weak authentication.Keys only.
"Root login is off by default."OpenSSH's default still lets root in with a key, and an image can change the default.PermitRootLogin no, checked with sshd -T.
"Automatic updates mean I am patched."The packages are. A new kernel waits for a reboot, and automatic reboots are off by default.Watch for /var/run/reboot-required.
"A default-deny firewall will break apt."Replies to connections the server opened match the established rule, and outgoing traffic is allowed.Keep the established rule first; open inbound ports only.
"ufw blocks everything I did not allow."Ports published by Docker are routed before ufw's rules see them.Publish container ports on 127.0.0.1, and check with ss.
"The database has a password, so it can listen everywhere."Database ports are on MITRE's list of commonly guessed services too.Listen on 127.0.0.1, with the firewall behind it.
"The backups run every night."A backup has only been proven once something has been restored from it.Restore one on purpose, before you need to.

The last row is the layer for the day the others fail, and it gets one line here because it has a guide of its own: server backups that actually restore covers snapshots, database dumps, copies kept elsewhere and how to test a restore. None of this is a job you finish. NIST's guide to general server security treats it as controls to select, implement and then maintain,23 and the commands in this guide are the maintaining: sshd -T, ss -tulpn and the journal are as useful next month as today.

The order that does not lock you out

Every step above can cut off your own access if it runs before the step it depends on. Two habits make the sequence safe. Never close the session you are working in until a fresh login has worked. And keep a way in that does not go through SSH or the network, such as a browser console in your hosting panel that shows the server's own screen.

  1. Open the console, and keep one session open

    Open the server's console in a browser tab before you change anything, and leave your first SSH session open until the last step.

  2. Update everything

    As root, run apt update, then apt full-upgrade (and apt install sudo if it is missing). If /var/run/reboot-required exists afterwards, reboot now, while nothing depends on the server yet.

  3. Create your user and give it sudo

    adduser alice, then adduser alice sudo. Give it a strong password: sudo and the console will ask for it.

  4. Install your key and prove it works

    Run ssh-copy-id from your computer (or copy root's authorized_keys if passwords are already off), log in as the new user from a new terminal, and check that sudo whoami prints root.

  5. Turn off passwords and root logins

    Write your snippet in /etc/ssh/sshd_config.d/, run sudo sshd -t, read sudo sshd -T, restart ssh and log in again from a new terminal. A login with ssh -o PubkeyAuthentication=no should now be refused.

  6. Add the firewall rules, then switch it on

    Deny incoming by default, limit 22, allow the ports your services need, read ufw show added, then run ufw enable and test a new login.

  7. Add fail2ban

    Install it, make sure the sshd jail is on and reads the journal, restart fail2ban and check fail2ban-client status sshd.

  8. Check that updates run by themselves

    Read 20auto-upgrades, look at the apt-daily timers, and decide how reboots will happen.

  9. Close what you do not need, then back up

    Run sudo ss -tulpn and stop or rebind anything on every address that should not be there. Take a backup and restore it once, somewhere harmless.

Questions

Is changing the SSH port worth it?

It can make your logs quieter, but it hides the door rather than locking it: anything that scans every port finds the new one, and the password prompt is still there. Turn passwords off first. If you move the port afterwards, open the new port in the firewall before you restart sshd.

Do I still need fail2ban if SSH only accepts keys?

Not to keep SSH safe: with passwords off there is nothing to guess. It still trims the noise in your logs, and it can protect other services that do take passwords, such as a mail server or a web login, if you give them jails.

Should I turn off root login completely?

Yes, once your own user can log in with a key and use sudo. The default, prohibit-password, already refuses passwords for root, but setting PermitRootLogin to no also means a copied root key is useless and every admin action goes through a named account.

What is the difference between ufw and nftables?

nftables is the Linux kernel's packet filtering framework and its command-line tool. ufw is a simpler front end that manages firewall rules for you. Both give you a default policy plus a list of exceptions; pick one and do not mix hand-written rules with ufw's.

How do I tell whether someone actually got in?

Look for logins that succeeded, not the ones that failed. journalctl -u ssh -g accepted lists the logins the SSH server accepted that are still in the journal, with the account and the address each came from; anything you do not recognise deserves attention straight away.

Sources

  1. SSH/Telnet Scanning ActivitySANS Internet Storm Center
  2. Brute Force: Password Guessing (T1110.001)MITRE ATT&CK
  3. RFC 4252: The Secure Shell (SSH) Authentication ProtocolIETF
  4. sshd_config(5)OpenBSD manual pages (OpenSSH)
  5. sshd_config(5), Debian trixieDebian manpages
  6. sshd(8)OpenBSD manual pages (OpenSSH)
  7. ssh-keygen(1)OpenBSD manual pages (OpenSSH)
  8. ssh-copy-id(1), Debian trixieDebian manpages
  9. OpenSSH 9.8 release notesOpenSSH
  10. CVE-2024-6387Ubuntu security (Canonical)
  11. OpenSSH serverUbuntu Server documentation (Canonical)
  12. User managementUbuntu Server documentation (Canonical)
  13. Configuring chainsnftables wiki
  14. Simple ruleset for a servernftables wiki
  15. FirewallUbuntu Server documentation (Canonical)
  16. ufw(8), Debian trixieDebian manpages
  17. fail2ban READMEfail2ban project
  18. config/jail.conffail2ban project
  19. Automatic updatesUbuntu Server documentation (Canonical)
  20. unattended-upgrade(8), Debian trixieDebian manpages
  21. ss(8), Debian trixieDebian manpages
  22. journalctl(1), Debian trixieDebian manpages
  23. SP 800-123: Guide to General Server SecurityNIST
  24. Installation guide, 6.3: Using individual components (setting up users and passwords)Debian
  25. Release notes for bookworm, 5.1.7: Changes to system loggingDebian
  26. fail2ban package: jail.d/defaults-debian.confDebian sources
  27. Packet filtering and firewallsDocker documentation

Put it to work on a server of your own.

Size a server for your workload with the calculator, or see which plan fits the job you have in mind.