student@ubuntu:~$
ctf Lesson 33 25 min read

NCAE: SSH Hardening & Defense

Lock down the most critical remote access service

SSH Hardening & Defense

SSH (Secure Shell) gives you a terminal on a remote computer. You type commands on your laptop, they execute on a server thousands of miles away. Every byte traveling between you and the server is encrypted, so nobody in between can see what you’re typing or what comes back.

This page teaches you how SSH works, why attackers target it first, and how to lock it down so that only authorized users can connect. By the end, you will be able to harden any SSH server from a default installation to a competition-ready configuration.

Prerequisites: You should be comfortable with file permissions and text editing from Weeks 1-2.


1. What is SSH?

SSH runs on port 22 by default. The client (your laptop) connects to the server (the remote machine) running a daemon called sshd. The basic command:

ssh username@hostname

That’s it. You provide a username and a hostname (or IP address), and SSH opens an encrypted terminal session on the remote machine. Everything you type travels through an encrypted tunnel.

# Connect to a server at 10.0.5.2 as the user 'admin'
ssh admin@10.0.5.2

# Connect on a non-standard port
ssh -p 2222 admin@10.0.5.2

# Run a single command without opening a shell
ssh admin@10.0.5.2 'uptime'

When you connect for the first time, SSH asks you to verify the server’s fingerprint. This prevents man-in-the-middle attacks — if someone intercepts your connection and pretends to be the server, the fingerprint won’t match.

Checkpoint: You SSH into a server you've connected to before, and SSH warns "REMOTE HOST IDENTIFICATION HAS CHANGED". What does this mean?

The server’s host key is different from what SSH stored in your ~/.ssh/known_hosts file. This either means (1) the server was reinstalled and got new keys, or (2) someone is intercepting your connection. In a competition, it usually means the server was rebuilt. Remove the old key with ssh-keygen -R hostname and reconnect. In production, investigate before removing — it could be an actual attack.


2. Why SSH is the #1 Target

If an attacker gets SSH access, they have a terminal. A terminal means they can do anything the logged-in user can do. If that user is root, the attacker owns the entire machine.

Here’s what a compromised SSH session gives an attacker:

Access Level What They Can Do
Regular user Read files, install malware in home directory, pivot to other machines
Sudo-capable user Everything above + install system-wide malware, modify configs, create backdoors
Root Total control: modify any file, create users, install rootkits, wipe logs

Attackers target SSH because:

  1. It’s almost always running (you need it to manage the server)
  2. Default configurations are permissive (root login enabled, passwords allowed)
  3. Brute-force tools like hydra can guess passwords at thousands of attempts per second
  4. A single weak password compromises the entire server

This is why SSH is always the first service defenders harden, and the first service attackers try to crack.


3. sshd_config — The Control File

Every SSH setting lives in one file: /etc/ssh/sshd_config. This is the server-side configuration. Editing this file and restarting sshd changes how the SSH daemon behaves.

Open it:

sudo nano /etc/ssh/sshd_config

Here are the 10 directives that matter most, in order of importance:

PermitRootLogin

PermitRootLogin no

Controls whether anyone can SSH in as root directly. Three options:

  • no — Root cannot log in via SSH at all. Best practice. Use a regular account and sudo when needed.
  • prohibit-password — Root can log in with a key, but not a password. Acceptable if you need root SSH for automation.
  • yes — Root can log in with a password. Never use this in competition or production.

PasswordAuthentication

PasswordAuthentication no

When set to no, SSH only accepts key-based authentication. Passwords are rejected entirely. This eliminates brute-force password attacks completely — there is no password to guess.

PubkeyAuthentication

PubkeyAuthentication yes

Enables key-based authentication. This should always be yes. Combined with PasswordAuthentication no, it means keys are the only way in.

MaxAuthTries

MaxAuthTries 4

How many authentication attempts are allowed per connection before the server disconnects. Default is 6. Setting it to 3-4 limits brute-force effectiveness per connection (though attackers can reconnect).

AllowTcpForwarding

AllowTcpForwarding no

SSH can tunnel network traffic through the encrypted connection. Attackers use this to pivot — routing their traffic through a compromised server to reach internal machines. Disable it unless you specifically need it.

X11Forwarding

X11Forwarding no

Allows forwarding graphical applications from the server to your screen. In competition, you don’t need this. Disable it to reduce attack surface.

PermitEmptyPasswords

PermitEmptyPasswords no

Prevents login with accounts that have no password set. This should always be no.

LoginGraceTime

