Locking Down the Front Door: SSH Hardening on Debian 13
Locking Down the Front Door: SSH Hardening on Debian 13
- 60% of MD5 Password Hashes Can Be Cracked in Under an Hour with a Single GPU
- Dirty Frag: Root Access on Every Major Linux Distribution — No Patch, No Warning
- Ubuntu 26.04 LTS (Resolute Raccoon): The Most Ambitious Ubuntu LTS in a Decade
- Proton Mail: Data Transferred to FBI Again!
- How Close Are Quantum Computers to Breaking RSA-2048?
- How to Prevent Ransomware Infection Risks?
- What is the best alternative to Microsoft Office?
Locking Down the Front Door:
SSH Hardening on Debian 13
A practical, step-by-step field guide to securing the OpenSSH management entry point on internet-facing Linux servers — covering key-based authentication, port migration, brute-force mitigation, and client configuration — without disrupting existing VPN or proxy workloads.
Context & Scope
After a Linux server is deployed to the public internet — whether running a reverse proxy, WireGuard VPN, or any other network service — the SSH daemon is often left in its default state: listening on port 22, accepting password authentication, and permitting root login. According to CISA and the SANS Institute, brute-force attacks, credential stuffing, and exploitation of weak SSH configurations remain among the most common attack vectors against exposed servers.
This guide targets Debian 13 (Trixie) servers managed primarily over SSH. The entire procedure applies only to the SSH management entry point and does not touch VPN, proxy, or other application-layer services.
Prerequisite: You need console or out-of-band access as a fallback before proceeding. Never disable password authentication until key-based login has been tested and confirmed working — locking yourself out is the most common mistake.
Firewall Status Check
Before changing the SSH port, confirm the current firewall state. Changing the port without first allowing it through the firewall results in an immediate lockout.
shell# Check nftables service state
systemctl is-active nftables
systemctl is-enabled nftables
# List active kernel ruleset
sudo nft list ruleset
# Check UFW / legacy iptables
sudo ufw status verbose
iptables --version
ip6tables --version
The key question is which tool owns inbound policy: nftables, UFW,
or iptables. If nftables.service is inactive yet
nft list ruleset shows rules, those rules were likely injected dynamically by Fail2ban —
this is normal and expected.
Auditing the Current SSH State
Read the four parameters that define your current exposure surface:
shellgrep -nE '^[#[:space:]]*(Port|PasswordAuthentication|PubkeyAuthentication|PermitRootLogin)' \
/etc/ssh/sshd_config
If the output resembles the following, the server is in the default high-exposure state:
sshd_config (default — insecure)Port 22
PermitRootLogin yes
PubkeyAuthentication yes
PasswordAuthentication yes
Check for active brute-force activity
shell# Fail2ban overview
sudo fail2ban-client status
sudo fail2ban-client status sshd
# Live SSH log (last 80 lines)
sudo journalctl -u ssh -n 80 --no-pager
Repeated log entries containing Invalid user, Failed password, Disconnected … [preauth], or Timeout before authentication are indicators of active automated probing — a common condition for any server that has been reachable on port 22 for more than a few hours.
Configuring Public Key Authentication
This is the most critical step. Public key-based authentication must be working and confirmed before password authentication is disabled. Reversing that order creates an unrecoverable lockout.
4.1 — Generate the key pair (on your local machine)
local shellssh-keygen -t ed25519 -C "admin-laptop"
# Suggested output names:
# Private key: ~/.ssh/id_admin
# Public key: ~/.ssh/id_admin.pub
Ed25519 is the recommended algorithm for new keys in 2025. It offers strong security, small key size, and fast signature operations. DSA (1024-bit) and RSA below 3072-bit are considered weak by current standards.
4.2 — Write the public key to the server
local shell → copy outputcat ~/.ssh/id_admin.pub
server shell
mkdir -p /root/.ssh
chmod 700 /root/.ssh
nano /root/.ssh/authorized_keys
# Paste the entire public key line, save, then:
chmod 600 /root/.ssh/authorized_keys
4.3 — Verify the public key configuration
server shell# Confirm the file is not empty
wc -c /root/.ssh/authorized_keys
cat -n /root/.ssh/authorized_keys
# Check effective sshd configuration (more reliable than reading the file)
sshd -T | grep -E 'permitrootlogin|pubkeyauthentication|passwordauthentication|authorizedkeysfile'
The output of sshd -T reflects the actual running configuration, accounting
for includes and overrides, making it more trustworthy than inspecting the config file directly.
Verifying Key Login Before Proceeding
Stop here and test. Do not modify PasswordAuthentication
or restart sshd until the following test passes successfully in a separate terminal session.
ssh -vvv -i ~/.ssh/id_admin root@203.0.113.10
The -vvv flag produces verbose debugging output. When connecting to a host for the
first time, SSH prompts for host fingerprint confirmation — this is standard behaviour and the
accepted fingerprint is written to ~/.ssh/known_hosts. Only continue to the
next section after a successful passwordless login is confirmed.
Hardening sshd_config
Back up the existing configuration before making any changes:
shellcp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak_$(date +%F_%H%M%S)
Open the configuration file and apply the following settings:
/etc/ssh/sshd_configPort 45871
PermitRootLogin prohibit-password
PubkeyAuthentication yes
PasswordAuthentication no
MaxStartups 20:30:60
| Parameter | Value | Effect |
|---|---|---|
| Port | 45871 | Moves the listener off the default port 22. Dramatically reduces automated scan noise without providing true security by obscurity alone. |
| PermitRootLogin | prohibit-password | Root may still authenticate via public key, but password and keyboard-interactive methods are rejected. The alias without-password is equivalent. |
| PubkeyAuthentication | yes | Explicitly enables public key authentication (already the default, but stated for clarity). |
| PasswordAuthentication | no | Eliminates the entire password-based brute-force attack surface. |
| MaxStartups | 20:30:60 | Throttles unauthenticated connections: allow 20 concurrent, begin probabilistic dropping at 30%, hard limit at 60. Reduces the chance of legitimate logins being crowded out during active scans. |
Validate the syntax and reload:
shell# Syntax check — must return no output on success
sshd -t
# Restart only after passing syntax check
systemctl restart ssh
Confirm the new state
shell# Verify listening port
ss -tlnp | grep ssh
# Verify effective configuration
sshd -T | grep -E '^port|permitrootlogin|pubkeyauthentication|passwordauthentication|maxstartups'
Expected state: SSH listens only on the new port ·
passwordauthentication no · pubkeyauthentication yes ·
permitrootlogin prohibit-password · maxstartups 20:30:60
Synchronising Fail2ban
After migrating the SSH port, Fail2ban must be updated to monitor the new port — otherwise it continues protecting port 22 while port 45871 is unwatched.
Fail2ban’s own documentation recommends placing local overrides in .local files
rather than modifying the default jail.conf directly. This ensures local changes
survive package updates.
mkdir -p /etc/fail2ban/jail.d
nano /etc/fail2ban/jail.d/sshd.local
/etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
port = 45871
logpath = %(sshd_log)s
backend = %(sshd_backend)s
shell
systemctl restart fail2ban
fail2ban-client status sshd
A healthy status output will show the jail as active with the updated port as its target.
Local Client Configuration
Typing -p 45871 -i ~/.ssh/id_admin on every connection is error-prone. The
OpenSSH client’s ~/.ssh/config file supports host aliases that encode all
connection parameters, accessed via a short name.
Host edge-node
HostName 203.0.113.10
User root
Port 45871
IdentityFile ~/.ssh/id_admin
shell
# Lock down the config file (required)
chmod 600 ~/.ssh/config
# Connect using the alias
ssh edge-node
File Transfer with scp & sftp
Both scp and sftp ride on top of the SSH transport, so they must
reference the new port and key. Note the port flag differs between the two tools:
| Tool | Port flag | Example |
|---|---|---|
| scp | -P (uppercase) |
scp -P 45871 backup.tar.gz root@203.0.113.10:/root/ |
| sftp | -P or -oPort= |
sftp -P 45871 root@203.0.113.10 |
Tip: If you configured the ~/.ssh/config alias in the previous
section, you can simply use sftp edge-node or scp -r ./files/ edge-node:/root/
— the port and key are resolved automatically from the alias.
The critical distinction to remember: ssh uses lowercase -p; both
scp and sftp use uppercase -P.
Ongoing Anomaly Monitoring
Port migration reduces scan noise significantly, but determined attackers will eventually discover any open port via a full port scan. Continue monitoring post-migration.
Live SSH log stream
shellsudo journalctl -u ssh -f
Fail2ban ban list
shellsudo fail2ban-client status sshd
sudo fail2ban-client get sshd banip
Active connections on the new port
shellsudo ss -tn '( sport = :45871 )'
If log noise on the new port continues to grow, consider supplementing with
port knocking (concealing the port until a specific knock sequence is received)
or restricting SSH access to specific source IP ranges via AllowUsers combined with
firewall rules. For very high-security environments, hardware security keys (e.g. FIDO2/YubiKey)
can be used with OpenSSH for a second authentication factor.
Baseline Status Checklist
After completing the above steps, the SSH management entry point of the Debian 13 server should meet the following minimum baseline:
- Default port 22 is no longer used for SSH
- Password authentication is disabled
- Root login via password is blocked (
prohibit-password) - Public key (Ed25519) authentication is configured and tested
- Fail2ban is active and monitoring the new port
- Local
~/.ssh/configalias is configured with correct port and key - scp and sftp are using the new port (
-Pflag or alias) - SSH logs and Fail2ban status are being observed post-migration
This configuration removes the most common, easily-automated attack vectors against SSH. It is a baseline, not an endpoint — regular OpenSSH package updates, periodic log review, and eventual consideration of additional controls (2FA, AllowUsers restrictions, post-quantum key exchange algorithms) form the ongoing security posture beyond this guide.
