Skip to main content

Hardening an AlmaLinux 8 Server Against SSH Brute-Force Attacks and Bots

Hardening an AlmaLinux 8 Server Against SSH Brute-Force Attacks and Bots

This guide documents the security hardening performed on an AlmaLinux 8 server running GitLab. The server was receiving a large number of automated SSH login attempts from external IP addresses.

The goal was to improve security without accidentally locking legitimate administrators out of the server.

1. Initial Security Problem

During SSH login, the server reported a large number of failed authentication attempts:

There were xxx failed login attempts since the last successful login.

We checked the effective SSH configuration:

sudo sshd -T | egrep 'permitrootlogin|passwordauthentication|pubkeyauthentication'

The server was configured as:

permitrootlogin yes
pubkeyauthentication yes
passwordauthentication yes

This meant that automated bots could continuously attempt username/password authentication, including directly against the root account.

2. Check Whether Fail2ban Is Installed

We checked for Fail2ban:

rpm -q fail2ban

It was not installed.

3. Fix the EPEL Repository Before Installing Fail2ban

Fail2ban is available through EPEL. However, attempting to install it produced dependency errors because the AlmaLinux 8 server was incorrectly using an EPEL 7 repository.

The problem was identified with:

sudo dnf repoinfo epel

The result showed:

Repo-name : Extra Packages for Enterprise Linux 7 - x86_64

This was incorrect because the operating system was AlmaLinux 8.

We inspected the EPEL files:

ls -la /etc/yum.repos.d/epel*
rpm -ql epel-release | grep '\.repo'
rpm -V epel-release

The verification showed that the active EPEL files had been modified, while correct EL8 versions were available as .rpmnew files.

Before changing anything, we backed up the existing repository configuration:

sudo mkdir -p /root/epel-repo-backup
sudo cp -a /etc/yum.repos.d/epel*.repo /root/epel-repo-backup/

We then replaced the incorrect repository files with the newer EL8 versions:

sudo mv /etc/yum.repos.d/epel.repo.rpmnew /etc/yum.repos.d/epel.repo
sudo mv /etc/yum.repos.d/epel-testing.repo.rpmnew /etc/yum.repos.d/epel-testing.repo

The DNF cache was then rebuilt:

sudo dnf clean all
sudo rm -rf /var/cache/dnf
sudo dnf makecache

Verification:

sudo dnf repoinfo epel

The repository now correctly reported:

Repo-name : Extra Packages for Enterprise Linux 8 - x86_64

4. Install Fail2ban

We first confirmed that the packages were now EL8 packages:

sudo dnf --showduplicates list fail2ban fail2ban-server fail2ban-firewalld

The correct versions were:

fail2ban.noarch             1.0.2-3.el8
fail2ban-firewalld.noarch   1.0.2-3.el8
fail2ban-server.noarch      1.0.2-3.el8

Fail2ban was then installed:

sudo dnf install fail2ban fail2ban-firewalld -y

The installation also installed the appropriate SELinux integration.

5. Verify Firewalld

We confirmed that firewalld was already running:

sudo systemctl status firewalld --no-pager

The service was active and enabled.

6. Configure Fail2ban for SSH

We created a dedicated SSH jail:

/etc/fail2ban/jail.d/sshd.local

Configuration:

[sshd]
enabled = true
port = ssh
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
banaction = firewallcmd-rich-rules

This means an IP that generates five failed SSH authentication attempts within ten minutes will be banned for one hour.

The configuration was validated before starting Fail2ban:

sudo fail2ban-client -t

The expected result is:

OK: configuration test is successful

7. Enable and Start Fail2ban

sudo systemctl enable --now fail2ban

Verify:

sudo systemctl status fail2ban --no-pager

Then check the SSH jail:

sudo fail2ban-client status sshd

On our server, Fail2ban immediately began detecting attacks and banning IP addresses:

Status for the jail: sshd
|- Filter
|  |- Currently failed: 6
|  |- Total failed: 23
`- Actions
   |- Currently banned: 1
   |- Total banned: 1
   `- Banned IP list: 152.32.182.8