LoginGraceTime 30

How many seconds the server waits for a user to authenticate after connecting. Default is 120 seconds. Set it to 30-60 to quickly drop connections that aren’t authenticating (reduces resource consumption from attack scripts).

AllowUsers / AllowGroups

AllowUsers admin deployer

Whitelists specific users who can SSH in. Everyone else is rejected, even if they have valid credentials. AllowGroups does the same for groups. This is one of the most powerful directives — if your competition team has three users, list exactly those three.

Port

Port 22

The port SSH listens on. Changing it to something like 2222 or 22222 reduces noise from automated scanners that only target port 22. But this is security by obscurity, not real security — a port scan reveals the new port in seconds. Use it to reduce log noise, not as a defense.

After editing, always test the config before restarting:

sudo sshd -t          # Test config syntax (silent = no errors)
sudo systemctl restart sshd
Checkpoint: You set PasswordAuthentication to "no" and restart sshd. You try to log in with a password and it still works. What went wrong?

Check for a second PasswordAuthentication directive lower in the file that overrides yours. Also check /etc/ssh/sshd_config.d/*.conf — drop-in config files in this directory are included after the main config and can override your settings. Run sshd -T | grep passwordauthentication to see the effective value after all configs are merged.


4. Key-Based Authentication

Passwords are guessable. Keys are not. SSH key authentication uses public-key cryptography: you generate a pair of keys — one private (secret, stays on your laptop, never shared) and one public (goes on every server you want to access).

How it works

  1. You connect to the server
  2. The server generates a random challenge and encrypts it with your public key
  3. Only your private key can decrypt the challenge
  4. Your SSH client decrypts it and sends proof back to the server
  5. The server confirms you hold the private key — you’re in

The private key never leaves your machine. The server never sees it. Even if someone intercepts the entire exchange, they cannot derive the private key.

Generate a key pair

ssh-keygen -t ed25519 -C "your@email.com"
  • -t ed25519: Use the Ed25519 algorithm (modern, fast, short keys). Avoid RSA unless you need compatibility with ancient systems.
  • -C "your@email.com": A comment embedded in the key to identify it later.

This creates two files:

  • ~/.ssh/id_ed25519 — your private key. Permissions must be 600 (owner read/write only). SSH refuses to use a private key with loose permissions.
  • ~/.ssh/id_ed25519.pub — your public key. Safe to share with anyone.

Copy the public key to a server

ssh-copy-id username@server

This appends your public key to ~/.ssh/authorized_keys on the server. Now you can log in without a password.

If ssh-copy-id is not available (some minimal systems), do it manually:

cat ~/.ssh/id_ed25519.pub | ssh username@server 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys'

Then disable passwords entirely

Once your key works, edit /etc/ssh/sshd_config on the server:

PasswordAuthentication no

Restart sshd. Now the only way in is with a valid key.

Permission requirements

SSH is strict about file permissions. If they’re wrong, key auth silently fails:

Path Required Permission
~/.ssh/ 700 (drwx——)
~/.ssh/authorized_keys 600 (-rw——-)
~/.ssh/id_ed25519 600 (-rw——-)

Fix them:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/id_ed25519
Checkpoint: You copy your key to a server, but key auth doesn't work --- it still asks for a password. What should you check?
  1. Permissions: ls -la ~/.ssh/ on the server. authorized_keys must be 600, .ssh/ must be 700, and the home directory must not be group-writable.
  2. Correct key: Does authorized_keys contain your public key? cat ~/.ssh/authorized_keys on the server.
  3. sshd config: Is PubkeyAuthentication yes? Run sshd -T | grep pubkey on the server.
  4. SELinux: On RHEL/CentOS, SELinux may block access. Run restorecon -rv ~/.ssh.
  5. SSH verbose mode: Connect with ssh -vvv user@server and read the debug output to see exactly where authentication fails.

5. fail2ban — Automatic Brute-Force Protection

Even with strong passwords, brute-force attacks flood your logs and consume resources. fail2ban monitors log files for repeated failures and temporarily bans offending IP addresses using the firewall.

How it works

  1. fail2ban watches /var/log/auth.log (or /var/log/secure on RHEL)
  2. It counts failed SSH login attempts per IP address
  3. After N failures within a time window, it adds a firewall rule blocking that IP
  4. After the ban duration expires, the rule is removed

Install and configure

sudo apt install fail2ban       # Debian/Ubuntu
sudo dnf install fail2ban       # RHEL/Fedora

Create a local config (never edit the main config — updates overwrite it):

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

Key settings in the [sshd] section:

[sshd]
enabled  = true
port     = ssh
filter   = sshd
logpath  = /var/log/auth.log
maxretry = 5
bantime  = 3600
findtime = 600
  • maxretry: Number of failures before banning (5 is reasonable)
  • bantime: How long the ban lasts in seconds (3600 = 1 hour)
  • findtime: Time window for counting failures (600 = 10 minutes)

This means: if an IP fails 5 times within 10 minutes, ban it for 1 hour.

sudo systemctl enable --now fail2ban

Useful commands

sudo fail2ban-client status sshd          # Show jail stats and banned IPs
sudo fail2ban-client set sshd unbanip IP  # Manually unban an IP
sudo fail2ban-client set sshd banip IP    # Manually ban an IP
Checkpoint: fail2ban is running but IPs aren't getting banned despite obvious brute-force attacks in the logs. What's wrong?
  1. Check the logpath — it must point to the correct log file for your distro (/var/log/auth.log on Debian, /var/log/secure on RHEL).
  2. Check that the [sshd] jail is enabled = true.
  3. Check that fail2ban can read the log: sudo fail2ban-client status sshd shows the number of currently monitored log files.
  4. Check if the backend is correct: backend = systemd may be needed on systemd-based distros if the log file isn’t being written in the traditional format.

6. The Hardening Checklist

Follow this step-by-step on any fresh server to go from default SSH to hardened SSH in under 10 minutes:

# Action Command
1 Backup original config sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
2 Disable root login sudo sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
3 Disable password auth sudo sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
4 Enable pubkey auth sudo sed -i 's/^#*PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
5 Set max auth tries echo 'MaxAuthTries 4' \| sudo tee -a /etc/ssh/sshd_config
6 Disable TCP forwarding echo 'AllowTcpForwarding no' \| sudo tee -a /etc/ssh/sshd_config
7 Disable X11 forwarding sudo sed -i 's/^#*X11Forwarding.*/X11Forwarding no/' /etc/ssh/sshd_config
8 Set login grace time echo 'LoginGraceTime 30' \| sudo tee -a /etc/ssh/sshd_config
9 Whitelist users echo 'AllowUsers admin your_username' \| sudo tee -a /etc/ssh/sshd_config
10 Test and restart sudo sshd -t && sudo systemctl restart sshd

