The rule that matters more than any setting: keep an existing session open while you change sshd_config. If you lock yourself out with one connection open, you fix it. With zero open, you rebuild the box.
1. Generate a modern key pair
ssh-keygen -t ed25519 -C "lab key" -f ~/.ssh/lab_ed25519
ls -l ~/.ssh/lab_ed25519*
ssh-keygen -lf ~/.ssh/lab_ed25519.pubEd25519, not RSA: shorter, faster, and no key-size decision to get wrong. The private key must be 600 — ssh refuses to use a key others can read, which is a feature.
Verify
2. Install the public key correctly
sudo useradd -m -s /bin/bash deploy
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo install -m 600 -o deploy -g deploy /dev/null /home/deploy/.ssh/authorized_keys
sudo tee -a /home/deploy/.ssh/authorized_keys < ~/.ssh/lab_ed25519.pub >/dev/null
sudo ls -la /home/deploy/.sshThe modes are not decoration. sshd ignores authorized_keys if the file or the directory is group- or world-writable, and it does so silently from the client's point of view — the failure appears only in the server's log. That is the single most common "my key does not work" cause.
Verify
3. Test the key before changing any config
ssh -i ~/.ssh/lab_ed25519 -o IdentitiesOnly=yes deploy@localhost 'whoami; echo OK'IdentitiesOnly=yes stops the agent offering every other key, so you are testing the key you think you are testing. If this fails, read the server side:
sudo journalctl -u ssh -n 20 --no-pager | grep -i -E "deploy|authent|key"Do not proceed until this works. Every step after this removes a fallback.
Verify
4. Harden sshd, one drop-in file
sudo tee /etc/ssh/sshd_config.d/99-lab-hardening.conf >/dev/null <<'CONF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 20
AllowUsers deploy
X11Forwarding no
AllowAgentForwarding no
CONF
sudo sshd -t && echo "config syntax OK"sshd -t validates without applying. Run it every time — a typo in sshd_config means the daemon refuses to start, and if you have already restarted it you are locked out.
A drop-in beats editing sshd_config: your changes survive a package upgrade and are one file to review. Confirm the include exists first:
grep -r "^Include" /etc/ssh/sshd_configVerify
5. Apply it, then verify from the outside
With your other terminal still connected:
sudo systemctl reload ssh || sudo systemctl reload sshdNow check what the server actually decided, rather than what you wrote:
sudo sshd -T | grep -E \
"^(permitrootlogin|passwordauthentication|maxauthtries|allowusers)"sshd -T dumps the effective configuration after all includes and defaults. This is the only trustworthy check, because a later Include can override your file.
Prove password auth is genuinely off:
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no deploy@localhostIt must fail with "Permission denied (publickey)" — meaning the server offered only key auth.
Verify
Verify
What this does not cover
Key-only access stops password guessing. It does not stop a stolen key. In production you also want short-lived certificates or an SSH CA, fail2ban or equivalent for noise reduction, and a bastion so production hosts are not reachable from the internet at all.
Clean up
sudo rm -f /etc/ssh/sshd_config.d/99-lab-hardening.conf
sudo sshd -t && (sudo systemctl reload ssh || sudo systemctl reload sshd)
sudo userdel -r deploy
rm -f ~/.ssh/lab_ed25519 ~/.ssh/lab_ed25519.pubWhere this goes next
The door is locked. Next: the disk behind it filling up at three in the morning.