This confirmed that Fail2ban was actively processing SSH authentication failures.

8. Verify Fail2ban Integration With Firewalld

Fail2ban was configured to use firewallcmd-rich-rules, which dynamically creates firewalld rules for banned IP addresses.

Check them with:

sudo firewall-cmd --list-rich-rules

Example:

rule family="ipv4" source address="77.90.185.20" port port="ssh" protocol="tcp" reject type="icmp-port-unreachable"

Multiple attacking IP addresses were automatically blocked after Fail2ban was enabled.

9. Create a Dedicated Administrative User

Direct administration using the root SSH account is undesirable. We created a dedicated administrative account:

sudo useradd -m -s /bin/bash ssh-user
sudo passwd ssh-user
sudo usermod -aG wheel ssh-user

The wheel group provides sudo privileges on AlmaLinux.

Verify membership:

id ssh-user

We then opened a completely separate SSH session and logged in as:

ssh ssh-user@SERVER_IP

Sudo access was tested:

sudo whoami

The expected result:

root

This test is extremely important. Do not disable root SSH access until another administrative account has been successfully tested from a separate SSH session.

10. Disable Direct Root SSH Login

Before changing SSH, we backed up the configuration:

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup-20260821

Direct root SSH login was disabled:

sudo sed -i 's/^[#[:space:]]*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config

Because password authentication is still being used for ssh-user, password authentication was intentionally kept enabled:

sudo sed -i 's/^[#[:space:]]*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config

The configuration was validated:

sudo sshd -t

Then SSH was reloaded without terminating existing sessions:

sudo systemctl reload sshd

The effective configuration was checked:

sudo sshd -T | egrep 'permitrootlogin|passwordauthentication|pubkeyauthentication'

The resulting configuration was:

permitrootlogin no
pubkeyauthentication yes
passwordauthentication yes

This means:

  • Direct root SSH login is disabled.
  • Password authentication remains available for the administrative user.
  • Public-key authentication is available for future use.
  • Fail2ban protects password authentication against repeated attempts.

11. Test SSH Again Before Closing Existing Sessions

A new SSH session was opened again using ssh-user.

ssh ssh-user@SERVER_IP

Then:

sudo whoami

returned:

root

Only after this test succeeded was it considered safe to close older root SSH sessions.

12. Disable Firewalld AllowZoneDrifting

Firewalld generated the following warning:

WARNING: AllowZoneDrifting is enabled.
This is considered an insecure configuration option.

The current setting was checked:

sudo grep -i '^AllowZoneDrifting' /etc/firewalld/firewalld.conf

It returned:

AllowZoneDrifting=yes

We changed it to:

sudo sed -i 's/^AllowZoneDrifting=.*/AllowZoneDrifting=no/' /etc/firewalld/firewalld.conf

Verification:

sudo grep -i '^AllowZoneDrifting' /etc/firewalld/firewalld.conf

Result:

AllowZoneDrifting=no

13. Verify the Active Firewall Zone

The active firewalld zone was checked:

sudo firewall-cmd --get-active-zones

The server uses:

public
  interfaces: ens192

The complete configuration was checked:

sudo firewall-cmd --list-all

The important portion was:

public (active)
  interfaces: ens192
  services: dhcpv6-client http https ssh

This means the server currently exposes the services required for GitLab:

  • SSH
  • HTTP
  • HTTPS

No unnecessary custom ports were listed in the active public zone.

14. Reload Firewalld

After verifying the configuration, reload firewalld so the AllowZoneDrifting=no configuration takes effect:

sudo firewall-cmd --reload

Then verify:

sudo firewall-cmd --list-all
sudo firewall-cmd --list-rich-rules
sudo fail2ban-client status sshd

If Fail2ban's runtime rules disappear after a firewall reload, restart Fail2ban:

sudo systemctl restart fail2ban

Then verify again:

sudo firewall-cmd --list-rich-rules
sudo fail2ban-client status sshd

15. Current Security Architecture

Internet
   |
   v
firewalld
   |
   +-- HTTP  (GitLab)
   +-- HTTPS (GitLab)
   +-- SSH
         |
         v
      Fail2ban
         |
         +-- Detect repeated authentication failures
         +-- Automatically ban attacking IPs
         +-- Add firewalld rich rules

SSH
 |
 +-- root login: DISABLED
 |
 +-- ssh-user
       |
       +-- Password authentication
       +-- Protected by Fail2ban
       +-- sudo via wheel

16. Useful Fail2ban Commands

Check Fail2ban:

sudo systemctl status fail2ban

Check enabled jails:

sudo fail2ban-client status

Check SSH protection:

sudo fail2ban-client status sshd

Check banned IP addresses:

sudo fail2ban-client status sshd

Check firewall rules:

sudo firewall-cmd --list-rich-rules

Manually ban an IP if required:

sudo fail2ban-client set sshd banip IP_ADDRESS

Manually unban an IP:

sudo fail2ban-client set sshd unbanip IP_ADDRESS

Restart Fail2ban:

sudo systemctl restart fail2ban

17. Current Fail2ban Policy

The current SSH policy is:

maxretry = 5
findtime = 10m
bantime = 1h

This means:

5 failed SSH attempts
within 10 minutes
        |
        v
IP banned for 1 hour

For a public production server, the ban duration can later be increased to 12 or 24 hours if desired.

18. Recommended Future Improvement: SSH Keys

The server is significantly safer now, but password authentication is still enabled because the administrator currently uses normal username/password SSH login.

The recommended future configuration is to configure an SSH public key for ssh-user, verify key authentication, and then disable SSH password authentication entirely.

The final desired configuration would be:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes

This would prevent password brute-force attacks from succeeding even if an attacker knows a valid SSH username.

19. Important Operational Rules

  • Never disable root/password SSH access until another administrative login has been tested.
  • Keep an existing SSH session open while modifying SSH or firewall configuration.
  • Always run sshd -t before reloading SSH.
  • Do not expose database, Redis, PostgreSQL, Elasticsearch/OpenSearch, or internal GitLab services directly to the Internet.
  • Use Fail2ban as protection against brute-force attacks, not as a replacement for upstream DDoS protection.
  • Keep AlmaLinux, GitLab, and security packages patched.
  • Review Fail2ban and SSH logs periodically for unusual activity.

20. DDoS Protection Consideration

Fail2ban and firewalld are useful against bots, scanning, and brute-force attacks, but they cannot stop a large volumetric DDoS attack that saturates the server's network connection.

For public HTTP/HTTPS services, an upstream protection layer such as Cloudflare or hosting-provider DDoS protection should be considered.

Internet
    |
    v
Cloudflare / Provider DDoS Protection
    |
    v
AlmaLinux Firewall
    |
    v
Fail2ban / SSH Protection
    |
    v
GitLab

21. Result

After these changes, the AlmaLinux 8 GitLab server has a substantially stronger security posture:

  • Fail2ban installed and enabled.
  • SSH brute-force attempts automatically detected.
  • Attacking IP addresses automatically blocked through firewalld.
  • Direct root SSH login disabled.
  • Dedicated ssh-user administrative account created.
  • Sudo access verified.
  • Firewalld active.
  • Only SSH, HTTP, and HTTPS exposed through the public zone.
  • AllowZoneDrifting disabled.
  • Correct AlmaLinux 8 EPEL repository restored.
  • SELinux-compatible Fail2ban packages installed.
  • SSH public-key authentication remains available for a future passwordless/key-only configuration.

Current SSH security:

Root SSH login        : Disabled
ssh-user SSH login    : Enabled
Password login        : Enabled
Public key support    : Enabled
Fail2ban              : Enabled
Firewalld             : Enabled
SSH brute-force bans  : Enabled
HTTP/HTTPS            : Enabled