Before step 10: Open a second terminal and keep it connected to the server. If your config has an error, you can fix it from the second terminal without getting locked out.


7. Testing Your Hardening

After hardening, verify every change works:

# Test config syntax (no output = no errors)
sudo sshd -t

# See effective config (merges all config files)
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication'

Expected output:

permitrootlogin no
passwordauthentication no
pubkeyauthentication yes

Verification tests

# Try logging in as root (should fail)
ssh root@your_server
# Expected: Permission denied (publickey).

# Try logging in with a password (should fail)
ssh -o PubkeyAuthentication=no user@your_server
# Expected: Permission denied (publickey).

# Try logging in with your key (should work)
ssh -i ~/.ssh/id_ed25519 user@your_server
# Expected: successful login

# Check what's listening on port 22
ss -tulnp | grep :22
# Expected: sshd listening on 0.0.0.0:22

Monitor the auth log

# Watch for login attempts in real time
sudo tail -f /var/log/auth.log

# Count failed attempts in the last hour
sudo grep "Failed password" /var/log/auth.log | wc -l

Exercises

  1. Config Audit: Read through /etc/ssh/sshd_config on a practice VM. List every directive that is currently set to a non-default value. For each one, explain what it does and whether it’s secure.

  2. Key Rotation: Generate a new Ed25519 key pair, copy it to your VM, verify it works, then remove the old key from authorized_keys. This is how you rotate keys in production.

  3. fail2ban Lab: Install fail2ban on your VM. Set maxretry = 3 and bantime = 60. From another machine, intentionally fail three logins. Verify your IP appears in fail2ban-client status sshd. Wait 60 seconds and verify it’s unbanned.

  4. Lockout Recovery: Intentionally misconfigure sshd (set AllowUsers to a user that doesn’t exist). Restart sshd. You’re locked out. How do you recover? (Answer: console access or a second SSH session you kept open.)


Resources

Practice: OverTheWire Bandit (SSH-based wargame) · TryHackMe — Linux Fundamentals (search “linux fundamentals”)

Reference: sshd_config man page · SSH.com — SSH key management

Video: NetworkChuck — SSH explained · Computerphile — Public key cryptography