Run journalctl -u ssh on a cloud server after it has been online for a day, and you may find hundreds of failed login attempts targeting usernames such as admin and oracle and these automated scans are common on internet-facing servers.
Reducing the attack surface means disabling password-based authentication, tightening the SSH daemon configuration, and adding network-level protections.
This guide focuses on Ubuntu 26.04 LTS, with notes for Ubuntu 24.04 where behavior or configuration differs.
How Ubuntu Loads sshd Configuration Files
Before changing SSH settings, it’s important to understand how sshd processes its configuration because this explains why some hardening changes appear to have no effect.
The main configuration file, /etc/ssh/sshd_config, can include additional files through a directive such as Include /etc/ssh/sshd_config.d/*.conf.
On Ubuntu, these drop-in files are typically processed in lexical order. For most directives, OpenSSH uses the first value obtained, so a setting in an earlier file can override a conflicting value in a later file.
Cloud images may also include a file such as 50-cloud-init.conf that enables password authentication. Adding PasswordAuthentication no at the bottom of the main configuration file may therefore fail to change the effective setting.
To make the hardening settings take precedence, create /etc/ssh/sshd_config.d/00-hardening.conf. Its filename places it before the usual 50-cloud-init.conf drop-in, allowing your settings to take effect when no earlier configuration directive overrides them.
Keep your existing SSH session to server1 (192.168.122.248) open while making these changes. Before applying any configuration, validate it with sshd -t and confirm the effective settings with sshd -T. Test a new SSH connection before closing your existing session to avoid locking yourself out.
1. Switch to Ed25519 Keys with ssh-keygen
Before disabling password authentication, make sure SSH key-based login works. Generate an Ed25519 key pair on the admin machine (192.168.122.1), then copy the public key to server1:
ssh-keygen -t ed25519 -a 100 -C "tecmint@admin" ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]
When prompted, set a passphrase to protect the private key if the key file is stolen.
-t ed25519generates an Ed25519 key pair that offers strong security with compact keys and fast operations.-a 100applies 100 rounds of key derivation when protecting the private key with a passphrase, increasing the cost of offline passphrase guessing.-Cadds a descriptive comment to help identify the key.
The ssh-copy-id command installs the public key in the remote account’s ~/.ssh/authorized_keys file. Test the connection before proceeding:
ssh -i ~/.ssh/id_ed25519 [email protected]
Confirm that you can log in successfully without entering the account password. If you configured a key passphrase, SSH may still prompt you for it.
2. Disable Password Logins with PasswordAuthentication
Once key-based login works, connect to server1 and create an SSH configuration drop-in file:
sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
Add these settings:
# /etc/ssh/sshd_config.d/00-hardening.conf PasswordAuthentication no KbdInteractiveAuthentication no
These directives close two common authentication paths:
PasswordAuthentication noprevents SSH clients from authenticating through the standard password authentication method.KbdInteractiveAuthentication nodisables keyboard-interactive authentication, which PAM can otherwise use to prompt users for passwords or other credentials.
Save the file, then validate the SSH configuration and inspect the effective values:
sudo sshd -t sudo sshd -T | grep -Ei '^(passwordauthentication|kbdinteractiveauthentication)'
If the configuration is valid and both settings are disabled, the output should be:
passwordauthentication no kbdinteractiveauthentication no
If either value is still yes, inspect the main configuration file and all included drop-ins for an earlier conflicting directive.
Remember that OpenSSH generally uses the first value obtained for a directive, so a file named 00-hardening.conf cannot override a conflicting value already read earlier.
After confirming the settings, restart SSH:
sudo systemctl restart ssh
Keep your existing session open. From the admin machine, test a connection with public-key authentication explicitly disabled:
ssh -o PubkeyAuthentication=no \
-o PreferredAuthentications=password \
[email protected]
The server should reject the connection because password authentication is disabled. Depending on the server’s available authentication methods and client version, the error may resemble:
Permission denied (publickey).
Once this test confirms the expected behavior and a separate key-based connection still works, you can safely continue with the remaining SSH hardening steps.
PasswordAuthentication no line was ignored, share it with someone still editing the bottom of sshd_config.3. Block Direct Root Logins with PermitRootLogin
Even after disabling password authentication, the root account can still log in using an SSH key if PermitRootLogin is set to prohibit-password.
Disabling direct root access ensures administrators connect through named user accounts and use sudo for privileged operations, improving accountability in system logs.
Open the existing hardening file:
sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
Add the following directive:
PermitRootLogin no
Save the file, validate the configuration, and restart SSH:
sudo sshd -t && sudo systemctl restart ssh
The PermitRootLogin no setting blocks SSH login directly as root, regardless of whether the client uses a password or a key. Administrators must connect using an approved user account and elevate privileges with sudo.
Keep your current SSH session open and test a new connection using your administrative account before closing it.
4. Allow Only Approved Users with AllowGroups
Disabling root access and password authentication still leaves other eligible accounts able to connect with SSH keys. Use AllowGroups to restrict SSH access to members of a dedicated group.
First, create the group and add your administrative account:
sudo groupadd sshusers sudo usermod -aG sshusers tecmint
If the group already exists, skip groupadd. Next, add this directive to /etc/ssh/sshd_config.d/00-hardening.conf:
AllowGroups sshusers
Validate and apply the configuration:
sudo sshd -t && sudo systemctl restart ssh
The AllowGroups directive permits SSH access only to users whose group memberships match the configured group. Users outside sshusers will be denied access, even if their accounts and public keys are otherwise valid.
sshusers before applying this restriction. If you administer the server through multiple accounts, add every required account to an approved group first. Existing sessions may remain connected even after a user is no longer eligible to establish a new SSH session.To verify group membership, run:
id tecmint
To investigate rejected connections, inspect the SSH service journal:
sudo journalctl -u ssh –since “10 minutes ago”
Look for messages indicating that a user’s groups are not permitted by AllowGroups.
5. Slow Down Guessing with MaxAuthTries and LoginGraceTime
With SSH access restricted to approved users, the next step is to limit authentication attempts and prevent unauthenticated connections from occupying resources indefinitely.
Open the hardening file again:
sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
Add the following configuration:
MaxAuthTries 3 LoginGraceTime 30 MaxStartups 10:30:60
Here’s what each setting does:
MaxAuthTries 3limits the number of authentication attempts permitted per connection. Clients offering several keys through an SSH agent may hit this limit before reaching the intended key.LoginGraceTime 30gives a client 30 seconds to complete authentication before the server disconnects it.MaxStartups 10:30:60limits concurrent unauthenticated connections. Once more than 10 unauthenticated connections are open, the server begins probabilistically dropping new connections, increasing the rejection probability until it reaches 100% at 60 concurrent connections.
Save the file and apply the changes:
sudo sshd -t && sudo systemctl restart ssh
If a client reports too many authentication failures, specify the intended identity explicitly:
ssh -o IdentitiesOnly=yes \
-i ~/.ssh/id_ed25519 \
[email protected]
The IdentitiesOnly yes option prevents SSH from offering unrelated identities held by the agent when connecting to this host.
What About PerSourcePenalties?
OpenSSH 9.8 introduced PerSourcePenalties, which can temporarily refuse connections from client addresses that repeatedly fail authentication or exhibit other undesirable behavior. Ubuntu 26.04 LTS ships OpenSSH 10.2p1, which includes this feature.
Ubuntu 24.04 LTS ships OpenSSH 9.6p1 in its original release package, so the feature is not available there by default. Check the installed version and configuration before relying on it.
Check the installed version with:
ssh -V sudo sshd -T | grep -i '^persourcepenalties'
If supported, inspect the effective PerSourcePenalties configuration before changing its defaults. This feature provides an additional layer of protection, but it does not replace firewall restrictions, key-based authentication, or monitoring.
6. Disable Unused Forwarding with AllowTcpForwarding
An authenticated SSH session can also be used to tunnel traffic into other systems or expose services through the server. If your server does not need these capabilities, disable them to reduce the attack surface.
Open the existing hardening file:
sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
Add the following configuration:
X11Forwarding no AllowAgentForwarding no AllowTcpForwarding no
Each setting controls a different SSH feature:
X11Forwarding nodisables forwarding graphical applications over SSH. This is useful on headless servers that do not need remote graphical applications.AllowAgentForwarding noprevents SSH clients from forwarding their local authentication agent through the server. This reduces the risk of an authenticated but compromised account being used to access the forwarded agent.AllowTcpForwarding nodisables TCP port forwarding, including local (ssh -L) and remote (ssh -R) tunnels.
These restrictions are appropriate for servers that provide shell access only. However, do not disable forwarding blindly on bastion hosts or jump servers.
ProxyJump itself does not require TCP forwarding on the jump host in the same way that application tunnels do, but the SSH connection through the jump host relies on its ability to establish the requested outbound SSH connection. Review your access architecture before applying restrictions.
Save the file, validate the configuration, and restart SSH:
sudo sshd -t && sudo systemctl restart ssh
Keep an existing administrative session open and test a new connection before closing it. If users depend on tunnels, agent forwarding, or graphical applications, verify those workflows separately.
7. Close Abandoned Sessions with ChannelTimeout
SSH provides keepalive settings that help detect clients that have become unreachable. However, these settings do not automatically terminate a connected user simply because they have stopped typing.
To detect unresponsive clients and place a limit on idle shell sessions, add the following directives to /etc/ssh/sshd_config.d/00-hardening.conf:
ClientAliveInterval 300 ClientAliveCountMax 2 ChannelTimeout session=30m
These settings serve different purposes:
ClientAliveInterval 300sends an encrypted keepalive request to the client after 300 seconds without receiving data from it.ClientAliveCountMax 2allows two unanswered keepalive requests before the server disconnects the client. With these values, an unresponsive connection is generally disconnected after approximately 10 minutes, depending on when the connection becomes unresponsive and when the checks occur.ChannelTimeout session=30mcloses an interactive session channel after 30 minutes without relevant channel activity. It requires OpenSSH 9.2 or later.
The distinction matters: a client that remains connected and responds to keepalive requests can stay connected even if its user is away from the keyboard.
ChannelTimeout provides a separate control for idle session channels, although it is not a universal inactivity timer for every type of SSH channel.
After saving the file, validate and apply the settings:
sudo sshd -t && sudo systemctl restart ssh
Check the effective values with:
sudo sshd -T | grep -Ei '^(clientaliveinterval|clientalivecountmax|channeltimeout)'
If ChannelTimeout is unsupported, sshd -t will report a configuration error. Check the installed OpenSSH version and consult man 5 sshd_config before using it, especially on Ubuntu 24.04 systems with the original OpenSSH 9.6 release.
8. Remove Weak Algorithms with ssh-audit
So far we’ve controlled who connects and for how long, and ssh-audit now checks the encryption. Scan from the admin machine:
sudo apt install ssh-audit ssh-audit 192.168.122.248
Review the output for weak, deprecated, or otherwise discouraged algorithms. The tool’s fail and warn labels are recommendations based on its security checks, so evaluate each finding against your OpenSSH version, compatibility requirements, and security policy before changing the configuration.
On server1, inspect the effective cryptographic defaults before overriding them:
sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
Add the following configuration:
KexAlgorithms mlkem768x25519-sha256,[email protected],curve25519-sha256 Ciphers [email protected],[email protected],[email protected] MACs [email protected],[email protected]
Here
KexAlgorithmsprefers post-quantum hybrids, with Curve25519 for older clients. Removemlkem768x25519-sha256on Ubuntu 24.04, since it needs OpenSSH 9.9 andsshd -totherwise fails withBad SSH2 KexAlgorithms.CiphersandMACskeep only authenticated and encrypt-then-MAC modes.
Run ssh-audit again after restarting to confirm the warnings are gone.
9. Restrict SSH by Source Address with UFW
The next layer stops unwanted traffic before it reaches sshd. If admins connect from a known network, allow SSH only from there, adding the rule before enabling UFW so your session survives:
sudo ufw allow from 192.168.122.0/24 to any port 22 proto tcp sudo ufw enable
Servers that must accept SSH from anywhere can use sudo ufw limit 22/tcp instead, which denies an address opening six or more connections in 30 seconds.
10. Ban Repeat Offenders with Fail2Ban
UFW handles addresses you can predict, and Fail2Ban bans the rest after reading failed logins from the journal:
sudo apt install fail2ban sudo nano /etc/fail2ban/jail.local
Add the following configuration:
# /etc/fail2ban/jail.local [sshd] enabled = true backend = systemd maxretry = 3 findtime = 10m bantime = 1h ignoreip = 127.0.0.1/8 192.168.122.1
Here:
backend = systemdreads SSH events from the journal.maxretry,findtime, andbantimeban an address for an hour after three failures in ten minutes.ignoreipkeeps your admin machine from being banned.
Restart the service and check that the jail lists currently banned entries:
sudo systemctl restart fail2ban sudo fail2ban-client status sshd
If it reports Sorry but the jail ‘sshd’ does not exist, test the file with sudo fail2ban-client -t.
Monitor SSH Logins and Recover from Lockouts
Hardening SSH is not a one-time task. Monitor authentication logs regularly to identify failed login attempts, unexpected access, and suspicious activity.
To record the fingerprints of public keys used during successful authentication, add the following directive to /etc/ssh/sshd_config.d/00-hardening.conf:
LogLevel VERBOSE
The VERBOSE logging level records additional authentication details, including the fingerprint of the public key used for a successful public-key login. Validate and apply the change:
With ten layers in place, the remaining job is watching logins. Adding LogLevel VERBOSE to the drop-in file records each key’s fingerprint, and one command shows the day’s activity:
sudo sshd -t && sudo systemctl restart ssh
On Ubuntu, use journalctl to review today’s SSH authentication activity:
sudo journalctl -u ssh --since today \
| grep -E 'Accepted|Failed|Invalid user'
Look for successful logins from unexpected addresses, repeated authentication failures, and attempts involving nonexistent accounts. Remember that log entries filtered with grep provide only a subset of the available evidence. Review the complete journal when investigating suspicious activity:
sudo journalctl -u ssh --since today
For ongoing monitoring, consider combining journal reviews with Fail2Ban, alerting, and your cloud provider’s security monitoring tools.
Recovering from an SSH Lockout
If a configuration mistake prevents new SSH connections, use an alternative access method such as your cloud provider’s serial console, recovery console, or a VM console.
For a locally managed virtual machine, virsh console may provide access if the guest has a configured serial console.
Once you regain administrative access, move the hardening drop-in out of the configuration directory and validate the remaining configuration:
sudo mv /etc/ssh/sshd_config.d/00-hardening.conf \
/root/00-hardening.conf.disabled
sudo sshd -t
If validation succeeds, restart SSH:
sudo systemctl restart ssh
Moving the file disables all the directives it contains, so this should be a temporary recovery measure rather than the final fix. Correct the offending setting, restore the intended hardening rules, and validate the configuration before reapplying them.
Confirm that key-based login works, root login is disabled, only approved users can connect, firewall rules permit legitimate administrators, and you have tested a recovery method.
A secure SSH configuration should reduce attack exposure without locking authorized administrators out of their own servers.
Conclusion
SSH hardening works best when multiple controls work together. By enforcing Ed25519 key-based authentication, disabling direct root access, restricting login groups, limiting authentication attempts, and applying firewall rules, you can significantly reduce the attack surface of an Ubuntu server.
Fail2Ban adds another layer by temporarily banning IP addresses that trigger repeated authentication failures.
Keeping your settings in a dedicated sshd_config.d drop-in file makes the configuration easier to maintain across servers. Before deploying it elsewhere, verify that each server supports the configured directives and that the rules match its access requirements.
Use sshd -t to validate the syntax and sshd -T to inspect the effective settings.
How do you secure SSH on your servers? Which settings do you apply everywhere, and which ones have caused unexpected problems?
Share your 00-hardening.conf configuration or a troubleshooting experience in the comments. Your experience could help another administrator avoid a costly lockout.





