01

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.

02

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.

03

Auditing the Current SSH State

Read the four parameters that define your current exposure surface:

shell
grep -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.

04

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 shell
ssh-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 output
cat ~/.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.

05

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.

local shell (separate 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.

06

Hardening sshd_config

Back up the existing configuration before making any changes:

shell
cp /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_config
Port                   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

07

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.

shell
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.

08

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.

~/.ssh/config
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
09

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:

ToolPort flagExample
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.

10

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

shell
sudo journalctl -u ssh -f

Fail2ban ban list

shell
sudo fail2ban-client status sshd
sudo fail2ban-client get sshd banip

Active connections on the new port

shell
sudo 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.

11

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/config alias is configured with correct port and key
  • scp and sftp are using the new port (-P flag 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.