A host went live on a Friday. Nobody had published its address, and no DNS record pointed at it.
Eight hours later its auth log held 1,118 failed authentications from 37 unique source IPs,
still arriving while I read them, including direct root password guesses. Nothing was
compromised. That is not the point. The point is that this is the default state of a fresh cloud image, not
bad luck, and the obvious hardening has a trap in it that silently puts the password login back.
Do this first, on any host you did not personally harden:
# how many are failing, and from how many places
grep -c "Failed password" /var/log/auth.log
grep "Failed password" /var/log/auth.log | grep -oE "from [0-9.]+" | sort -u | wc -l
# what the server will actually accept, not what the file says
sudo sshd -T | grep -E "permitrootlogin|passwordauthentication"
# does root have a usable password at all
sudo passwd -S root
The last two matter more than the count. A brute-force against key-only SSH is noise. The same traffic
against PasswordAuthentication yes and a real password on root is a live exposure.
On this host passwd -S root returned P, meaning a usable password, and
sshd -T confirmed both password auth and root login were on.
sshd -T, not sshd_config. The main config file is
not the whole config. It has an Include /etc/ssh/sshd_config.d/*.conf line near the top, and on
a cloud image the interesting values live in those drop-ins rather than the file you just opened.
sshd -T prints what the daemon actually resolved.
Every instinct says to name a hardening drop-in 99-hardening.conf. Last file wins, surely.
It does not. sshd uses the first value it reads for any keyword. Files in
sshd_config.d are read in lexical order, so 50-cloud-init.conf is read before your
99- file and its PasswordAuthentication yes wins. Your file is present, correct,
and completely inert.
# name it to sort FIRST, not last
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf >/dev/null <<'EOF'
PermitRootLogin prohibit-password
PasswordAuthentication no
KbdInteractiveAuthentication no
EOF
sudo sshd -t && sudo systemctl reload ssh
sudo sshd -T | grep -E "permitrootlogin|passwordauthentication"
Sorting first buys you two things. It beats cloud-init's file, and it beats the
PermitRootLogin yes sitting further down the main config. It also survives cloud-init
rewriting its own drop-in later, which it does on its own schedule and without telling you.
sshd -t catches a syntax error, but it cannot
catch you locking yourself out with a correct file.
Locking root with passwd -l root looks like the obvious next step. Think about what it buys
you.
Once PasswordAuthentication no is in force, that password is unreachable from the
network. There is no path to it. Its only remaining use is typing it into your provider's web
console when the box will not boot or you have broken sshd. That is your break-glass recovery, and locking
the account removes it in exchange for no reduction in attack surface at all.
The argument for locking it is that cloud-init might flip password auth back on. That is a real risk, and the drop-in's sort order is what handles it. Fix the regression, keep the recovery path.
Install it. It trims log noise and slows the volume down:
sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd
But be clear about what it does. fail2ban rate-limits guessing. Key-only authentication makes guessing pointless. If you have to choose which one to do at 2am, turn off password authentication. The volume of attempts is not the problem. The fact that any of them could ever succeed is.
Two checks, from a machine that is not the one you are hardening:
# a password attempt must now be refused outright
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password root@<host>
# expect: Permission denied (publickey)
# and confirm every real login has been a key
sudo grep "Accepted" /var/log/auth.log | tail -20
That last line is the one worth reading properly. Every Accepted entry on this host was
publickey, all of them from a single jump host. That fact turned out to matter enormously about
ten minutes later, when the next hardening step was going to be a source-IP firewall rule. See
why restricting SSH to your own IP is how you lock yourself
out.
sshd -T
rather than assuming, then move on.99- loses to
cloud-init's 50- because sshd takes the first value it reads. Name it 10-, verify
with sshd -T, and leave root's password set as your console break-glass.