Why public key auth is the single biggest lever
SSH is the most important gateway for server access, and also one of the most heavily automated attack targets on the internet. This article shows how key-based authentication with ed25519, disabled root login, fail2ban, and a hardened sshd_config reliably block brute-force attacks, rounded out with bastion host patterns for secure access to production servers.
Table of Contents
- 1. Why password authentication over SSH remains a persistent attack vector
- 2. Key-based authentication with ed25519 instead of passwords
- 3. Disabling root login and using sudo with a dedicated admin user
- 4. Changing the SSH port: real security gain or security theater?
- 5. Configuring fail2ban against SSH brute-force attacks
- 6. Hardening sshd_config: ciphers, MACs, KexAlgorithms, and timeouts
- 7. SSH agent forwarding: risks and safer alternatives
- 8. Bastion and jump host patterns for production access
- 9. SSH hardening compared: insecure vs. hardened
- 10. Summary
- 11. FAQ
1. Why password authentication over SSH remains a persistent attack vector
Port 22 is one of the most intensively scanned ports on the entire public internet. Automated botnets continuously sweep IPv4 address ranges for open SSH services and then test thousands of combinations of common usernames like root, admin, or ubuntu against the most frequent passwords found in leaked databases. Tools like Shodan make open SSH ports discoverable within seconds, so a freshly provisioned server with password authentication often sees its first brute-force attempts within a few hours, regardless of how obscure or new the IP address is.
The structural problem with passwords lies in human weakness, not in the cryptography itself: even a twelve-character, randomly generated password offers little protection if it's disclosed through a phishing email or identical to a password leaked from another service. Credential-stuffing attacks exploit exactly this reuse, automatically testing millions of leaked combinations against SSH servers. A single successful login is enough to gain full shell control of the server, often with the same privileges as a legitimate administrator, which makes SSH one of the most rewarding targets an attacker can go after.
2. Key-based authentication with ed25519 instead of passwords
Public key authentication replaces "something you know" with "something you have": the private key never leaves the user's machine, while only the public key is stored on the server in ~/.ssh/authorized_keys. An attacker who compromises the server finds only the public half there, which is worthless without its private counterpart. That removes the entire attack surface for brute-force and credential-stuffing attempts, because there's no longer a password to guess or reuse from a leak.
For new keys, the ed25519 algorithm is preferable to classic RSA: it's based on elliptic-curve cryptography, produces significantly shorter keys at equal or higher security, is more resistant to certain side-channel attacks, and computes noticeably faster. An RSA key, if still needed for compatibility reasons, should be at least 4096 bits, whereas an ed25519 key already offers security comparable to RSA-3072 or higher at a fixed, much shorter key length.
The actual hardening step is only complete once PasswordAuthentication no is set in sshd_config, combined with KbdInteractiveAuthentication no so that interactive password prompts via PAM don't remain a backdoor. Before disabling it, an existing SSH session must stay open and a login with the new key must be successfully tested in a second terminal, otherwise a typo in the configuration risks a complete lockout.
# Generate a new ed25519 key pair, protect it with a passphrase
ssh-keygen -t ed25519 -a 100 -C "deploy@mironsoft.de" -f ~/.ssh/id_ed25519_prod
# Securely copy the public key to the target server
ssh-copy-id -i ~/.ssh/id_ed25519_prod.pub deploy@server.mironsoft.de
# Test the login BEFORE disabling PasswordAuthentication
ssh -i ~/.ssh/id_ed25519_prod deploy@server.mironsoft.de "echo Key login successful"
# Use ssh-agent so the passphrase isn't required on every connect
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519_prod
3. Disabling root login and using sudo with a dedicated admin user
The username root is known on every Linux system and is therefore the first login name automated attacks try. A direct root login over SSH also means every action on the server happens under the same identity: there's no way to reconstruct afterward which person ran which command if multiple administrators share the same root password or the same root key. PermitRootLogin no in sshd_config disables direct login entirely and forces every access through a named, individual account.
The recommended replacement is a dedicated admin user per person who is a member of the sudo group and explicitly requests privileged commands via sudo. That brings two concrete benefits: first, sudo logs every executed command with a timestamp and username in /var/log/auth.log by default, which is what makes forensic analysis after an incident possible in the first place. Second, an individual person's access can be revoked granularly, for example when someone leaves the team, without having to change a shared root password for the whole team.
Anyone who still needs key-based root access for automation, for compatibility reasons, should set PermitRootLogin prohibit-password instead of no, which consistently blocks password logins as root but still allows key-based access. For the vast majority of production setups, though, a fully disabled root login over SSH combined with a sudo workflow is the more robust and better auditable solution.
4. Changing the SSH port: real security gain or security theater?
Moving SSH to a port other than 22 measurably reduces the volume of log noise from automated mass scanners, which primarily sweep the well-known default ports of common services. In practice, the number of daily login attempts often drops by over 90 percent after a port change, which makes /var/log/auth.log considerably easier to read and reduces the load on intrusion-detection systems. That effect is real, but it applies only to unspecific, broadly scattered attacks, not targeted ones.
Against an attacker specifically targeting a given IP address or domain, a changed port offers no meaningful protection: a full port scan with nmap -p- finds an open SSH service on any port within a few minutes. Security that relies exclusively on hiding a configuration detail is called security through obscurity and must never be the sole protective measure, since it's ineffective against targeted attacks and, if misprioritized, can even breed a false sense of safety.
In practice, there's little reason not to change the port as a supplementary measure, as long as it's rolled out carefully: firewall rules, monitoring dashboards, and documentation all need to be updated consistently, and restrictive corporate firewalls sometimes block outbound non-standard ports. Changing the port only makes sense in combination with the effective measures covered in this article, such as key auth and fail2ban, never as a substitute for them.
5. Configuring fail2ban against SSH brute-force attacks
fail2ban watches log files like /var/log/auth.log in real time, detects patterns of failed login attempts using regular expressions, and temporarily bans offending IP addresses via iptables or nftables. The concept behind it is called a "jail": each monitored service has its own configuration with filter rules and response thresholds, and the sshd jail ships as a preconfigured template in most distributions, only needing to be enabled.
The three key knobs are maxretry (number of allowed failed attempts), findtime (the time window in which those attempts are counted), and bantime (the ban duration). For production SSH servers, a value of three to five failed attempts within ten minutes has proven effective, followed by an escalating ban duration via bantime.increment, which grows exponentially for repeat offenses from the same IP address. It's important to consistently exempt your own administration IP range via ignoreip to avoid an accidental self-lockout from too many failed test connections.
For servers behind a cloud load balancer or CDN, fail2ban needs to extract the actual client IP from the correct log fields, instead of accidentally banning the upstream proxy's IP address. The current ban status can be checked at any time via fail2ban-client status sshd, which works well for a simple monitoring script or an alert on an unusually high number of banned IP addresses.
# /etc/fail2ban/jail.local: harden the sshd jail, define exceptions
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 4
findtime = 10m
bantime = 1h
bantime.increment = true
bantime.factor = 4
bantime.maxtime = 1w
ignoreip = 127.0.0.1/8 203.0.113.0/24
# Reload fail2ban after a configuration change
sudo fail2ban-client reload sshd
# List currently banned IP addresses in the sshd jail
sudo fail2ban-client status sshd
# Manually unban an IP address that was blocked by mistake
sudo fail2ban-client set sshd unbanip 203.0.113.42
For a simple security dashboard, the current fail2ban status can be exported regularly as a structured report, for example via a cron job script that parses fail2ban-client status sshd and enriches it with raw data from auth.log. A report like that shows at a glance which IP addresses are currently banned and which usernames are being targeted most often by login attempts.
{
"report_type": "ssh_brute_force_summary",
"generated_at": "2026-07-12T06:00:00Z",
"source": "fail2ban + auth.log",
"jail": "sshd",
"window_hours": 24,
"total_failed_attempts": 4821,
"unique_attacking_ips": 312,
"currently_banned_ips": 47,
"top_attacking_ips": [
{ "ip": "198.51.100.23", "attempts": 214, "usernames_tried": ["root", "admin", "test"], "status": "banned" },
{ "ip": "203.0.113.88", "attempts": 176, "usernames_tried": ["root", "ubuntu"], "status": "banned" },
{ "ip": "192.0.2.140", "attempts": 98, "usernames_tried": ["root"], "status": "banned" }
],
"most_targeted_usernames": [
{ "username": "root", "attempts": 3102 },
{ "username": "admin", "attempts": 611 },
{ "username": "ubuntu", "attempts": 340 }
]
}
6. Hardening sshd_config: ciphers, MACs, KexAlgorithms, and timeouts
Beyond the authentication method, sshd_config also determines which cryptographic algorithms are even allowed for transport encryption, integrity checking, and key exchange. Default installations often still permit outdated algorithms for compatibility reasons, such as CBC modes or diffie-hellman-group1-sha1, which are vulnerable to known attacks like the Lucky Thirteen attack or suffer from insufficient key lengths. An explicit allowlist using Ciphers, MACs, and KexAlgorithms restricts negotiation to exclusively modern, currently trusted algorithms such as chacha20-poly1305 and curve25519-sha256.
Beyond cryptography, connection limits reduce the attack surface against resource-based attacks: LoginGraceTime caps how long an open but not yet authenticated connection may stay alive before being automatically dropped, preventing thousands of half-open connections from overwhelming the server. MaxAuthTries caps the number of authentication attempts per connection at the server level, independent of fail2ban, and MaxStartups caps the total number of simultaneous unauthenticated connections.
AllowUsers or AllowGroups additionally restrict which system accounts may even log in via SSH at all, regardless of their other local permissions. Every change to sshd_config should be checked for syntax errors with sshd -t before reloading, since a failed restart of the SSH service on a remote server without out-of-band access can mean a complete lockout. The ssh-audit command-line tool inspects a running configuration externally and lists remaining weaknesses in a clear overview.
# /etc/ssh/sshd_config: hardened configuration (excerpt)
Port 2222
Protocol 2
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
# Only allow modern, secure algorithms
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512
# Connection and timing limits against resource-based attacks
LoginGraceTime 20
MaxAuthTries 3
MaxSessions 4
MaxStartups 10:30:60
ClientAliveInterval 300
ClientAliveCountMax 2
# Only explicitly allowed accounts may log in via SSH
AllowUsers deploy admin-jsmith
X11Forwarding no
AllowTcpForwarding local
7. SSH agent forwarding: risks and safer alternatives
SSH agent forwarding lets a locally running ssh-agent, including its loaded keys, be relayed to a remote host over an existing SSH connection, so that host can reach further servers without the passphrase being entered again. That's convenient for multi-hop connections through an intermediate server, but it carries a concrete risk: anyone with root privileges on the intermediate server, whether a legitimate administrator or an attacker after a compromise, can access the forwarded agent socket during the active connection and generate signatures with the user's private key on their behalf, without ever seeing the key itself.
The more robust alternative for the classic "reach server B through server A" use case is ProxyJump (shorthand -J), which transparently tunnels a connection through an intermediate host without ever exposing the agent socket on that host. If agent forwarding is unavoidable regardless, say for legacy workflows, ssh-add -c reduces the risk by requiring an explicit confirmation on the local machine for every use of the forwarded key, instead of signing silently.
8. Bastion and jump host patterns for production access
In a bastion host architecture, production servers are never directly reachable over SSH from the public internet; access happens exclusively through a single, dedicated, heavily hardened intermediate server, the bastion or jump host. This reduces the entire server fleet's attack surface to one consistently monitored entry point: internal servers no longer need a public IP address or a firewall rule reachable directly from the internet on port 22, which renders classic mass scans against the actual infrastructure useless.
Because the bastion host itself becomes the single most valuable target as a result, it must be hardened rigorously with every measure described in this article and kept as minimal as possible: no other services, no stored credentials for internal systems, full session logging, and, where possible, multi-factor authentication in addition to the key. ProxyJump in the client-side ~/.ssh/config makes the detour through the bastion host transparent for developers, so every connection doesn't have to be built manually in two separate steps.
For larger server fleets, it's also worth moving from statically distributed authorized_keys files to SSH certificates: an internal certificate authority signs short-lived user certificates, which servers accept via TrustedUserCAKeys without needing every individual public key maintained on every server. Revoking access then simply means no longer issuing a new certificate, instead of removing keys one by one across hundreds of servers.
# ~/.ssh/config: wire up the bastion host transparently via ProxyJump
Host bastion
HostName bastion.mironsoft.de
User jump-user
Port 2222
IdentityFile ~/.ssh/id_ed25519_prod
IdentitiesOnly yes
ForwardAgent no
Host prod-app-*
HostName %h.internal.mironsoft.de
User deploy
Port 2222
IdentityFile ~/.ssh/id_ed25519_prod
IdentitiesOnly yes
ProxyJump bastion
ForwardAgent no
ServerAliveInterval 60
# Terminal usage, the ProxyJump tunnel is built automatically:
# ssh prod-app-01
9. SSH hardening compared: insecure vs. hardened
The table below compares the measures covered in this article side by side and makes visible the concrete effect each setting has on an SSH server's attack surface.
| Area | Insecure pattern | Hardened pattern |
|---|---|---|
| Authentication | Password login, even a complex one | ed25519 key, PasswordAuthentication no |
| Root access | Direct root login allowed | PermitRootLogin no, sudo with audit log |
| Port | Default port 22, no further rate limiting | Optionally changed, combined with fail2ban |
| Brute-force protection | No fail2ban, unlimited login attempts | fail2ban with escalating bantime |
| Cryptography | Outdated ciphers/MACs (CBC, SHA1) | Modern allowlist (chacha20, hmac-sha2-etm) |
| Access architecture | Every server directly reachable from the internet | Bastion host, ProxyJump, no public port 22 |
Mironsoft
Server hardening, SSH security, and infrastructure protection for Magento hosting
Ready to properly secure SSH access to your servers?
We harden your server infrastructure from sshd_config all the way to the bastion host, set up fail2ban and monitoring, and make sure production access stays auditable and resistant to attack.
SSH security audit
ssh-audit analysis, review of sshd_config and key management
Bastion host setup
ProxyJump architecture, certificate-based access management
Monitoring & fail2ban
Brute-force detection, alerting on suspicious login patterns
10. Summary
Hardening SSH security primarily means replacing passwords with key-based authentication using ed25519 and consistently enforcing PasswordAuthentication no, combined with a disabled root login and a dedicated admin user with sudo rights for full auditability. fail2ban adds automated protection against brute-force attempts on top of that foundation, while a hardened sshd_config with modern cryptography, tight connection limits, and an explicit allowlist of permitted accounts further shrinks the remaining attack surface. Changing the port can meaningfully cut down log noise, but it doesn't replace any of these measures.
For environments with multiple servers, a bastion host architecture with ProxyJump is the most consistent next step: only a single, maximally hardened entry point is reachable from the internet at all, while the rest of the server fleet stays internal only. In larger teams, SSH certificates additionally cut the operational overhead of key management significantly, since access rights are managed centrally through a certificate authority instead of through distributed authorized_keys files.
Hardening SSH Security, the Essentials at a Glance
Keys over passwords
Generate an ed25519 key, only set PasswordAuthentication no after a successful login test.
Lock down root
PermitRootLogin no, dedicated admin user with sudo and a full audit log.
Enable fail2ban
sshd jail with escalating bantime, exempt your own IP ranges via ignoreip.
Use a bastion host
ProxyJump instead of agent forwarding, production servers with no public port 22.