If you manage a Linux server exposed to the Internet, sooner or later you will see in the logs thousands of SSH login attempts from random IP addressesIt's not that they've taken a dislike to you: they are automated bots that go around IP ranges looking for poorly protected doors.
On a small server, those attempts can translate into CPU usage spikes, performance degradation, and even occasional service outagesThe good news is that Linux offers an arsenal of tools to limit password attempts, block aggressive IPs, and harden SSH to make it very difficult for an attacker.
Detecting SSH brute-force attacks and their impact on the server
Before we start configuring anything, it's helpful to understand how to detect if we're experiencing a brute-force attack against SSH or other servicesand what effects it has on the machine.
A very typical symptom is seeing a a sudden increase in CPU usage without any change in legitimate traffic or database loadFor example, you might have a CPU spike for a few minutes while memory and disk usage remain virtually flat, which usually points to compute-intensive processes (such as many authentication attempts) rather than heavy queries.
To analyze what was happening in a specific interval, it is very useful to use journalctlThe systemd tool for reading system, service, kernel, and authentication logs. A classic example of a query would be:
journalctl --since "2025-11-16 13:10" --until "2025-11-16 13:16"
With that type of query you can review in detail What messages did the system register during the window in which the CPU spiked?: services that restart, authentication failures, kernel errors, etc.
In many cases you will find repeated lines related to SSH, such as "Failed password for" indicating failed login attemptsThat's practically synonymous with bots brute-forcely testing credentials.
Quantify failed attempts in /var/log/auth.log
On Debian/Ubuntu systems, the key file for tracking anything related to authentication is /var/log/auth.logThis is where successful logins, failed attempts, PAM events, account lockouts, etc. are recorded.
If you want to know how many times the pattern has been recorded "Failed password" in the current log, you can use:
sudo grep 'Failed password' /var/log/auth.log | wc -l
The result can be surprising: it's not uncommon to see thousands of failed attempts accumulated in a matter of hoursAnd remember that's just the current file.
Since the logs are rotated, it's important to also review the older files. You can see what time interval the current log covers with something like:
head -n 1 /var/log/auth.log
tail -n 1 /var/log/auth.log
That way you'll know the Date of the first and last record present in auth.logThe rest of the previous attempts will be in auth.log.1 and in the compressed files auth.log.N.gz.
To review compressed old logs and count failed password attempts, you can use:
zgrep 'Failed password' /var/log/auth.log.*.gz | wc -l
If you add up what you see in auth.log, auth.log.1 and auth.log.*.gz You'll get a good idea of the Historical record of brute-force attacks, taking into account that the oldest ones may have already disappeared due to the rotation.
How authentication log rotation works
The way and frequency with which the logs rotate depends on the configuration of logrotateIn Ubuntu, the rotation of auth.log is usually defined in /etc/logrotate.d/rsyslog, where you will typically find something like:
weekly
rotate 4
compress
That means that it The log is rotated weekly, four old copies are kept, and the old ones are compressed into .gz files.A daily cron job is responsible for running logrotate and applying these rules.
Therefore, when you calculate your brute-force attempts, assume that You're only seeing the historical data from several weeks ago.Anything that happened further back in time will no longer be in the system.
Identify attacking users and IPs
Beyond the total count, it's important to know which users are being targeted and from which IP addresses are the attacks originating?With a little awk on auth.log you have it at hand.
To see which usernames are being tried in the failed attempts:
sudo grep 'Failed password' /var/log/auth.log \
| awk '{print $(NF-5)}' \
| sort | uniq -c
And to see the IPs with the most suspicious activity:
sudo grep 'Failed password' /var/log/auth.log \
| awk '{print $(NF-3)}' \
| sort | uniq -c | head
This will allow you to quickly detect if they are attacking. real accounts from your system or generic users such as root, admin, test, user, etc., and which IPs you should block most urgently.
Is it really so serious that there are thousands of attempts?
It must be assumed that, if your server is accessible from the Internet and has SSH listening on port 22 or any otherThis server will receive this type of traffic 24 hours a day. It's normal to see thousands of failed attempts if the server has been running for any length of time.
The actual gravity depends on your settings:
| Configuration | Approximate risk |
|---|---|
| Weak passwords + open SSH to the Internet | Very high risk of compromise |
| Strong passwords | Moderate risk, but resource consumption |
| Fail2ban properly configured | Low risk and highly mitigated attacks |
| Access only with SSH keys | Risk very close to zero by brute force |
In other words: automated attacks are commonplace, but if your configuration is lax, Just one failed password is enough to cause you to lose the entire server.That's why it's so important to limit attempts, block IPs, and, where possible, eliminate passwords.
Basic SSH hardening: critical options in sshd_config

