# Replace CSF with firewalld and fail2ban: Secure Step-by-Step

Source: https://srvscripts.com/guides/replace-csf-with-firewalld-fail2ban/
Updated: 2026-10-07
Publisher: srvScripts (https://srvscripts.com/)

firewalld and fail2ban are packaged by the distribution, native to nftables and maintained on the same schedule as the operating system. That combination covers everything most hosting servers used CSF for: a port allow list, connection rate limiting, and automatic banning of brute-force sources. This tutorial performs the swap on AlmaLinux 9 or 10, keeping the server reachable throughout. It assumes you have console access as a safety net; a firewall change over SSH with no console is how servers get lost.

In short: Export the CSF port list and csf.allow, uninstall CSF with csf -x and the package or uninstall.sh, then enable firewalld and add the same services and ports to the public zone with –permanent before reloading.

**Short answer:** Export the CSF port list and `csf.allow`, uninstall CSF with `csf -x` and the package or `uninstall.sh`, then enable firewalld and add the same services and ports to the `public` zone with `--permanent` before reloading. Install `fail2ban` and `fail2ban-firewalld` from EPEL, define `sshd`, mail and FTP jails in `/etc/fail2ban/jail.local` with `banaction = firewallcmd-rich-rules`, and confirm a test ban appears in `firewall-cmd --list-rich-rules`.

## Record what CSF is doing today

Before removing anything, capture the open ports and the permanent allow list so nothing is forgotten:

```
grep -E '^(TCP_IN|TCP_OUT|UDP_IN|UDP_OUT) ' /etc/csf/csf.conf
grep -v '^#' /etc/csf/csf.allow | grep -v '^$' > /root/csf-allow-export.txt
csf -l > /root/csf-live-rules.txt
```

Keep those files. The allow list becomes a firewalld trusted source list, and the port list becomes your zone services.

## Remove CSF cleanly

CSF ships its own uninstaller, which stops the daemons, flushes the chains and removes the cron entries. On cPanel it is an RPM, so use dnf; elsewhere run the script:

```
csf -x
rpm -q cpanel-csf && dnf remove -y cpanel-csf || sh /etc/csf/uninstall.sh
pgrep -a lfd
nft list ruleset | head
```

`pgrep` should return nothing and the ruleset should be essentially empty. If you are on a panel server, also remove the panel plugin so no one can re-enable CSF from the UI later. There is now no firewall; move directly to the next step.

## Install and configure firewalld

```
dnf install -y firewalld
systemctl enable --now firewalld
firewall-cmd --get-default-zone
```

The default zone is `public`. Add the services and ports you exported, using named services where they exist and raw ports where they do not. A cPanel-style server looks like this:

```
firewall-cmd --permanent --zone=public --add-service={ssh,http,https,smtp,smtps,submission,imap,imaps,pop3,pop3s,dns,ftp}
firewall-cmd --permanent --zone=public --add-port={2083/tcp,2087/tcp,2096/tcp}
firewall-cmd --permanent --zone=public --add-port=30000-35000/tcp
firewall-cmd --reload
firewall-cmd --list-all
```

The high port range is for passive FTP; match it to your FTP daemon’s configuration. If SSH runs on a non-standard port, add that port and remove the `ssh` service. Any addresses from `csf.allow` that need unrestricted access go into the `trusted` zone:

```
firewall-cmd --permanent --zone=trusted --add-source=198.51.100.0/24
firewall-cmd --reload
```

For rate limiting comparable to CSF’s `CT_LIMIT` and `PORTFLOOD`, use a rich rule:

```
firewall-cmd --permanent --add-rich-rule='rule service name="ssh" accept limit value="10/m"'
firewall-cmd --reload
```

## Install fail2ban and build the jails

fail2ban comes from EPEL. Install it with the firewalld action package so bans are applied as firewalld rich rules rather than raw iptables calls:

```
dnf install -y epel-release
dnf install -y fail2ban fail2ban-firewalld
systemctl enable --now fail2ban
```

Never edit `jail.conf`; create `/etc/fail2ban/jail.local` with the jails you need. On AlmaLinux 9 and 10, sshd logs to the journal, so use the systemd backend:

```
[DEFAULT]
backend = systemd
banaction = firewallcmd-rich-rules
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 198.51.100.0/24

[sshd]
enabled = true

[postfix]
enabled = true

[dovecot]
enabled = true

[proftpd]
enabled = true
logpath = /var/log/proftpd/proftpd.log
```

On an Exim server replace the `postfix` jail with `exim` and point `logpath` at `/var/log/exim_mainlog`. cPanel and DirectAdmin login failures need a custom filter; cPanel logs failed logins to `/usr/local/cpanel/logs/login_log` with the string `FAILED LOGIN`, which a short regex in `/etc/fail2ban/filter.d/cpanel.conf` can match:

```
[Definition]
failregex = ^.*\[.*\] .* FAILED LOGIN .* from .*$
```

Then add a `[cpanel]` jail with that `logpath` and `backend = auto`. Reload and confirm every jail started:

```
fail2ban-client reload
fail2ban-client status
```

## Permanent bans and manual blocks

CSF’s `csf -d` has a direct equivalent in firewalld:

```
firewall-cmd --permanent --zone=drop --add-source=203.0.113.10
firewall-cmd --reload
```

For a fail2ban ban that persists across restarts, set `bantime = -1` in a dedicated recidive jail, which bans addresses that keep getting banned by other jails. That reproduces `LF_PERMBLOCK` without a separate mechanism.

## Common pitfall

Forgetting `--permanent` is the classic one: the rule works until the next reload and then vanishes. The second is mismatched backends: with `backend = systemd` fail2ban ignores `logpath`, so any jail that reads a plain file must set `backend = auto` explicitly. If a jail shows zero failures for a service you know is under attack, this is almost always why.

## Verify

Test each layer independently. From another host, try a port that should be closed and one that should be open:

```
nc -zv server.example.com 3306
nc -zv server.example.com 443
```

Then generate a few failed SSH logins from a test address and watch the ban appear:

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

The test address should appear in both. Finally, reboot the server once during a maintenance window and repeat the checks; a firewall that only works until the first reboot is worse than none because it stops anyone worrying about it. Our [SSH hardening guide](/guides/harden-ssh-almalinux-9/) pairs well with this setup, since key-only authentication removes most of what fail2ban would otherwise be banning.

Diagram: Logs feed fail2ban jails, which ban through firewalld and nftables; zones keep admin ports restricted.

## Replace CSF with firewalld at a glance

**Official documentation:** [AlmaLinux wiki](https://wiki.almalinux.org/), [Linux man pages](https://man7.org/linux/man-pages/).

**Related guides:** [Copy Fail, Dirty Frag and Fragnesia: mitigating the 2026 Linux privilege-escalation trio without a reboot](https://srvscripts.com/guides/copy-fail-dirty-frag-mitigation/) · [Installing Sentinel Firewall as a drop-in CSF replacement on Ubuntu 24.04 and Debian 13](https://srvscripts.com/guides/sentinel-firewall-csf-replacement/) · [CVE-2026-65638, 65639 and 67402 explained: patching the CSF Messenger and URLGET remote-code flaws](https://srvscripts.com/guides/csf-cve-2026-65638-patch/).

**See also:** [fail2ban vs CSF in 2026: Which Firewall for a Hosting Server?](/guides/fail2ban-vs-csf/)

## Frequently asked questions

### Is firewalld with fail2ban as good as CSF for a hosting server?

For port filtering, allow lists, rate limiting and brute-force banning, yes; both use nftables underneath. What it lacks is CSF’s single configuration file, the WHM plugin and integrated checks such as process tracking and the security report, which need separate tooling.

### Can I run CSF and firewalld at the same time?

No. Both manage the nftables ruleset and they overwrite each other’s chains on every reload, so the server ends up with whichever loaded last. Remove CSF completely before enabling firewalld.

### Does fail2ban work with cPanel or DirectAdmin logins?

Yes, with a custom filter. cPanel writes failed logins to `/usr/local/cpanel/logs/login_log` and DirectAdmin to `/var/log/directadmin/login.log`; a short `failregex` in `/etc/fail2ban/filter.d/` plus a jail using `backend = auto` bans the source address like any other service.
