Security Hardening

SSH key authentication, disabling root login and passwords, and basic brute-force protection with fail2ban.

The baseline every internet-facing server needs

A freshly provisioned Linux server, reachable on the public internet, is scanned by automated bots within minutes of coming online — not targeted, just swept up by tools constantly probing every reachable IP for weak SSH passwords and known-default configurations. None of what follows is exotic; it's the small set of changes that eliminate almost all of that automated risk, and every serious server-hardening checklist starts here.

SSH key authentication instead of passwords

Password authentication over SSH is vulnerable to brute-force guessing — an attacker (or, in practice, a botnet) can attempt thousands of password guesses per minute against port 22 with no other access required at all. Key-based authentication replaces "something you know" (a password, guessable or phishable) with "something you have" (a private key, which never leaves your machine and is never transmitted anywhere, even during login).

Generating a key pair

Bash
$ ssh-keygen -t ed25519 -C "ali@laptop"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/ali/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):

ed25519 is the modern recommended key type — smaller and faster than the older, still-common rsa, with equivalent or better security at typical key sizes. This creates two files: id_ed25519 (the private key — never share this, never commit it, ideally protect it with a passphrase too) and id_ed25519.pub (the public key — safe to share, this is what goes on servers).

Installing the public key on a server

Bash
$ ssh-copy-id deploy@203.0.113.10

ssh-copy-id appends your public key to ~/.ssh/authorized_keys on the remote server for that user — anyone holding the matching private key can now authenticate as deploy without a password at all. Without the helper, the same result is one line:

Bash
$ cat ~/.ssh/id_ed25519.pub | ssh deploy@203.0.113.10 "cat >> ~/.ssh/authorized_keys"

Disabling password authentication entirely

Once key-based login is confirmed working, password authentication should be turned off server-side — otherwise an attacker can simply ignore your keys and keep trying passwords anyway:

Bash
# /etc/ssh/sshd_config
PasswordAuthentication no
PubkeyAuthentication yes
Bash
$ sudo systemctl restart sshd

Test this in a second, still-open terminal session before closing your first one. A typo in sshd_config, or a missing key on the server, combined with password auth already disabled, is exactly how people lock themselves out of a remote box with no console access to fix it.

Disabling root login over SSH

Even with keys required, allowing direct SSH login as root is an unnecessary risk: it's the one account name an attacker never has to guess, and it bypasses any "who did what" accountability that comes from each admin using their own named account plus sudo.

Bash
# /etc/ssh/sshd_config
PermitRootLogin no

The correct pattern instead: log in as a named, unprivileged user (with a key), then use sudo for anything that genuinely needs root:

Bash
$ ssh deploy@203.0.113.10
deploy@web-01:~$ sudo systemctl restart nginx
[sudo] password for deploy:

This also means compromising any one SSH key only grants that specific user's privileges, not root outright, and every privileged action is individually logged against a real username rather than anonymously as "root."

A basic firewall and fail2ban

Networking Basics already covered ufw/iptables for controlling which ports are reachable at all — restricting SSH to only the IP ranges that need it (a VPN range, an office IP) where possible is a strong additional layer on top of key-only auth.

fail2ban adds a complementary, different kind of protection: it watches log files (like /var/log/auth.log) for repeated failed login attempts from the same address, and automatically firewalls that address out after a configured threshold.

Ini
# /etc/fail2ban/jail.local
[sshd]
enabled  = true
port     = ssh
maxretry = 5
findtime = 10m
bantime  = 1h
Bash
$ sudo systemctl enable --now fail2ban
$ sudo fail2ban-client status sshd
Status for the jail: sshd
|- Currently failed: 2
|- Total failed:     134
`- Currently banned: 1
   `- IP list: 198.51.100.23

maxretry = 5 within findtime = 10m triggers a bantime = 1h firewall ban for that source address — automatically, with no manual intervention. This doesn't replace key-only authentication (a determined, patient attacker with a huge pool of source IPs can still grind slowly enough to dodge the threshold), but it shuts down the overwhelming majority of automated, unsophisticated brute-force scanning immediately, and keeps auth.log from filling up with thousands of noisy failed attempts.

Putting the layers together

Layer Defends against Configured in
SSH key authentication Password guessing/brute-forcing ~/.ssh/authorized_keys, sshd_config
PermitRootLogin no Direct root compromise, lack of per-user accountability sshd_config
Firewall (ufw/iptables) Any exposure at all from ports/services that don't need to be public ufw, iptables rules
fail2ban Automated brute-force scanning against whatever remains reachable /etc/fail2ban/jail.local

None of these four is sufficient alone — a firewall doesn't help if SSH itself is left password-protected and exposed; key-only auth doesn't help if the firewall exposes a database port to the entire internet. Together, they cover the specific things automated internet-wide scanning actually probes for.

Common mistakes

  • Disabling PasswordAuthentication before confirming key-based login actually works, in the same session, with no second terminal open as a fallback — this is the single most common way to lock yourself out of a server entirely.
  • Leaving PermitRootLogin yes (or not set, which defaults to allowing some form of root login on many distributions) because it's "just for emergencies" — every automated scanner on the internet already assumes an account named root exists and targets it specifically.
  • Treating a private key file (id_ed25519, no .pub extension) as safe to share or commit to a repository — only the .pub file is meant to be shared; the private key should never leave the machine that generated it.
  • Relying on fail2ban alone without key-based authentication — it slows down brute-force scanning but does nothing if the password being guessed is eventually the right one; the two are complementary, not substitutes for each other.
  • Forgetting that a firewall change or an sshd_config edit can lock out remote access entirely if done carelessly — always keep a second, already-authenticated session open (or console/out-of-band access) while testing any change to how SSH itself is reached.