The first line of defense is within oneself SSH daemon (sshd) and its configuration file /etc/ssh/sshd_config (and the associated .d files). A few well-tuned directives greatly reduce the attack surface.
Keep in mind that in modern distributions like Ubuntu 22.04 and later, sshd first reads /etc/ssh/sshd_config and then the files in /etc/ssh/sshd_config.d/*.conf in alphabetical order. Anything that appears later may overwrite previously defined parameters, so be very careful what you change.
Disable passwordless access and unnecessary sessions
Although it comes well configured in most modern distributions, it doesn't hurt to confirm that Logins without a defined password are not allowed.The key directive is:
PermitEmptyPasswords no
It's usually commented out or explicitly set to "no". Also, make sure you have features you won't use disabled, such as... X11 Forwarding (X11Forwarding) If you don't do remote graphics sessions:
X11Forwarding no
Regarding protocols, if for whatever reason you manage a very old system, check that only certain actions are allowed. SSH protocol 2:
Protocol 2
Change the default port and limit which interface it listens on.
Another simple, though not foolproof, trick is Move SSH from port 22 to a non-standard port.This doesn't protect you from a serious attacker, but it filters out a significant amount of automated noise that only scans the 22.
Port 2222
ListenAddress 192.168.56.8
In addition to changing the port, you can specify a specific address on which SSH will listen, for example, your internal IP address if you want to confine it to a specific network. However, if your distribution uses systemd's ssh.socket, you might need to... Disable the socket and return to the classic ssh.service. to respect the port configuration:
sudo systemctl disable ssh.socket
sudo systemctl daemon-reload
sudo systemctl enable ssh.service
sudo systemctl start ssh.service
Whenever you change the port, test the connection from another terminal before closing the main session, so you don't get stuck without remote access.
Block or limit root access
The root user is easy prey for attackers, so it makes sense. prevent connecting via SSHeven with passwords. You control this behavior with the directive:
PermitRootLogin no
In many modern installations, it comes in "prohibit-password" mode, which prevents password logins but leaves the door open for certificates. If you want to be safe, leave it set to "no" and use a regular account with sudo for administration.
Define who can access: AllowUsers and AllowGroups
By default, any user with valid shell and defined password You can try connecting via SSH. That's usually not ideal on production servers, where perhaps only two or three accounts should have access.
To limit the allowed users, you have the following directives. AllowUsers y AllowGroups. For example: uterine
AllowUsers harry hermione
AllowGroups gryffindor
The list is separated by spaces and the semantics are those of a "whitelist": Only listed accounts and groups will be able to authenticateAlso keep in mind that AllowUsers takes precedence over AllowGroups, so don't mix them unless you're clear on the order of evaluation.
A good practice is to work primarily with typical groups. sshusers or admins and add the authorized accounts there, instead of keeping a list of users one by one in the file.
Limit authentication attempts and downtime
Another layer of protection is in Reduce how many failed attempts are allowed on a single connection and how long a session can remain inactive. For the first question, you can use:
MaxAuthTries 3
With this, the server will close the connection after three incorrect attempts, which This makes any attack that tries many passwords against the same SSH session less effective..
Regarding idle time, SSH allows you to terminate connections that remain open with no activity beyond a defined threshold. ClientAliveInterval (in seconds):
ClientAliveInterval 180
After three minutes of no traffic, the server will send keepalive messages and, if the client does not respond, The session will close automaticallyIt's a way to reduce risks from forgotten, unlocked terminals.
Restrict access by IP address: TCP Wrappers and Match
In some scenarios you might be interested in Only certain IPs or ranges can access via SSHYou have several ways to do it: from the firewall itself (iptables/nftables), through TCP Wrappers, to the Match blocks of sshd_config.
With TCP Wrappers, still used in many distributions, access is controlled with /etc/hosts.allow and /etc/hosts.deny. The flow is: First, hosts.allow is evaluated, and then hosts.deny.A restrictive example would be:
# /etc/hosts.deny
ALL: ALL
# /etc/hosts.allow
sshd: 192.168.1.89 192.168.1.55
sshd: ALL: DENY
With that configuration, Only two specific hosts will be able to connect via SSHand the rest will be denied. It is very effective in closed environments, although less flexible than a good modern firewall.
Another option, more typical of SSH, is to use blocks Match within sshd_config to apply rules based on address or user. Imagine you want a user "git" to be able to log in from anywhere, but your administrator user "greg" can only log in from the LAN 192.168.1.0/24. You could combine AllowUsers with Match Address rules, although you must be very careful to... don't close yourself off from yourself.
Fail2ban: Automatic blocks versus brute force
Even if you strengthen SSH, bots will still try credentials, causing CPU usage and log noise. To mitigate this, this comes into play. Fail2ban, a log-based intrusion prevention system which automatically blocks IPs with too many errors.
Fail2ban is written in Python and relies on "prisons" or jails, each associated with a service and one or more log filesWhen it detects a repeated error pattern (password failures, forbidden access, etc.), it triggers actions, usually firewall rules to block the source.
Install Fail2ban on common Linux distributions
The basic installation is quite straightforward using your distribution's package manager. On Ubuntu or Debian, this would suffice:
sudo apt update
sudo apt install fail2ban
In RHEL-based systems (RHEL, CentOS, AlmaLinux, Rocky, etc.) the typical command would be with dnf or yum, depending on the version:
sudo dnf install fail2ban
The package usually includes a systemd service that starts automatically, although it's worth checking that Fail2ban starts at boot and is active:
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo systemctl status fail2ban
Configuration structure: jail.conf, jail.local, and jail.d
The configuration lives in /etc/fail2ban/The main file is jail.conf, but editing it directly is not recommended because it gets overwritten during updates. Instead, you should:
- Create or edit /etc/fail2ban/jail.local to overwrite default values.
- Or add specific files to /etc/fail2ban/jail.d/*.conf.
Fail2ban loads the configuration in this order:
/etc/fail2ban/jail.conf
/etc/fail2ban/jail.d/*.conf
/etc/fail2ban/jail.local
Everything you define in jail.local takes precedence over everything that came before, so You can customize without touching the package files..
Important global parameters: bantime, maxretry, ignoreip
Inside jail.conf (or jail.local) you'll see a [DEFAULT] section with global parameters that affect all jails unless overridden. The most important ones are:
- bantime: time during which an IP will be blocked (in seconds) after exceeding the number of allowed failures.
- maxretry: maximum number of failed attempts before applying the ban.
- findtime: time window in which those attempts are counted (e.g., 10m for ten minutes).
- ignoreip: list of IPs or ranges that Fail2ban should never block (for example, your own public IP or your management network).
For example, you could have something like this in [DEFAULT]:
[DEFAULT]
bantime = 600
findtime = 600
maxretry = 5
ignoreip = 127.0.0.1/8 192.168.1.0/24
With that configuration, Any IP address that fails five times in ten minutes will be blocked for ten minutes., unless it is part of the ignored ranges.
Configure jail sshd to stop SSH attacks
The most common jail is the SSH jail. In many distributions, it comes pre-configured in jail.conf; you just need to activate it or fine-tune its values in jail.local. A simple example:
[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 600
findtime = 10m
In this case, Three failed login attempts logged in auth.log within ten minutes will result in a ten-minute block on the attacker's IP address.Fail2ban injects rules into the firewall (iptables, nftables or UFW, depending on the system) so that connections from that IP do not even reach sshd.
To apply any configuration changes, remember to restart the service:
sudo systemctl restart fail2ban
View the status of prisons and blocked IPs
Fail2ban includes a very handy control utility, fail2ban-clientWith this tool, you can see which prisons are active and which IPs have been blocked. For example:
sudo fail2ban-client status
It would show something similar to:
Status
|- Number of jail: 1
`- Jail list: sshd
For detailed information about the SSHD jail:
sudo fail2ban-client status sshd
The output includes, among other fields, the number of currently banned IPs and the total historical number of addresses that have been blocked at least once, plus the ongoing IP list.
With this data you can get an idea of How much malicious traffic is your server receiving and how effective is the current configuration?.
Permanent locks and incremental bantime
If you want to be especially tough on repeat offenders, Fail2ban offers two interesting strategies: permanent ban and incremental ban.
To permanently ban any IP address that exceeds the failure threshold, simply enter:
bantime = -1
With that adjustment, Sanctioned IPs are never automatically unblockedYou can only remove them manually if you need to.
More flexible is the incremental mechanism, in which each recidivism increases the ban time according to a factor:
bantime = 10m
bantime.increment = true
bantime.rndtime = 0
bantime.factor = 4
bantime.maxtime = -1
With these values, the progression would look something like this:
- 1rd block: 10 minutes
- 2nd block: 40 minutes
- 3rd block: 160 minutes (~2 hours and 40)
- 4th block: around 10,6h
- 5th block: some 42h
Since bantime.maxtime is -1, the duration can continue to grow indefinitely, leaving the really heavy hitters out of the game forever.
Using Fail2ban beyond SSH: Apache, WordPress, MySQL, online stores…
Once you get the hang of Fail2ban, the logical thing to do is to extend it to protect other sensitive services besides SSH: web administration panels, CMS, databases, etc.
For example, for an online store (Magento, PrestaShop, WooCommerce…) it makes a lot of sense to create a jail that monitors the Apache or Nginx access logs looking for many 401/403 codes in /admin or /loginA minimal Apache-based configuration could be:
[apache-auth]
enabled = true
filter = apache-auth
logpath = /var/log/apache2/access.log
maxretry = 5
bantime = 3600
In WordPress environments, a common combination is to monitor /wp-login.php and /xmlrpc.phpThese are the classic entry points for brute-force and bot attacks. The filter could be placed in /etc/fail2ban/filter.d/wordpress.conf:
[Definition]
failregex = .*"POST /wp-login.php HTTP.*" 403
ignoreregex =
And the corresponding jail in jail.local:
[wordpress]
enabled = true
filter = wordpress
logpath = /var/log/apache2/access.log
maxretry = 3
bantime = 3600
The same idea applies to exposed databases (something generally best avoided): if you want to protect MySQL from continuous failed access attempts, you can create a filter for the “Access denied for user” messages in the error log:
[Definition]
failregex = ^<HOST>.*Access denied for user.*$
ignoreregex =
And then jail:
[mysqld-auth]
enabled = true
filter = mysql
logpath = /var/log/mysql/error.log
maxretry = 5
bantime = 1800
On hosting servers with control panels like cPanel or PleskFail2ban also integrates well: it can monitor mail services, Apache, FTP, and even the control panel itself, blocking IPs that overstep the mark with login attempts.
Authentication with SSH keys: the end of password attacks
All of the above helps, but the real leap in quality comes when you decide Stop using passwords for SSH and switch to public/private keysAt that point, brute-force password attacks cease to make sense.
The idea is simple: each legitimate user has a key pair, a private key that stays on your device and a public key that is copied to the server in the corresponding user's ~/.ssh/authorized_keys file.
When the client connects, it does not send the private key; it sends the public key and then signs a server challenge with the private one. The server checks that signature against the public one, and only if they match does it allow access.
Why SSH keys negate password brute force
In a classic password scheme, the attacker simply has to try strings of text until one matches the stored password (or its hash). Although a good password has many possible combinations, We're talking about orders of magnitude like 10¹⁰ possibilities for average passwords.
A typical 256-bit SSH key (like those in Ed25519) moves in search spaces on the order of 10⁶¹⁷ combinationsIn practice, it is mathematically impossible for an attacker to guess a private key by brute force with modern computers.
But what's more, the server doesn't even try to calculate anything if the The public key presented is not in authorized_keysIn that case, it discards the connection almost immediately, without invoking PAM or traditional authentication processes, so CPU consumption during a massive attack is minimal.
Generate and verify SSH keys on the client
Before accessing the server, check if your machine already has a generated SSH key pair. Simply list the contents of ~/.ssh:
ls -l ~/.ssh
If you see files like id_ed25519 and id_ed25519.pub If you have id_rsa and id_rsa.pub, you already have a valid pair. Ed25519 is more modern and lightweight, so it's usually the best option these days.
If you don't have keys, generate new ones with:
ssh-keygen -t ed25519 -C "tu_usuario@tu_equipo"
The command will create two files:
- id_ed25519: the private key, which you should never share.
- id_ed25519.pub: the public key, which you can copy to the servers.
You can view the contents of the public key with:
cat ~/.ssh/id_ed25519.pub
Copy the public key to the server and test access
On the server, make sure that the ~/.ssh directory exists for the user you will be logging in as (for example, git or your administrator user) and that it has permits 700:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
Then add the content of your client's id_ed25519.pub to ~ / .Ssh / authorized_keys (creating the file if it doesn't exist) and give it permissions 600:
echo "TU_PUBLIC_KEY" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
Remember to replace YOUR_PUBLIC_KEY with the complete line you saw using `cat` on your machine. From there, you can test the connection by explicitly specifying the key if you wish.
ssh -i ~/.ssh/id_ed25519 usuario@IP_DEL_SERVIDOR
If everything is fine, The server won't ask for a password and you'll jump directly to the shellAt that point you're ready to consider disabling password authentication.
Completely disable PasswordAuthentication on the server
Once you've verified that you can access your account with your password from at least one device (ideally two, in case you lose one), it's a good idea Disable password login for SSHThis nips in the bud any attempt at classic brute force.
Before touching the configuration, it's a good idea to see which files are defining PasswordAuthentication, because in many modern installations There are .d files that overwrite the value of the main sshd_config:
sudo grep -R "PasswordAuthentication" /etc/ssh/
It is common to find something like:
/etc/ssh/sshd_config.d/50-cloud-init.conf:PasswordAuthentication yes
/etc/ssh/sshd_config:PasswordAuthentication no
In that case, the effective configuration will be "yes" because the 50-cloud-init.conf file is loaded afterward and overwrites the value. You can verify the final result that sshd is applying with:
sudo sshd -T | grep passwordauthentication
To disable real passwords, edit the responsible file (for example /etc/ssh/sshd_config.d/50-cloud-init.conf) and leave:
PasswordAuthentication no
Then restart the SSH service:
sudo systemctl restart ssh
And double-check with:
sudo sshd -T | grep passwordauthentication
If it returns "no", Password login attempts will be rejected immediatelyPrograms like PuTTY will display an error because they cannot provide password-type credentials, but your clients with keys will continue to function without issue.
Combining SSH keys with Fail2ban and other measures
When you remove PasswordAuthentication from the equation, the value of Fail2ban for SSH becomes more helpful than critical, since The bots don't even have a field to enter the password in.Even so, it is advisable to keep the sshd jail active because it serves as an additional layer against unusual attempts of other types or misuse of keys.
If this combo of SSH keys only + Fail2ban + root lock + finely tuned AllowUsers/AllowGroups lists Add a restrictive firewall (iptables/nftables, UFW, firewalld) and, if appropriate, access control lists with TCP Wrappers, and you will have reduced the probability of brute-force intrusion to a negligible level.
In even more sensitive environments, you can go a step further and introduce two-factor authentication (2FA) for SSHusing modules like Google Authenticator or similar via PAM, or even restricting who can access the SSH port using dedicated VPNs.
With all these elements well integrated—log detection, careful sshd configuration, extensive use of Fail2ban, SSH keys instead of passwords, and, when necessary, IP-based controls—a Linux server can handle the continuous brute-force attacks on SSH and other exposed serviceswhile maintaining convenient and secure access for administrators.