Hardening the SSH server
How to protect the main entry point: disable passwords and root login, change the defaults, and close off the rest in sshd_config.
How to protect the main entry point: turn off passwords and root login,
check that sshd actually applied the settings, and close off the extra
exposure in sshd_config.
Why SSH is target number one
SSH is the main door into a server, and it's constantly being pulled at. Any public server catches a nonstop stream of password-guessing attempts over SSH from bots. If password login is open and the password is weak, sooner or later it gets cracked.
Hardening is closing off the most obvious ways in. sshd settings live in
/etc/ssh/sshd_config and in files under the /etc/ssh/sshd_config.d/
directory. After an edit, sshd needs to be asked to reread the config.
Turn off password login
The main step: move to keys from the SSH keys lesson and turn off passwords entirely. Then password-guessing is pointless, there's simply no password.
PasswordAuthentication no
PubkeyAuthentication yes
Before turning off passwords, make sure key login already works, or you'll lock yourself out. Keep a second SSH session open while you check the new config.
A second path to a password: keyboard-interactive
sshd has another method, keyboard-interactive. It's enabled by default
(KbdInteractiveAuthentication yes). On Debian and Ubuntu sshd runs
through PAM (UsePAM yes), and then keyboard-interactive asks for the
same password, just through a different route. You turn off
PasswordAuthentication but leave this method on, and password login is
still possible.
So both get turned off:
PasswordAuthentication no
KbdInteractiveAuthentication no
You can see what methods the server offers right from the client. Deny the key on your side, and the server lists what's left:
$ ssh -o BatchMode=yes -o PubkeyAuthentication=no deploy@server true
deploy@server: Permission denied (publickey,password,keyboard-interactive).
After hardening, only publickey remains in the parentheses.
Why an sshd_config edit sometimes has no effect
On Debian 12 and Ubuntu 22.04 there's a line near the top of
/etc/ssh/sshd_config:
Include /etc/ssh/sshd_config.d/*.conf
sshd takes the first value it meets for a setting, not the last. Files
from sshd_config.d/ are pulled in right at the start, before the rest of
the main config, and cloud-init often puts PasswordAuthentication yes
there. Then your edit further down the file is simply ignored, and
silently at that.
So first check what sshd actually applied, and what's in the directory:
$ sudo sshd -T | grep -i passwordauth
passwordauthentication yes
$ ls /etc/ssh/sshd_config.d/
50-cloud-init.conf
sshd -T prints the final config after all Includes. If it disagrees with
what you wrote, look for the overriding line.
A reliable approach: put your own settings in a separate file in the same
directory, but also remove the conflicting line from the cloud-init file.
Files are pulled in alphabetically, and 50-cloud-init.conf still comes
before your 99-...:
# /etc/ssh/sshd_config.d/99-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitRootLogin no
After the edit, run sudo sshd -T | grep -i passwordauth again and check
the value is now no.
Disable root login
Direct root login is dangerous: it's instant all-powerful access, and the
name root is known to every bot. It's better to log in as a regular
user and raise privileges with sudo, as in the users lesson.
By default PermitRootLogin prohibit-password is set: root's password
isn't accepted, but login by key is allowed. If root's key ever leaked, or
someone left it in place "temporarily," it still works. Full denial:
PermitRootLogin no
Now not even the right key gets you in as root directly, and an attacker
also has to guess the username. In the sshd log, the refusal shows up as
ROOT LOGIN REFUSED.
Apply and verify
After an edit, the config gets checked for errors and the service is asked to reread it:
$ sudo sshd -t # check the config syntax
$ sudo systemctl reload ssh
sshd -t catches a typo before the restart. Without this check, a broken
config brings sshd down, and it stops accepting new connections. For
example, a typo in a setting name:
$ sudo sshd -t
/etc/ssh/sshd_config.d/99-hardening.conf: line 5: Bad configuration option: PasswordAuthentcation
/etc/ssh/sshd_config.d/99-hardening.conf: terminating, 1 bad configuration options
What reload does: it sends a SIGHUP signal to the main sshd process. On
that signal, sshd restarts itself with the same path and flags and rereads
the config. On Debian, the ssh.service unit runs sshd -t once more
before that. Without systemd (as in this lesson's containers), the same
thing is done by hand, the main process's PID is in the pid file:
kill -HUP "$(cat /var/run/sshd.pid)"
Already-open SSH sessions don't drop: each one lives in its own process. New connections follow the new rules. That's why the spare session saves you: if the new config locks you out, you can undo everything through it.
What else narrows the attack surface
A few settings that close off extra exposure:
- Limit users:
AllowUsers deploylets in only the listed accounts, everyone else is refused right away. - Change the port from 22 to a nonstandard one cuts the mass, noise-level scanning (this reduces log noise, it isn't real protection).
- fail2ban automatically bans an IP after several failed attempts, by reading the SSH logs.
- Timeouts and limits:
MaxAuthTriesandLoginGraceTimecap the number of attempts and the time allowed to log in within one connection.
All of this works together with the principle of least privilege: exactly what's needed stays open, everything else is closed.
Changing the port and socket activation
Starting with Ubuntu 22.10, sshd comes up through socket activation: the
port is listened on not by the service itself but by the ssh.socket
unit. A plain reload doesn't help here, a new Port from the config
only takes effect after the socket is restarted.
$ systemctl is-enabled ssh.socket # enabled = socket activation is on
$ sudo systemctl daemon-reload
$ sudo systemctl restart ssh.socket
If ssh.socket is disabled or there's no such unit (Debian 12, Ubuntu
22.04), the port takes effect with a plain sudo systemctl reload ssh.
You can always check where sshd actually listens with
ss -tlnp | grep sshd.
Common mistakes
- Turned off passwords without checking the key first. New connections
are refused, and there's no old session left. Key login first, then
PasswordAuthentication no. - The edit is in the file but has no effect. The value is overridden
by a line above it: in a file from
sshd_config.d/or an uncommented line near the top ofsshd_configitself. The "first value wins" rule applies within a single file too. Checksshd -Tand look for every occurrence of the setting (grep -rin passwordauth /etc/ssh/). - Turned off
PasswordAuthenticationbut notKbdInteractiveAuthentication. With PAM it keeps asking for a password. Check the method list from the client. - Left
PermitRootLoginat its default.prohibit-passwordstill lets root in by key. - Fixed the file and forgot to reload.
sshd -Treads the files from disk and shows the new value, while the running sshd still lives with the old config. Check the behavior itself: an actual login attempt and the method list. - Reload without
sshd -t. sshd won't come up with a broken config.
What to remember
SSH is target number one for password guessing, so passwords get turned
off (PasswordAuthentication no and KbdInteractiveAuthentication no)
in favor of keys, and direct root login is denied (PermitRootLogin no),
with a regular user plus sudo instead.
An edit is easy to lose: the first value met wins, and
Include /etc/ssh/sshd_config.d/*.conf near the top of the file is pulled
in first. sshd -T shows the final config.
Before a reload, the config gets checked with sshd -t, and a spare
session is kept open so you don't lock yourself out. On top of that, the
circle is narrowed (AllowUsers) and attempts are capped (MaxAuthTries,
fail2ban).
What you'll need for the tasks
- Log in to the server as root:
docker exec -it <server-container> bash. - Final config:
sshd -T, filtered withgrep -i. - Find every place a setting is set:
grep -rin <setting> /etc/ssh/. - Check the syntax:
sshd -t. - Reread the config without systemd:
kill -HUP "$(cat /var/run/sshd.pid)". - Methods the server offers:
ssh -o BatchMode=yes -o PubkeyAuthentication=no <user>@<server> true. - sshd log in the labs:
docker logs <server-container>. - Editor on the server:
vi(included in the image).
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson linux-security-03-ssh-hardening
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress