İnternete açık bir Linux sunucunun SSH logları birkaç saat içinde "Invalid user admin from 203.0.113.50 port 51514" gibi satırlarla dolar. Fail2ban bu logları okur ve art arda başarısız giriş denemesi yapan adresleri güvenlik duvarında engeller. Bu rehberde Debian, Ubuntu, AlmaLinux ve Rocky Linux üzerinde SSH ve Nginx için doğru çalışan bir Fail2ban kurulumunu, kopyala-yapıştır tariflerin çoğunu çalışmaz hâle getiren dağıtım varsayılanlarını ve banların çoğunu baştan gereksiz kılan SSH ayarlarını anlatıyoruz. Ayarları, dağıtımların Ekim 2026 itibarıyla sunduğu Fail2ban paketlerinin (1.0.x ve 1.1.x) yapılandırma dosyalarıyla tek tek karşılaştırdık; proje Ağustos 2026'da 1.1.1 sürümünü yayımladı.
Fail2ban nasıl çalışır?
Fail2ban, Python ile yazılmış ve üç parçadan oluşan bir servistir:- Filtre (filter): Log satırındaki başarısız denemeyi tanıyan ve istemcinin IP adresini ayıklayan düzenli ifadeler (failregex). Yeri: /etc/fail2ban/filter.d/
- Jail: Filtreyi bir log kaynağına (logpath ile bir dosyaya ya da backend = systemd ile systemd günlüğüne, yani journal'a) ve eşik değerlerine bağlar: findtime süresi içinde maxretry kadar hata üreten IP, bantime boyunca engellenir.
- Eylem (action): Banı uygular ve süre dolunca kaldırır; bunu genellikle bir nftables kümesi (set), bir iptables zinciri, bir firewalld kuralı (rich rule) ya da bir ufw kuralıyla yapar. Yeri: /etc/fail2ban/action.d/
Aracın sınırlarını bilmek de en az bu kadar önemli. Fail2ban; binlerce adresin her birinin yalnızca bir iki kez denediği dağıtık ve yavaş (low and slow) saldırıları, ilk denemede başarılı olan bir girişi (sızmış bir anahtar, zayıf bir parola), SQL enjeksiyonu gibi uygulama açıklarını, loga düşmeyen hiçbir şeyi ve hacimsel DDoS saldırılarını durduramaz. Onu korumanın kendisi olarak değil, yalnızca anahtarla girişe izin veren bir SSH yapılandırmasının üzerinde log gürültüsünü ve gereksiz yükü azaltan bir katman olarak düşünün. OpenSSH 9.8 ve sonraki sürümlerdeki PerSourcePenalties özelliği de sürekli hata yapan adresleri bir süreliğine reddeder; ceza süresi varsayılan olarak en fazla 10 dakika olduğu için Fail2ban'ın yerini tutmaz, onu tamamlar. Resmin bütünü için production ekibi için sunucu sertleştirme ve katmanlı savunma (defence in depth) konumuza göz atabilirsiniz.
1. Adım: Önce SSH erişimini sıkılaştırın
Parola ile giriş kapalıysa SSH üzerinden yapılan bir kaba kuvvet (brute force) saldırısının tahmin edebileceği hiçbir şey kalmaz; bu yüzden işe buradan başlayın. Kendi bilgisayarınızda bir anahtar oluşturun, sunucuya kopyalayın ve anahtarla bağlanabildiğinizi doğrulayın:
Bash:
ssh-keygen -t ed25519 -C "siz@laptop"
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@SUNUCU_IP
ssh admin@SUNUCU_IP
Ardından sunucuda ayrı bir yapılandırma dosyası (drop-in) oluşturun. Güncel Debian, Ubuntu ve AlmaLinux/Rocky sürümlerinde sshd_config dosyasının en başında /etc/ssh/sshd_config.d/*.conf dosyalarını dâhil eden bir Include satırı bulunur (grep -i include /etc/ssh/sshd_config ile kontrol edebilirsiniz).
Kod:
# /etc/ssh/sshd_config.d/00-hardening.conf
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
Sunucuya normal bir kullanıcıyla bağlanıp sudo kullanıyorsanız PermitRootLogin no yazın. KbdInteractiveAuthentication satırını atlamayın: PAM etkinken klavye etkileşimli (keyboard-interactive) giriş, PasswordAuthentication kapalı olsa bile parola sorabilir.
sshd her ayar için okuduğu ilk değeri kullanır. Bulut imajlarında cloud-init'in oluşturduğu /etc/ssh/sshd_config.d/50-cloud-init.conf dosyası sık görülür ve içinde PasswordAuthentication yes yazıyor olabilir. Adı 00 ile başlayan dosyanız ondan önce okunur; yine de çakışan bütün satırları bulun ve dosyalara değil, etkin yapılandırmaya bakın:
Bash:
sudo grep -ri "passwordauthentication" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/
sudo sshd -t
sudo sshd -T | grep -Ei '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|pubkeyauthentication)'
# Debian/Ubuntu (AlmaLinux/Rocky'de servis adı sshd)
sudo systemctl reload ssh
Test ederken açık oturumunuzu kapatmayın, ikinci bir terminal kullanın. SSH ayarlarına ya da güvenlik duvarına dokunmadan önce sağlayıcınızın web konsoluna veya kurtarma (rescue) sistemine nasıl ulaşacağınızı öğrenin.
Standart dışı bir SSH portu yalnızca log gürültüsünü azaltır; tek başına bir güvenlik önlemi değildir. Portu taşırsanız jail'e de bildirin (sık yapılan hatalara bakın). Ubuntu 22.10 ve sonrasında portu ssh.socket yönettiği için kullandığınız sürümün Ubuntu belgelerini izleyin. Temel önlemlerin geri kalanı production Linux için minimum sunucu sertleştirme kontrol listemizde yer alıyor.
2. Adım: Fail2ban kurulumu
Debian ve Ubuntu:
Bash:
sudo apt update
sudo apt install fail2ban
sudo systemctl enable --now fail2ban
Yeni kurulmuş bir Debian 12'de servis bu noktada büyük olasılıkla başlamayacaktır. Bu beklenen bir durum: Nedenini 4. adımda açıklıyoruz, 3. adımdaki jail.local da sorunu çözüyor.
AlmaLinux ve Rocky Linux 9 veya 10'da Fail2ban EPEL deposundan gelir; EPEL belgeleri önce CRB deposunun etkinleştirilmesini önerir:
Bash:
sudo dnf config-manager --set-enabled crb
sudo dnf install epel-release
sudo dnf install fail2ban
sudo systemctl enable --now fail2ban
EPEL'deki fail2ban, kendi içinde dosya taşımayan bir meta pakettir (meta package); banları firewalld üzerinden uygulayan fail2ban-firewalld paketini de kurar. Sunucuda firewalld yoksa bunun yerine fail2ban-server paketini kurun ve bir sonraki adımda nftables'ı seçin.
Paketlerin varsayılan ayarları, rehberlerin çoğunun varsaydığından epey farklıdır. Kendi sisteminizi fail2ban-client --version ve ls /etc/fail2ban/jail.d/ komutlarıyla kontrol edin:
| Dağıtım | Paket sürümü | Önemli varsayılanlar |
|---|---|---|
| Debian 12 | 1.0.2 | sshd jail'i açık gelir ama yeni kurulmuş bir Debian 12'de bulunmayan /var/log/auth.log dosyasını okumaya çalışır; banaction değeri iptables-multiport |
| Debian 13 | 1.1.0 (yamalı) | sshd jail'i systemd günlüğünü okur; banlar nftables ile uygulanır |
| Ubuntu 24.04 | 1.0.2 | DEFAULT bölümünde bütün jail'ler için backend = systemd; banlar nftables ile uygulanır |
| Ubuntu 26.04 | 1.1.0 (yamalı) | Debian 13 ile aynı |
| AlmaLinux/Rocky 9 ve 10 (EPEL) | 1.1.0 (yamalı) | Hiçbir jail açık gelmez; sshd jail'i açıldığında günlüğü okur; banlar firewalld kurallarıyla (rich rule) uygulanır |
3. Adım: Ayarları jail.local dosyasına yazın
jail.conf dosyasını düzenlemeyin; pakete aittir ve yerel değişiklikler güncellemelerle çakışır. Fail2ban sırasıyla jail.conf, jail.d/*.conf, jail.local ve jail.d/*.local dosyalarını okur; aynı ayar birden fazla dosyada varsa en son okunan geçerli olur. Dolayısıyla jail.local hem paketin varsayılanlarını hem de dağıtımınızın jail.d altına koyduğu .conf dosyalarını geçersiz kılar. İyi bir başlangıç noktası:
INI:
# /etc/fail2ban/jail.local
[DEFAULT]
# Asla banlanmayacak adresler: loopback, ofis IP'si, VPN aralığı (örnek değerler)
ignoreip = 127.0.0.1/8 ::1 203.0.113.10 198.51.100.0/24
bantime = 1h
findtime = 10m
maxretry = 5
# Tekrar edenlere artan süre: 1 saat, 2 saat, 4 saat ... en fazla 1 hafta
bantime.increment = true
bantime.maxtime = 1w
# Ban yöntemi: gerçekten çalışan güvenlik duvarıyla eşleşmeli (aşağıdaki tabloya bakın)
banaction = nftables-multiport
banaction_allports = nftables-allports
[sshd]
enabled = true
port = ssh
backend = systemd
maxretry = 3
- ignoreip: Hiçbir zaman banlanmayacak adresler, CIDR aralıkları veya makine adları (hostname): sabit ofis IP'niz, VPN çıkış adresiniz ve izleme (monitoring) sistemleriniz. Varsayılan olarak açık olan ignoreself, sunucunun kendi adreslerini zaten kapsar.
- bantime, findtime, maxretry: Paketle gelen değerler 10m, 10m ve 5'tir; m, h, d, w gibi birimler kullanılabilir. bantime = -1 (kalıcı ban) kullanmayın; güvenlik duvarını zamanla eskimiş kayıtlarla doldurur.
- bantime.increment: Veritabanındaki ban geçmişine bakarak tekrar eden adreslerin ban süresini bantime.maxtime sınırına kadar katlar (varsayılan formülle 1, 2, 4, 8… kat).
- sshd için backend = systemd: SSH olaylarını auth.log olsa da olmasa da systemd günlüğünden okur; bunun neden önemli olduğunu 4. adımda anlatıyoruz.
Ban eylemini sunucuda gerçekten çalışan güvenlik duvarına göre seçin:
| Sunucudaki güvenlik duvarı | banaction | banaction_allports | Banları nerede görürsünüz? |
|---|---|---|---|
| nftables ya da güvenlik duvarı yöneticisi yok (Debian, Ubuntu) | nftables-multiport | nftables-allports | nft list table inet f2b-table |
| firewalld (AlmaLinux/Rocky varsayılanı) | firewallcmd-rich-rules | firewallcmd-rich-rules | firewall-cmd --list-rich-rules |
| ufw (banların ufw listesinde görünmesini istiyorsanız) | ufw | ufw | ufw status numbered |
| Eski tip iptables | iptables-multiport | iptables-allports | iptables -S | grep f2b |
ufw eylemi adresi tüm portlarda engeller; nftables banları da ufw ile yan yana çalışır, Ubuntu'nun kendi paketi bu şekilde ayarlanmıştır. firewalld kullanan bir sunucuda iki satırı da firewallcmd-rich-rules yapın (EPEL varsayılanı budur), yoksa jail.local bu varsayılanı geçersiz kılar. Debian 12'de iki nftables satırını mutlaka yazın: Paket hâlâ iptables-multiport varsayılanıyla gelir, yeni kurulmuş bir Debian 12'de ise genellikle nftables bulunur ama iptables komutu bulunmaz.
4. Adım: Debian 12 ve Ubuntu 24.04'teki backend tuzağı
Debian 12'den itibaren rsyslog varsayılan olarak kurulmuyor ve loglar systemd günlüğüne gidiyor; bu yüzden yeni bir kurulumda /var/log/auth.log dosyası yoktur. Debian 12'nin Fail2ban paketi (1.0.2) açık gelen sshd jail'ini hâlâ bu dosyaya yönlendirir ve servis başlamayı reddeder:
Kod:
ERROR Failed during configuration: Have not found any log file for sshd jail
İki çözümden birini seçin:
- Günlüğü okuyun: Yukarıdaki jail.local örneğinde olduğu gibi sshd bölümüne backend = systemd yazın. python3-systemd Debian 12'de önerilen (recommended) paket olduğundan apt onu genellikle kendiliğinden kurar; kurmadıysa sudo apt install python3-systemd çalıştırın.
- Metin logunu geri getirin: sudo apt install rsyslog komutunu çalıştırıp Fail2ban'ı yeniden başlatın.
Ubuntu 24.04'te tuzak tam tersidir: Paket DEFAULT bölümünde backend = systemd ayarlar, yani bütün jail'ler günlüğü okur. systemd backend'i logpath ayarını yok sayar; Nginx ise loglarını kendi dosyalarına yazar. Sonuçta Nginx jail'i hatasız başlar ama hiçbir satırı yakalamaz. Dosyadan okuyan her jail'e (recidive dâhil) aşağıdaki Nginx örneklerinde olduğu gibi backend = auto yazın. AlmaLinux ve Rocky'de EPEL'in isteğe bağlı fail2ban-systemd paketi de aynı etkiyi yaratır.
Üçüncü bir tuzak daha var: OpenSSH 9.8 sunucuyu sshd ve sshd-session olarak ikiye ayırdı ve bazı log satırları artık sshd-session adıyla yazılıyor. Fail2ban 1.1.1 ile Debian 13, Ubuntu 26.04 ve EPEL paketleri bunu tanıyor; ancak eski bir Fail2ban yeni bir OpenSSH ile birlikte kullanılırsa (örneğin OpenSSH paketini bookworm-backports'tan kurmuş bir Debian 12), sshd jail'inin hata sayacı sıfırda kalabilir. Mümkünse güncelleyin; değilse daemon adını geçersiz kılın:
INI:
# /etc/fail2ban/filter.d/sshd.local
[DEFAULT]
_daemon = sshd(?:-session)?
Ardından günlük sorgusunun yeni süreç adını da kapsaması için jail.local içindeki mevcut sshd bölümüne şu satırı ekleyin:
INI:
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-session
5. Adım: Güvenmeden önce test edin
fail2ban-client -t yalnızca yapılandırmanın yüklenebildiğini kanıtlar; filtrelerin gerçek log satırlarını yakaladığını göstermez. Her değişiklikten sonra:
Bash:
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
Sağlıklı bir çıktı log kaynağını gösterir ve hataları sayar (değerler örnektir):
Kod:
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 41
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 3
|- Total banned: 12
`- Banned IP list: 203.0.113.50 203.0.113.77 192.0.2.44
Saldırılar logda göründüğü hâlde Total failed sıfırda kalıyorsa filtreyi gerçek veriyle çalıştırın; çıktının sonundaki özet kaç satırın eşleştiğini gösterir:
Bash:
sudo fail2ban-regex systemd-journal sshd
# ya da bir kesiti test edin (servis adı Debian/Ubuntu'da "ssh", AlmaLinux/Rocky'de "sshd")
sudo journalctl -u ssh --since "2 hours ago" --no-pager > /tmp/ssh-test.log
sudo fail2ban-regex --print-all-matched /tmp/ssh-test.log sshd
Ardından kontrolünüzdeki ikinci bir adresten, örneğin telefonunuzun mobil erişim noktasından ya da başka bir VPS'ten saldırıyı taklit edin; asla üzerinde çalıştığınız bağlantıdan değil:
Bash:
# Test makinesinde: var olmayan bir kullanıcı adı her modda hata sayılır.
# PubkeyAuthentication=no her denemeyi tek hataya indirir (sunulan her anahtar ayrıca
# "Failed publickey for invalid user" satırı yazar); accept-new ise BatchMode'un
# bilinmeyen sunucu anahtarında oturum açmayı denemeden bağlantıyı kesmesini önler.
for i in 1 2 3 4; do
ssh -o BatchMode=yes -o PubkeyAuthentication=no -o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=5 olmayankullanici@SUNUCU_IP
done
Üçüncü denemeden sonra (maxretry = 3) bir sonraki bağlantı reddedilir ya da zaman aşımına uğrar. OpenSSH 9.8 ve sonrasındaki sshd bu bağlantıyı PerSourcePenalties nedeniyle kendisi de reddedebileceğinden banı fail2ban-client status sshd ile ve güvenlik duvarında doğrulayın: nft list set inet f2b-table addr-set-sshd (küme ilk banla oluşur; IPv6 adresleri addr6-set-sshd kümesine gider), firewall-cmd --list-rich-rules ya da ufw status numbered. Testten sonra deneme adresinin banını kaldırın (bir sonraki adım).
sshd filtresinin modları da vardır. Varsayılan normal mod yanlış parolaları ve var olmayan kullanıcı adlarını sayar. SSH yalnızca anahtar kabul ettiğinde root gibi gerçek hesapları deneyen botlar çoğunlukla aşağıdaki gibi satırlar bırakır; bunları yalnızca aggressive (veya ddos) modu sayar:
Kod:
Connection closed by authenticating user root 203.0.113.9 port 51514 [preauth]
Bunları da banlamak için sshd bölümüne mode = aggressive ekleyin ve 22 numaralı porta bağlanan izleme sistemlerini ignoreip listesine alın; çünkü aggressive mod, el sıkışmayı (handshake) hiç tamamlamayan bağlantıları da sayar.
6. Adım: IP banını kaldırma ve tekrar eden saldırganlar
Bash:
# Tek bir jail'de
sudo fail2ban-client set sshd unbanip 203.0.113.50
# Tüm jail'lerde ve veritabanında
sudo fail2ban-client unban 203.0.113.50
# Hangi jail hangi adresleri banlıyor
sudo fail2ban-client banned
# Banlı adresler, ban ve bitiş zamanlarıyla
sudo fail2ban-client get sshd banip --with-time
Kendinizi dışarıda mı bıraktınız? Başka bir adresten ya da sağlayıcının konsolundan bağlanın, fail2ban-client unban ile kendi IP'nizin banını kaldırın ve adresi jail.local içindeki ignoreip satırına ekleyin. fail2ban-client set sshd addignoreip komutunun etkisi yalnızca bir sonraki yeniden yükleme (reload) ya da yeniden başlatmaya kadar sürer.
Israrcı saldırganlar için paketle gelen recidive jail'i, Fail2ban'ın kendi logunu okur ve bir gün içinde tekrar tekrar (maxretry kadar, burada 5 kez) banlanan bir adresi bir hafta boyunca tüm portlarda engeller:
INI:
# /etc/fail2ban/jail.local dosyasının sonuna ekleyin
[recidive]
enabled = true
# Dosyadan okur; DEFAULT bölümünde backend = systemd olan Ubuntu 24.04'te şart
backend = auto
jail.conf içinde recidive jail'inin üstündeki açıklama, veritabanı kayıtlarının varsayılan bir günden daha uzun tutulmasını da öneriyor:
INI:
# /etc/fail2ban/fail2ban.local (7,5 gün; jail.conf'taki önerilen değer)
[DEFAULT]
dbpurgeage = 648000
recidive, Fail2ban'ın varsayılan olduğu gibi /var/log/fail2ban.log dosyasına ve DEBUG dışında bir seviyede log yazmasını gerektirir. bantime.increment ile kısmen aynı işi yapar; ikisini birlikte çalıştırmak sorun değildir ama tek bir sunucuda çoğu zaman biri yeterlidir.
7. Adım: Nginx jail'leri
Fail2ban, Nginx hata logunu okuyan hazır filtrelerle gelir. Varsayılan yol /var/log/nginx/*error.log olup example.com.error.log gibi site bazlı dosyaları da kapsar:- nginx-http-auth: Başarısız HTTP Basic Auth girişleri (yanlış parola ya da htpasswd dosyasında bulunmayan kullanıcı).
- nginx-limit-req: Nginx hız sınırlamasına (rate limiting) takılan istekler; limit_req yapılandırılmış olmalıdır.
- nginx-botsearch: Sunucunuzda olmayan yolları yoklayan botlar: phpMyAdmin, webmail ya da WordPress olmayan bir sitede wp-login.php gibi. 404'leriniz yalnızca erişim logunda görünüyorsa jail'in logpath ayarını erişim loguna çevirin; filtre o biçimi de tanır.
- İsteğe bağlı olanlar: nginx-bad-request (erişim logundaki 400 yanıtları) ve Fail2ban 1.1.0'dan itibaren nginx-forbidden (deny kuralıyla reddedilen istekler).
Bir giriş sayfası için hız sınırı, http bloğuna ve sitenin server bloğuna yazılır:
Kod:
# http { } bağlamı, örneğin /etc/nginx/conf.d/limits.conf
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=10r/m;
# sitenin server { } bloğu
location = /wp-login.php {
limit_req zone=wplogin burst=5 nodelay;
# mevcut PHP location bloğunuzdaki include ve fastcgi_pass satırlarını buraya kopyalayın
}
Burst sınırını aşan istekler 503 durum koduyla (limit_req_status) reddedilir ve hata loguna error seviyesinde "limiting requests, excess: … by zone "wplogin", client: …" satırı yazılır; nginx-limit-req bu satırları sayar. Jail'leri açın; port ve log yolları hazır tanımlardan gelir:
INI:
[nginx-http-auth]
enabled = true
backend = auto
[nginx-limit-req]
enabled = true
backend = auto
maxretry = 10
[nginx-botsearch]
enabled = true
backend = auto
WordPress girişleri için özel filtre
Hız sınırı saldırganı yavaşlatır, özel bir filtre ise banlar. Standart bir WordPress, başarısız girişte formu 200 durum koduyla yeniden gösterir, başarılı girişte ise normalde 302 ile yönlendirir; dolayısıyla wp-login.php adresine gönderilen ve 200 ile yanıtlanan bir POST isteği başarısız bir denemedir. Güvenlik eklentileri bu yanıtları değiştirebildiği için bunu önce kendi erişim logunuzda doğrulayın.
INI:
# /etc/fail2ban/filter.d/nginx-wp-login.conf
[Definition]
failregex = ^<HOST> - \S+ \[\] "POST /(?:[^"\s]*/)?wp-login\.php(?:\?[^"\s]*)? HTTP/[\d.]+" 200\b
ignoreregex =
datepattern = ^[^\[]*\[({DATE})
{^LN-BEG}
Fail2ban eşleştirmeden önce zaman damgasını satırdan çıkarır; düzenli ifadedeki boş köşeli parantezlerin nedeni budur. Filtre, Nginx'in varsayılan combined log biçimini bekler. Önce test edin, sonra jail'i ekleyin:
Bash:
sudo fail2ban-regex --print-all-matched /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-wp-login.conf
INI:
[nginx-wp-login]
enabled = true
backend = auto
port = http,https
filter = nginx-wp-login
logpath = /var/log/nginx/access.log
/var/www/example.com/logs/access.log
maxretry = 6
findtime = 10m
bantime = 1h
Site bazlı her logu ayrı bir satıra yazın; joker karakterler (glob) de çalışır. Desenler Fail2ban başlarken ya da yeniden yüklenirken genişletildiği için yeni log dosyası olan bir site ekledikten sonra sudo fail2ban-client reload çalıştırın.
Cloudflare veya ters proxy arkasındaki siteler
Nginx; Cloudflare, bir yük dengeleyici (load balancer) ya da başka bir proxy'nin arkasındaysa varsayılan ayarlarla iki sorun çıkar:- Logda yanlış IP. realip yapılandırılmamışsa bütün istekler proxy'den geliyormuş gibi görünür; Fail2ban proxy'yi banlar ve site aynı anda çok sayıda ziyaretçiye kapanır. Ziyaretçi adresini, Cloudflare'in her IP aralığı için bir set_real_ip_from satırı (liste cloudflare.com/ips adresinde) ve real_ip_header CF-Connecting-IP ile geri getirin. Modülün Nginx derlemenizde olup olmadığını nginx -V 2>&1 | grep -o http_realip_module gösterir.
- Etkisiz kalan banlar. Proxy üzerinden gelen istekler sunucuya yine proxy'nin adresinden ulaşır; sunucunun kendi güvenlik duvarındaki ban bu istekleri durdurmaz. Engellemeyi Cloudflare tarafında (edge) yapın: Fail2ban'ın cloudflare-token eylemi Cloudflare API'si üzerinden bir IP Access Rule oluşturur. Bunun için zone kimliğiniz ve bir API token'ı gerekir; token jail.local içinde durduğu için eylem dosyası bu dosyanın izinlerini 640 yapmanızı öneriyor. Diğer seçenek Cloudflare'in kendi hız sınırlama kurallarıdır. Asıl sunucuya (origin) doğrudan gelenler için yerel banı da koruyun ve 80 ile 443 numaralı portları yalnızca Cloudflare aralıklarına açın.
Kod:
# /etc/nginx/conf.d/cloudflare-realip.conf: cloudflare.com/ips listesindeki her aralık için bir satır
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 2400:cb00::/32;
# ...kalan IPv4 ve IPv6 aralıkları...
real_ip_header CF-Connecting-IP;
INI:
# Jail içinde: hem Cloudflare'de hem yerelde banla
action = cloudflare-token[cfzone="ZONE_KIMLIGINIZ", cftoken="API_TOKENINIZ"]
%(action_)s
Proxy ve yük dengeleyici adreslerini ek bir önlem olarak ignoreip listesine ekleyin. SSH trafiği Cloudflare'in HTTP proxy'sinden geçmediği için sshd jail'i bu durumdan etkilenmez.
Kontrol paneli olan sunucular: Önce neyin çalıştığına bakın
cPanel'de yerleşik olarak cPHulk, DirectAdmin'de Brute Force Monitor bulunur; Plesk ise Fail2Ban'ı kurulabilir bir bileşen olarak sunar ve Tools & Settings > IP Address Banning (Fail2Ban) ekranından yönetir. Birçok cPanel ve DirectAdmin sunucusunda ayrıca CSF/LFD ya da Imunify360 çalışır (CSF'nin asıl geliştiricisi Way to the Web, Ağustos 2025'te projeyi bıraktı; geliştirme artık fork'lar üzerinden sürüyor, bu yüzden sunucunuzdaki sürümün hâlâ güncelleme aldığından emin olun). Bunların üzerine bir ban aracı daha eklemek, aynı güvenlik duvarını düzenleyen iki program ve bir müşteri engellendiğinde bakılacak iki ayrı yer demektir. Önce neyin etkin olduğunu bulun:
Bash:
systemctl list-units --type=service | grep -Ei 'fail2ban|lfd|csf|imunify|cphulk'
Sonra her servisi tek bir aracın sorumluluğuna bırakın. Plesk'te jail'leri dosyaları elle düzenleyerek değil, panelden ya da ip_ban komut satırı aracıyla yönetin.
Gündelik işletim ve bakım
- Log: /var/log/fail2ban.log her Found, Ban ve Unban olayını kaydeder: sudo grep -E " (Ban|Unban) " /var/log/fail2ban.log | tail -20
- Yeniden başlatmak yerine yeniden yükleyin: Bir düzenlemeden sonra sudo fail2ban-client -t, ardından sudo fail2ban-client reload çalıştırın; mevcut banlar yerinde kalır.
- Veritabanı: Banlar /var/lib/fail2ban/fail2ban.sqlite3 sayesinde yeniden başlatmalardan sonra da korunur; eski kayıtların ne kadar tutulacağını dbpurgeage (varsayılan 1d) belirler.
- IPv6: Fail2ban 0.10'dan beri destekleniyor; nftables, firewalld ve ufw eylemleri IPv6 ile çalışır. IPv6 ön eklerinizi (prefix) ignoreip listesine ekleyin. Tek bir saldırgan çoğu zaman koca bir /64 bloğunu kontrol ettiğinden IPv6'da adres bazlı banlar daha zayıftır; SSH için anahtar zorunluluğunun önce gelmesinin bir nedeni de budur.
- Güncellemeler: .local dosyalarınız paket güncellemelerinden etkilenmez; yine de yeni sürümlerin değişiklik notlarını (changelog) okuyun (örneğin 1.1.1 iptables eylemini yeniden yazdı).
- Açık portlar: Fail2ban başarısız denemeleri loglayan servisleri korur, hiç ihtiyacınız olmayan portları kapatmaz. ss -tulpn çıktısını düzenli olarak gözden geçirin; açıkta kalan 111 numaralı portu (rpcbind) kapatma rehberimiz tipik bir örnektir.
Kontrol listesi
- Anahtarla giriş çalışıyor; sshd -T çıktısında passwordauthentication no ve kbdinteractiveauthentication no görünüyor.
- Sağlayıcı konsoluna veya kurtarma sistemine erişim test edildi.
- Ayarlarınız jail.local ya da jail.d/*.local içinde duruyor; jail.conf dosyasına dokunulmadı.
- ignoreip listesi ofis, VPN ve izleme adreslerinizi IPv4 ve IPv6 olarak içeriyor.
- banaction çalışan güvenlik duvarıyla eşleşiyor; auth.log olmayan sistemlerde sshd için backend = systemd, dosyadan okuyan jail'ler için backend = auto yazılı.
- fail2ban-regex her jail için eşleşme gösteriyor; test IP'sinden gelen deneme banlanıyor ve ban güvenlik duvarında görünüyor.
- Proxy arkasında realip yapılandırıldı, proxy aralıkları ignoreip listesinde ve engelleme Cloudflare tarafında yapılıyor.
- Tekrar eden saldırganlar için recidive ya da bantime.increment etkin.
Sık yapılan hatalar
- Kendinizi dışarıda bırakmak: Kendi bağlantınızdan test etmek ya da ignoreip ayarını unutmak.
- jail.conf dosyasını düzenlemek ve ardından her paket güncellemesinde çakışmayla uğraşmak.
- Yanlış backend: Debian 12'de auto (auth.log yok) ya da Ubuntu 24.04'te Nginx veya recidive için systemd (sessizce hiçbir şey yakalamaz).
- Port uyuşmazlığı: SSH 2222 numaralı porta taşındı ama jail'de hâlâ port = ssh yazıyor; ban yalnızca 22 numaralı portu kapatır. port = 2222 yazın ya da o jail'de banaction = %(banaction_allports)s kullanın.
- Proxy'yi banlamak: Cloudflare veya yük dengeleyici önünde realip yapılandırılmadığında banlar saldırgan yerine proxy adreslerini vurur.
Sorun giderme
| Belirti | Olası neden | Çözüm |
|---|---|---|
| Servis başlamıyor: "Have not found any log file for sshd jail" | rsyslog kurulu olmayan Debian 12 | sshd için backend = systemd ya da rsyslog kurulumu |
| Ubuntu 24.04'te servis başlamıyor: "No module named 'asynchat'" | Python 3.12 ile gelen ilk 24.04 paketi | sudo apt update && sudo apt install --only-upgrade fail2ban (1.0.2-3ubuntu0.1 ile düzeltildi) |
| Jail çalışıyor ama Total failed hep 0 | Yanlış backend ya da logpath, günlük eşleşmesi veya sshd-session logları (OpenSSH 9.8+) | fail2ban-regex ile test edin; backend ayarını düzeltin; 4. adımdaki gibi güncelleyin ya da geçersiz kılın |
| IP banlı görünüyor ama hâlâ erişebiliyor | Port uyuşmazlığı, proxy üzerinden gelen trafik ya da başarısız ban eylemi | port = ayarını, proxy yapılandırmasını ve fail2ban.log içindeki hataları kontrol edin |
| firewall-cmd veya "FirewallD is not running" içeren ban hataları | firewalld durmuş ya da kurulu değil | firewalld servisini başlatın ya da banaction değerini nftables-multiport yapın |
| Ofisiniz sürekli banlanıyor | Eksik ignoreip ya da aggressive modun bir izleme sistemini yakalaması | Banı kaldırın, adresi ignoreip listesine ekleyin, reload edin |
Sık sorulan sorular
SSH yalnızca anahtar kabul ediyorsa Fail2ban'a yine de ihtiyacım var mı?Yalnızca anahtarla giriş kabul eden bir SSH sunucusuna pratikte kaba kuvvetle girilemez; ama Fail2ban log gürültüsünü ve boşa harcanan bağlantıları azaltır, web girişleri, e-posta ve FTP gibi parola kabul eden servisleri de korur. Birçok yönetici sshd jail'ini kısa bir ban süresiyle açık tutar.
Hangi değerlerle başlamalıyım?
maxretry 3-5, findtime 10 dakika, bantime 1 saat ve en fazla bir haftaya çıkan bantime.increment iyi bir başlangıçtır. Değerleri ancak logları bir süre izledikten sonra sıkılaştırın; kalıcı banlar çoğunlukla yalnızca güvenlik duvarını doldurur.
Kendi IP'min banını nasıl kaldırırım, bir adresi nasıl beyaz listeye alırım?
sudo fail2ban-client unban komutunu kendi IP adresinizle çalıştırdığınızda ban tüm jail'lerden ve veritabanından kalkar. Ardından adresi jail.local içindeki ignoreip satırına ekleyip sudo fail2ban-client reload çalıştırın.
Fail2ban IPv6 ile çalışır mı?
Evet, 0.10 sürümünden beri; nftables, firewalld ve ufw eylemleri iki adres ailesini de destekler. Ofisinizin IPv6 ön eklerini ignoreip listesine eklemeyi unutmayın.
Fail2ban mı, CrowdSec mi?
İkisi de logları ayrıştırıp saldırganları engeller. CrowdSec tespit ile engellemeyi ayırır (engellemeyi yapan bileşenlere remediation component ya da bouncer denir) ve sinyalleri bir topluluk engelleme listesi (blocklist) üzerinden paylaşabilir; bu da çok sayıda sunucuyu birlikte yöneten ekiplere uyar. Fail2ban küçüktür, bütün büyük dağıtımlarda paket olarak bulunur ve denetlemesi kolaydır. Hangisini seçerseniz seçin, ikisinin aynı servisi yönetmesine izin vermeyin.
Sonuç ve sonraki adımlar
Önce SSH girişini anahtarla sınırlayın, gerisini Fail2ban'a bırakın: tek bir jail.local, güvenlik duvarınızla eşleşen bir ban eylemi, her log kaynağı için doğru backend ve ikinci bir IP'den yapılmış bir test. Sonraki adımda sitelerinizin ihtiyaç duyduğu Nginx jail'lerini ekleyin, recidive jail'ini etkinleştirin ve Cloudflare arkasındaysanız engellemeyi Cloudflare tarafına taşıyın. Kurulumun tamamına dışarıdan bir gözün de bakmasını isterseniz AIOR'un siber güvenlik, yedekleme ve veri koruma hizmetleri sunucu sertleştirmeyi de kapsıyor. Bir filtre eşleşmiyorsa jail.local dosyanızın ilgili kısmını ve fail2ban-regex çıktısını bu konuya ekleyin.Put a Linux server on a public IP and within hours its SSH log fills with lines such as "Invalid user admin from 203.0.113.50 port 51514". Fail2ban reads those logs and blocks addresses that keep failing. This guide sets it up for SSH and Nginx on Debian, Ubuntu, AlmaLinux and Rocky Linux, including the distribution defaults that break most copy-paste tutorials and the SSH settings that make most bans unnecessary. We checked every setting against the configuration files of the Fail2ban packages the distributions ship as of October 2026 (1.0.x and 1.1.x); upstream released 1.1.1 in August 2026.
How Fail2ban works
Fail2ban is a Python daemon with three parts:- Filter: regular expressions (failregex) that recognise a failure in a log line and extract the client IP (/etc/fail2ban/filter.d/).
- Jail: connects a filter to a log source (a file via logpath, or the systemd journal via backend = systemd) and to thresholds: an IP with maxretry failures within findtime is banned for bantime.
- Action: applies the ban and later lifts it, usually through an nftables set, an iptables chain, a firewalld rich rule or a ufw rule (/etc/fail2ban/action.d/).
Its limits matter as much. Fail2ban can't stop distributed "low and slow" attacks where thousands of addresses try once or twice each, a first attempt that succeeds (a leaked key, a weak password), application bugs such as SQL injection, anything that isn't logged, or volumetric DDoS. Treat it as a noise and load reducer on top of key-only SSH, not as the protection itself. OpenSSH 9.8 and later add PerSourcePenalties, which briefly refuses addresses that keep failing; the penalties are short (10 minutes at most by default), so it complements Fail2ban rather than replacing it. For the wider picture, see our thread on server-side hardening and defence in depth for production teams.
Step 1: Lock down SSH first
If password logins are off, a brute-force attack on SSH has nothing to guess, so start here. On your own computer, create a key, copy it to the server and confirm that it works:
Bash:
ssh-keygen -t ed25519 -C "you@laptop"
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@SERVER_IP
ssh admin@SERVER_IP
Then create a drop-in file on the server. Current Debian, Ubuntu and AlmaLinux/Rocky releases include /etc/ssh/sshd_config.d/*.conf at the top of sshd_config (check with grep -i include /etc/ssh/sshd_config).
Kod:
# /etc/ssh/sshd_config.d/00-hardening.conf
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
Use PermitRootLogin no if you log in as a normal user with sudo. Don't skip KbdInteractiveAuthentication: with PAM enabled, keyboard-interactive login can still ask for a password while PasswordAuthentication is off.
sshd uses the first value it reads for each keyword, and cloud images often carry /etc/ssh/sshd_config.d/50-cloud-init.conf, written by cloud-init, which may say PasswordAuthentication yes. Your 00-… file is read first, but find every conflicting line anyway and check the effective configuration rather than the files:
Bash:
sudo grep -ri "passwordauthentication" /etc/ssh/sshd_config /etc/ssh/sshd_config.d/
sudo sshd -t
sudo sshd -T | grep -Ei '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin|pubkeyauthentication)'
# Debian/Ubuntu (on AlmaLinux/Rocky the unit is sshd)
sudo systemctl reload ssh
Keep your current session open while you test from a second terminal, and find out how to reach your provider's web console or rescue system before you touch SSH or the firewall.
A non-standard SSH port only reduces log noise; it is not a security control. If you move it, tell the jail (see common mistakes), and on Ubuntu 22.10 and later follow Ubuntu's documentation, because ssh.socket handles the port. The rest of a sensible baseline is in our minimum viable server hardening checklist.
Step 2: Install Fail2ban
Debian and Ubuntu:
Bash:
sudo apt update
sudo apt install fail2ban
sudo systemctl enable --now fail2ban
On a fresh Debian 12 the service will probably fail to start at this point; Step 4 explains why, and the jail.local in Step 3 fixes it.
AlmaLinux and Rocky Linux 9 or 10 get Fail2ban from EPEL, and the EPEL documentation recommends enabling CRB first:
Bash:
sudo dnf config-manager --set-enabled crb
sudo dnf install epel-release
sudo dnf install fail2ban
sudo systemctl enable --now fail2ban
In EPEL, fail2ban is a meta-package that also installs fail2ban-firewalld, which makes firewalld the ban method. Without firewalld, install fail2ban-server instead and pick nftables in the next step.
Packaged defaults differ more than most tutorials admit. Check yours with fail2ban-client --version and ls /etc/fail2ban/jail.d/:
| Distribution | Packaged version | Defaults that matter |
|---|---|---|
| Debian 12 | 1.0.2 | sshd jail enabled, but it reads /var/log/auth.log, which a fresh Debian 12 doesn't have; banaction is iptables-multiport |
| Debian 13 | 1.1.0 (patched) | sshd jail reads the journal; bans through nftables |
| Ubuntu 24.04 | 1.0.2 | backend = systemd for every jail (set in the DEFAULT section); bans through nftables |
| Ubuntu 26.04 | 1.1.0 (patched) | Same as Debian 13 |
| AlmaLinux/Rocky 9 and 10 (EPEL) | 1.1.0 (patched) | No jail enabled; the sshd jail reads the journal once enabled; bans through firewalld rich rules |
Step 3: jail.local, the file you actually edit
Don't edit jail.conf: it belongs to the package, and local edits conflict with upgrades. Fail2ban reads jail.conf, jail.d/*.conf, jail.local and jail.d/*.local in that order, and later files win, so jail.local overrides both the stock defaults and the jail.d/*.conf files your distribution ships. A good starting point:
INI:
# /etc/fail2ban/jail.local
[DEFAULT]
# Never ban these: loopback, office IP, VPN range (example values)
ignoreip = 127.0.0.1/8 ::1 203.0.113.10 198.51.100.0/24
bantime = 1h
findtime = 10m
maxretry = 5
# Repeat offenders: 1h, 2h, 4h ... capped at one week
bantime.increment = true
bantime.maxtime = 1w
# Ban method: must match the firewall you actually run (see the table below)
banaction = nftables-multiport
banaction_allports = nftables-allports
[sshd]
enabled = true
port = ssh
backend = systemd
maxretry = 3
- ignoreip: addresses, CIDR ranges or host names that are never banned: your static office IP, VPN exit and monitoring systems. ignoreself (on by default) already covers the server's own addresses.
- bantime, findtime, maxretry: the stock values are 10m, 10m and 5, and units such as m, h, d and w work. Avoid bantime = -1 (permanent): it slowly fills the firewall with stale entries.
- bantime.increment: uses the ban history in the database to multiply the ban time for repeat offenders (1, 2, 4, 8… times by default), up to bantime.maxtime.
- backend = systemd for sshd: reads SSH events from the journal, whether or not auth.log exists; Step 4 explains why that matters.
Choose the ban action for the firewall that is actually running:
| Firewall on the server | banaction | banaction_allports | Where bans show up |
|---|---|---|---|
| nftables, or no firewall manager (Debian, Ubuntu) | nftables-multiport | nftables-allports | nft list table inet f2b-table |
| firewalld (AlmaLinux/Rocky default) | firewallcmd-rich-rules | firewallcmd-rich-rules | firewall-cmd --list-rich-rules |
| ufw, if you want bans listed in ufw | ufw | ufw | ufw status numbered |
| Legacy iptables | iptables-multiport | iptables-allports | iptables -S | grep f2b |
The ufw action blocks the address on all ports; nftables bans also work alongside ufw, which is how Ubuntu's own package is set up. On a firewalld server, set both lines to firewallcmd-rich-rules (the EPEL default), or jail.local will override it. On Debian 12, keep nftables explicit: the package still defaults to iptables-multiport, and a fresh Debian 12 usually has nftables but not the iptables command.
Step 4: The backend trap on Debian 12 and Ubuntu 24.04
Since Debian 12, rsyslog is no longer installed by default and logs go to the systemd journal, so a fresh install has no /var/log/auth.log. Debian 12's Fail2ban package (1.0.2) still points its enabled sshd jail at that file, and the service refuses to start:
Kod:
ERROR Failed during configuration: Have not found any log file for sshd jail
Pick one fix:
- Read the journal: backend = systemd in the sshd section, as in the jail.local above. python3-systemd is a recommended package on Debian 12, so apt normally installs it; otherwise run sudo apt install python3-systemd.
- Bring back the text log: sudo apt install rsyslog, then restart Fail2ban.
Ubuntu 24.04 has the opposite trap: its package sets backend = systemd in the DEFAULT section, so every jail reads the journal. The systemd backend ignores logpath, and Nginx logs to its own files, so an Nginx jail starts cleanly and never matches anything. Give every file-based jail, recidive included, backend = auto, as the Nginx examples below do. On AlmaLinux and Rocky, EPEL's optional fail2ban-systemd package has the same effect.
A third trap: OpenSSH 9.8 split the server into sshd and sshd-session, and some log lines now come from sshd-session. Fail2ban 1.1.1 and the Debian 13, Ubuntu 26.04 and EPEL packages handle this, but an older Fail2ban paired with a newer OpenSSH (say, Debian 12 with OpenSSH from bookworm-backports) can leave the sshd jail at zero failures. Upgrade if you can; otherwise override the daemon name:
INI:
# /etc/fail2ban/filter.d/sshd.local
[DEFAULT]
_daemon = sshd(?:-session)?
Then add this line to the existing sshd section of jail.local so the journal query includes the new process name:
INI:
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd + _COMM=sshd-session
Step 5: Test before you trust it
fail2ban-client -t only proves that the configuration loads, not that your filters match real log lines. After every change:
Bash:
sudo fail2ban-client -t
sudo systemctl restart fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
Healthy output names the log source and counts failures (values are illustrative):
Kod:
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 41
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 3
|- Total banned: 12
`- Banned IP list: 203.0.113.50 203.0.113.77 192.0.2.44
If Total failed stays at 0 while attacks show up in the log, run the filter against real data; the summary at the end shows how many lines matched:
Bash:
sudo fail2ban-regex systemd-journal sshd
# or test an excerpt (unit "ssh" on Debian/Ubuntu, "sshd" on AlmaLinux/Rocky)
sudo journalctl -u ssh --since "2 hours ago" --no-pager > /tmp/ssh-test.log
sudo fail2ban-regex --print-all-matched /tmp/ssh-test.log sshd
Then simulate an attack from a second address you control, such as a phone hotspot or another VPS, never from the connection you're working on:
Bash:
# On the test machine: an unknown user name counts as a failure in every mode.
# PubkeyAuthentication=no keeps it to one failure per attempt (every offered key adds a
# "Failed publickey for invalid user" line); accept-new stops BatchMode from aborting
# on an unknown host key before it even tries to log in.
for i in 1 2 3 4; do
ssh -o BatchMode=yes -o PubkeyAuthentication=no -o StrictHostKeyChecking=accept-new \
-o ConnectTimeout=5 nosuchuser@SERVER_IP
done
After the third attempt (maxretry = 3) the next connection is refused or times out. Since sshd from OpenSSH 9.8 on can also refuse it by itself (PerSourcePenalties), confirm the ban with fail2ban-client status sshd and in the firewall: nft list set inet f2b-table addr-set-sshd (created with the first ban; IPv6 addresses go to addr6-set-sshd), firewall-cmd --list-rich-rules or ufw status numbered. Unban the test address afterwards (next step).
The sshd filter also has modes. The default, normal, counts wrong passwords and unknown user names. With key-only SSH, bots that try real accounts such as root mostly leave lines like the one below, which only the aggressive (or ddos) mode counts:
Kod:
Connection closed by authenticating user root 203.0.113.9 port 51514 [preauth]
To ban those too, add mode = aggressive to the sshd section and put monitoring systems that connect to port 22 into ignoreip, because aggressive mode also counts connections that never finish the handshake.
Step 6: Unban an IP and handle repeat offenders
Bash:
# One jail
sudo fail2ban-client set sshd unbanip 203.0.113.50
# Every jail and the database
sudo fail2ban-client unban 203.0.113.50
# Which jails ban which addresses
sudo fail2ban-client banned
# Banned addresses with ban and expiry times
sudo fail2ban-client get sshd banip --with-time
Locked out? Connect from another address or through the provider's console, run fail2ban-client unban with your IP and add it to ignoreip in jail.local. fail2ban-client set sshd addignoreip only lasts until the next reload or restart.
For persistent attackers, the stock recidive jail reads Fail2ban's own log and bans an address on all ports for a week if it was banned repeatedly (maxretry times, 5 here) within one day:
INI:
# Append to /etc/fail2ban/jail.local
[recidive]
enabled = true
# reads a file: required on Ubuntu 24.04, where DEFAULT sets backend = systemd
backend = auto
The comment above recidive in jail.conf also suggests keeping database entries longer than the default one day:
INI:
# /etc/fail2ban/fail2ban.local (7.5 days, the value suggested in jail.conf)
[DEFAULT]
dbpurgeage = 648000
recidive needs Fail2ban to log to /var/log/fail2ban.log (the default) at a level other than DEBUG. It overlaps with bantime.increment; running both is fine, but one is usually enough on a single server.
Step 7: Nginx jails
Fail2ban ships Nginx filters for the error log (/var/log/nginx/*error.log by default, which also matches per-site files such as example.com.error.log):- nginx-http-auth: failed HTTP basic authentication (a wrong password or an unknown htpasswd user).
- nginx-limit-req: requests rejected by Nginx rate limiting; it needs limit_req configured.
- nginx-botsearch: probes for paths you don't have, such as phpMyAdmin, webmail or wp-login.php on a site that isn't WordPress. If your 404s only show up in the access log, point the jail's logpath there; the filter understands that format too.
- Optional: nginx-bad-request (400 responses in the access log) and, from Fail2ban 1.1.0, nginx-forbidden (requests refused by a deny rule).
A rate limit for a login page goes into the http block and the site's server block:
Kod:
# http { } context, e.g. /etc/nginx/conf.d/limits.conf
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=10r/m;
# server { } block of the site
location = /wp-login.php {
limit_req zone=wplogin burst=5 nodelay;
# copy the include and fastcgi_pass lines from your existing PHP location
}
Requests beyond the burst get status 503 (limit_req_status) and an error-level log line, "limiting requests, excess: … by zone "wplogin", client: …", which nginx-limit-req counts. Enable the jails; ports and log paths come from the stock definitions:
INI:
[nginx-http-auth]
enabled = true
backend = auto
[nginx-limit-req]
enabled = true
backend = auto
maxretry = 10
[nginx-botsearch]
enabled = true
backend = auto
A custom filter for WordPress logins
Rate limiting slows attackers down; a custom filter bans them. On a stock WordPress a failed login re-displays the form with status 200, while a successful one normally redirects with 302, so a POST to wp-login.php answered with 200 is a failure. Confirm that in your own access log first, because security plugins can change the responses.
INI:
# /etc/fail2ban/filter.d/nginx-wp-login.conf
[Definition]
failregex = ^<HOST> - \S+ \[\] "POST /(?:[^"\s]*/)?wp-login\.php(?:\?[^"\s]*)? HTTP/[\d.]+" 200\b
ignoreregex =
datepattern = ^[^\[]*\[({DATE})
{^LN-BEG}
Fail2ban cuts the timestamp out of the line before matching, hence the empty brackets in the regex. The filter expects Nginx's default combined log format. Test it, then add the jail:
Bash:
sudo fail2ban-regex --print-all-matched /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-wp-login.conf
INI:
[nginx-wp-login]
enabled = true
backend = auto
port = http,https
filter = nginx-wp-login
logpath = /var/log/nginx/access.log
/var/www/example.com/logs/access.log
maxretry = 6
findtime = 10m
bantime = 1h
List each per-site log on its own line; globs work too. Globs are expanded when Fail2ban starts or reloads, so run sudo fail2ban-client reload after adding a site with a new log file.
Behind Cloudflare or a reverse proxy
When Nginx sits behind Cloudflare, a load balancer or another proxy, two things go wrong by default:- The wrong IP in the logs. Without realip configured, every request seems to come from the proxy, Fail2ban bans the proxy, and the site goes down for many visitors at once. Restore the visitor address with set_real_ip_from for each Cloudflare range (listed at cloudflare.com/ips) and real_ip_header CF-Connecting-IP; nginx -V 2>&1 | grep -o http_realip_module confirms that your build has the module.
- Bans that don't bite. Proxied requests still arrive from the proxy's address, so a ban in the server's own firewall doesn't stop them. Block at the edge: Fail2ban's cloudflare-token action creates an IP Access Rule through the Cloudflare API (it needs your zone ID and an API token, and its action file asks for jail.local permissions of 640 because the token lives there), or use Cloudflare's own rate-limiting rules. Keep the local ban for direct hits on the origin, and allow ports 80 and 443 only from Cloudflare's ranges.
Kod:
# /etc/nginx/conf.d/cloudflare-realip.conf: one line per range from cloudflare.com/ips
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 2400:cb00::/32;
# ...the remaining IPv4 and IPv6 ranges...
real_ip_header CF-Connecting-IP;
INI:
# In the jail: ban at Cloudflare and locally
action = cloudflare-token[cfzone="YOUR_ZONE_ID", cftoken="YOUR_API_TOKEN"]
%(action_)s
Add proxy and load balancer addresses to ignoreip as a safety net. SSH doesn't pass through Cloudflare's HTTP proxy, so the sshd jail is unaffected.
Control-panel servers: check what's already running
cPanel has cPHulk built in, DirectAdmin has its Brute Force Monitor, and Plesk offers Fail2Ban as an installable component under Tools & Settings > IP Address Banning (Fail2Ban). Many cPanel and DirectAdmin servers also run CSF/LFD or Imunify360 (CSF's original developer, Way to the Web, discontinued it in August 2025 and it now lives on in forks, so make sure the version on your server still gets updates). Another banning tool on top means two programs editing one firewall and two places to look when a customer gets blocked. Check what is active first:
Bash:
systemctl list-units --type=service | grep -Ei 'fail2ban|lfd|csf|imunify|cphulk'
Then give each service a single owner. On Plesk, manage jails from the panel or its ip_ban utility rather than by editing files.
Running it day to day
- Log: /var/log/fail2ban.log records every Found, Ban and Unban: sudo grep -E " (Ban|Unban) " /var/log/fail2ban.log | tail -20
- Reload, don't restart: after an edit, run sudo fail2ban-client -t and then sudo fail2ban-client reload; current bans stay in place.
- Database: bans survive restarts in /var/lib/fail2ban/fail2ban.sqlite3, and dbpurgeage (1d by default) sets how long old entries stay.
- IPv6: supported since Fail2ban 0.10 by the nftables, firewalld and ufw actions. Add your IPv6 prefixes to ignoreip. One attacker often controls a whole /64, so per-address bans are weaker on IPv6, one more reason key-only SSH comes first.
- Upgrades: .local files survive package upgrades, but read the changelog of each new release anyway (1.1.1, for example, reworked the iptables action).
- Open ports: Fail2ban protects services that log failures; it doesn't close ports you never needed. Review ss -tulpn regularly; our guide to closing an exposed port 111 (rpcbind) is a typical case.
Checklist
- Key login works, and sshd -T shows passwordauthentication no and kbdinteractiveauthentication no.
- Provider console or rescue access has been tested.
- Settings live in jail.local or jail.d/*.local; jail.conf is untouched.
- ignoreip covers your office, VPN and monitoring addresses, IPv4 and IPv6.
- banaction matches the running firewall; sshd uses backend = systemd where there is no auth.log, and file-based jails use backend = auto.
- fail2ban-regex shows matches for every jail, and a ban from a test IP appears in the firewall.
- Behind a proxy: realip is configured, proxy ranges are in ignoreip and blocking happens at the edge.
- recidive or bantime.increment handles repeat offenders.
Common mistakes
- Locking yourself out by testing from your own connection or forgetting ignoreip.
- Editing jail.conf and then fighting package upgrades.
- The wrong backend: auto on Debian 12 (no auth.log), or systemd for Nginx or recidive on Ubuntu 24.04 (silently matches nothing).
- A port mismatch: SSH moved to 2222 but the jail still says port = ssh, so the ban only blocks port 22. Set port = 2222, or use banaction = %(banaction_allports)s in that jail.
- Banning the proxy: no realip in front of Cloudflare or a load balancer, so bans hit the proxy's addresses instead of the attacker.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Service won't start: "Have not found any log file for sshd jail" | Debian 12 without rsyslog | backend = systemd for sshd, or install rsyslog |
| Service won't start on Ubuntu 24.04: "No module named 'asynchat'" | The original 24.04 package with Python 3.12 | sudo apt update && sudo apt install --only-upgrade fail2ban (fixed in 1.0.2-3ubuntu0.1) |
| Jail runs but Total failed stays at 0 | Wrong backend or logpath, journal match, or sshd-session logging (OpenSSH 9.8+) | Test with fail2ban-regex; fix the backend; upgrade or override as in Step 4 |
| IP shows as banned but still gets through | Port mismatch, traffic through a proxy, or a failing ban action | Check port =, the proxy setup and errors in fail2ban.log |
| Ban errors mentioning firewall-cmd or "FirewallD is not running" | firewalld stopped or not installed | Start firewalld, or switch banaction to nftables-multiport |
| Your office keeps getting banned | Missing ignoreip, or aggressive mode catching a monitor | Unban, add to ignoreip, reload |
FAQ
Do I still need Fail2ban if SSH accepts keys only?A key-only SSH server can't be brute-forced in practice, but Fail2ban still cuts log noise and wasted connections, and it protects services that do take passwords, such as web logins, mail and FTP. Many admins keep the sshd jail with a short ban time.
Which values should I start with?
maxretry 3 to 5, findtime 10 minutes and bantime 1 hour, with bantime.increment capped at a week. Tighten only after watching the logs for a while; permanent bans mostly fill the firewall.
How do I unban myself or whitelist an address?
sudo fail2ban-client unban followed by your IP removes the ban from every jail and the database. Then add the address to ignoreip in jail.local and run sudo fail2ban-client reload.
Does Fail2ban work with IPv6?
Yes, since version 0.10; the nftables, firewalld and ufw actions handle both address families. Add your IPv6 office prefixes to ignoreip.
Fail2ban or CrowdSec?
Both parse logs and block offenders. CrowdSec separates detection from blocking (remediation components, also called bouncers) and can share signals through a community blocklist, which suits larger fleets; Fail2ban is small, packaged by every major distribution and easy to audit. Whichever you choose, don't let both manage the same service.