Fail2ban 自訂 banaction 結合 Cloudflare 的實作筆記
本文記錄了如何利用 Fail2ban 搭配 Cloudflare API 來有效阻擋惡意 IP。文中詳細說明了 Fail2ban 在使用 Cloudflare CDN 時的常見問題,並提供了取得真實 IP、設定自訂 banaction 的完整步驟與實作細節,讓你避開設定上的各種坑,輕鬆提升伺服器安全性。

問題背景
在閱讀本文前,有一些基礎概念需要稍微解釋一下,這將幫助你理解為什麼我們需要使用 Fail2ban 結合 Cloudflare API 的方式來阻擋攻擊者的 IP。
[!tip] 問題所在的核心原因
Fail2ban 是一個常用於保護伺服器安全的工具,能透過監測主機的 log 檔案,自動封鎖具有惡意行為的 IP 位址。但在搭配使用 Cloudflare CDN 服務的情況下,這個方式卻會失效。
[!tip] 為什麼一定要透過 Cloudflare API 阻擋 IP
由於攻擊的請求是透過 Cloudflare CDN 進入你的伺服器,因此在伺服器層級封鎖攻擊者的真實 IP 是無效的。
真正有效的做法,是要直接透過 Cloudflare 提供的 API,將攻擊者的 IP 加入 Cloudflare 自身的防火牆規則中,這樣才能在流量進入 CDN 時就直接被阻擋,徹底防止攻擊流量進入到你的主機。
[!tip] 腳本用途的補充說明
本文中提供的 Bash 腳本會定期從 Cloudflare 官方網站取得最新的 Proxy IP 清單。之所以需要定期執行這個動作,是因為 Cloudflare 的 Proxy IP 並非固定不變,Cloudflare 會不定期增加或調整 Proxy IP 範圍。如果沒有定期更新這份 IP 清單,伺服器上的 log 就可能無法正確識別訪客的真實 IP,導致 Fail2ban 或其他 IP 偵測工具無法有效運作。定期更新 IP 清單的動作確保 Nginx 可以信任最新的 Cloudflare Proxy IP,並透過 HTTP header (CF-Connecting-IP) 正確記錄訪客真實 IP,為後續的封鎖攻擊 IP 做好準備。
這也是為什麼我們必須透過 Cloudflare Firewall API 去進行真正的 IP 封鎖。
解決方案概述
最近,我的伺服器遭遇了斷斷續續的攻擊,機器人透過腳本不斷嘗試存取一些並不存在的 PHP 檔案(例如:wp-login.php、xmlrpc.php 等),試圖找到潛在的漏洞或導致服務過載,給我造成了很大的困擾。
為了阻擋這些惡意請求,我選擇了常見的伺服器安全工具——Fail2ban。它可以透過分析伺服器的 log 檔案,自動偵測並阻擋有惡意行為的 IP,原本認為這是一個快速且有效的解決方式。
然而,當我搞定 Fail2ban 之後,卻發現還有一些意想不到的坑需要處理。因為我前面還有掛一個 Cloudflare 作為 proxy,所有的請求都先經過 Cloudflare,再轉發到我的主機。如果你單純 Ban 掉那些 IP,那恭喜你,你把全部使用者都給 Ban 了,因為 Fail2ban 封鎖的 IP 實際上並非攻擊者的真實 IP,而是 Cloudflare 的 Proxy IP。
為了解決這個問題,我首先試著解決 log 紀錄訪客真實 IP 的問題,撰寫了一個定期更新 Cloudflare IP 清單的腳本,讓 Nginx log 正確記錄真實 IP。原本以為這樣 Fail2ban 就能順利封鎖惡意 IP,但結果卻發現實際封鎖的仍然是 Cloudflare Proxy 的 IP,導致封鎖失效。
思考過後才想到可以用 Cloudflare 提供的 Firewall API,將惡意 IP 直接封鎖在 Cloudflare 那裡,這樣才可以真正的阻擋這些攻擊 IP。
實作步驟
1. 環境準備
我伺服器作業系統是 Ubuntu 19.04
如果你不是這個作業系統,可能有些地方指令會不太一樣,不過概念上是差不多的,可以斟酌參考。
- Ubuntu 19.04
- Nginx
2. 設定 Nginx 取得真實 IP
為了讓 Nginx 內的 log 取得真實的 IP 必須在 nginx.config 上下點功夫,以下是我寫的一個 Bash 腳本,會定期從 Cloudflare 抓取最新的 Proxy IP 清單,並自動更新 Nginx 的設定檔,使 Nginx 能正確識別訪客真實 IP:
#!/bin/bash
CF_IPV4_URL="https://www.cloudflare.com/ips-v4"
CF_IPV6_URL="https://www.cloudflare.com/ips-v6"
OUTPUT_FILE="/etc/nginx/conf.d/real_ip_cloudflare.conf"
# 產生檔案,確保每次都完全覆蓋
{
echo "# Auto-generated Cloudflare IP allow list"
echo "# Last updated: $(date)"
curl -s $CF_IPV4_URL | while read ip; do
echo "set_real_ip_from $ip;"
done
curl -s $CF_IPV6_URL | while read ip; do
echo "set_real_ip_from $ip;"
done
echo "real_ip_header CF-Connecting-IP;"
echo "real_ip_recursive on;"
} > "$OUTPUT_FILE"
# 測試 nginx 設定,如果正確才 reload
if nginx -t; then
systemctl reload nginx
else
echo "Nginx config test failed. Not reloading."
fi
Nginx 如果你沒改動過設定,會有 autoload 機制,所以只要放在這路徑之下就可以正確運作了
主要是這三個設定
- set_real_ip_from 指定允許 Nginx 信任 Cloudflare 的 Proxy IP。
- real_ip_header CF-Connecting-IP 指定真正訪客的 IP 是由 Cloudflare 提供的這個 header (CF-Connecting-IP) 所傳遞。
- real_ip_recursive on 表示當 Nginx 在多層 proxy 時會往前遞迴找真正的訪客 IP。
這個時候可以檢查一下 nginx 的 access.log 確認一下 IP 是否是真實 IP 了
3. 配置 Fail2ban
安裝過程我就不講了,網路上教學一大堆,肯定比我寫得詳細
Fail2ban 的運作主要圍繞以下三個核心概念進行設定:
- action:發現惡意行為後要執行的動作
- filter:定義要在 log 中搜尋什麼模式
- jail:把 filter + action 組合起來的設定單元
3.1 設定 Filter
filter.d/nginx-php-attacks.conf
[Definition]
failregex = ^<HOST> - - \[.*\] "(GET|POST).*\.php.*HTTP.*" 404
ignoreregex =
這邊我設定只要是 GET or POST
訪問 .php
檔案,status code 404 我就算一次紀錄
3.2 設定 Jail
jail.d/nginx-php-attacks.local
[nginx-php-attacks]
enabled = true
port = http,https
filter = nginx-php-attacks
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 600
bantime = 86400
action = cloudflare[name=nginx-php-attacks], iptables-multiport[name=nginx-php-attacks, port="http,https"], ip6tables-multiport[name=nginx-php-attacks, port="http,https"]
使用 nginx-php-attacks
這個 filter 去監聽 logpath /var/log/nginx/access.log
只 600 秒內給你 5 次機會,你超過了就讓你坐牢 24 小時
其實只需要
cloudflare[name=nginx-php-attacks]
就可以了,只是我多了 iptables 也一起去 ban IP
3.3 設定 Action
接下來就是比較複雜的 action 設定了,最多坑都在這邊
Fail2ban 官方提供的 Cloudflare action 已經過時 github source 另外一個坑是內建的 cloudflare 的 API 已經棄用,建議用新的 API 執行 action:
action.d/cloudflare.conf
[Definition]
actionban = curl -4 -s -o /dev/null -X POST <_cf_api_prms> \
-d '{"mode":"block","configuration":{"target":"<cftarget>","value":"<ip>"},"notes":"Fail2Ban <name>"}' \
<_cf_api_url>
actionunban = id=$(curl -4 -s -X GET <_cf_api_prms> \
"<_cf_api_url>?mode=block&configuration_target=<cftarget>&configuration_value=<ip>&page=1&per_page=1" \
| jq -r '.result[0].id')
if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;
curl -4 -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id"
_cf_api_url = https://api.cloudflare.com/client/v4/zones/<cfzone>/firewall/access_rules/rules
_cf_api_prms = -H 'Authorization: Bearer <cftoken>' -H 'Content-Type: application/json'
[Init]
cfzone = YOUR_ZONE
cftoken = YOUT_ZONE_TOKEN
我用了 Cloudflare 比較安全的 Zone
方式來使用 Firewall API
執行 ban
的時候我會打 API 執行 block IP unban
的時候會打 API 刪除 block 設定
用了 jq 這個套件來處理 response 如果沒有這個套件要記得裝一下
which jq
可以確認
另外要注意的是我用了curl -4
意思是我用 IPv4 發出請求,不然 token 會因為 IP 不符擋下來
4. 設定 Cloudflare API
因為要透過 API 設定 Firewall,所以我們必須設定一個 API Token 確保安全 User API Tokens 點下新增,設定可以參考
- Zone:Read, Firewall Services:Edit
必須要設定主機 IP 這也是為什麼我上面有提到 curl 要用 IPv4 去發送的原因
也別忘了 Zone ID 在主頁右下角
如此一來我們就可以把 Zone ID 跟 Token 貼回剛剛的 action.d/cloudflare.conf
5. 驗證與測試
記得只要改動設定檔都要重啟 service fail2ban restart
可以使用指令
fail2ban-client status
fail2ban-client status nginx-php-attacks
確認我們的 nginx-php-attacks
有正確的設定了,也可以測試一下
fail2ban-client set nginx-php-attacks banip 168.61.83.86
如果有成功你會看到
|- Currently banned: 1
`- Banned IP list: 168.61.83.86 ...
記得 unban 也要測試下
fail2ban-client set nginx-php-attacks unbanip 168.61.83.86
總結
完成以上步驟後,整個流程就順利串接起來了,現在 Fail2ban 可以透過 Cloudflare API 有效地管理封鎖(ban)與解除封鎖(unban)的 IP。這個解決方案的主要優點是:
- 能夠正確識別並封鎖攻擊者的真實 IP
- 在 CDN 層級就阻擋惡意流量
- 自動化處理封鎖和解鎖流程
- 定期更新 Cloudflare IP 清單,確保系統持續有效運作
需要注意的是:
- 要定期檢查 Fail2ban 的運作狀態
- 確保 Cloudflare API Token 的安全性
- 監控系統日誌,及時發現異常情況