安裝包裡與 WAF 有關的參數只有幾個;誤判時要改的是哪個檔案、怎麼寫、怎麼讓它生效;怎麼確認 WAF 真的在擋而不只是在轉發;WAF 或後端掛掉時整條鏈會發生什麼;以及哪些事是刻意沒做的。
全部在主機 B 的 .env(由 install.sh 產生,root 0600)。改完跑 sudo bash install.sh --reconfigure,它會重新產生設定並重建有變動的容器。
| 參數 | 預設 | 作用 | 生效方式 |
|---|---|---|---|
| WAF_BACKEND_URL | 必填 | 被保護網站的內網位址含埠,如 http://192.168.0.111:8000。填錯的症狀是 WAF 對所有請求回 502 | --reconfigure(重建 waf-nginx) |
| WAF_RULE_ENGINE | On | On 擋;DetectionOnly 只記錄。後者規則全跑、分數全算、audit.log 全寫、平台事件照送,只是不回 403。上線第一天建議用它觀察誤判 | --reconfigure |
| WAF_PARANOIA | 1 | CRS Paranoia Level 1~4(第 1 篇第七節)。升級前先 DetectionOnly 跑一段時間 | --reconfigure |
| WAF_BACKEND_PATH | 空 | 歡迎頁與主站同 hostname 時,只有這個路徑前綴導到被保護網站(如 /beakplatform)。只影響 cloudflared ingress,WAF 本身不看它 | --reconfigure(重寫 tunnel ingress) |
| WELCOME_HOSTNAME | 空 | 有值就多起 welcome 與 waf-welcome 兩個容器(總覽第六節) | --reconfigure |
| TRUSTED_INGRESS_EXTRA | 空 | Vector 歸因時額外信任的直連來源。不是 WAF 的設定,但直接決定 WAF 事件的 actor.ip(第 3 篇第四節) | --reconfigure(重啟 vector) |
| ADMIN_IPS | 安裝時自動加入 SSH 來源與平台主機 | 誰能從內網直打 8080/8082 做驗證。不在名單內的來源連線逾時 | --reconfigure(重產 nftables) |
兩個不在 .env、要改 docker-compose.yml 才能動的值:ANOMALY_INBOUND=5、ANOMALY_OUTBOUND=4。它們是 CRS 出廠預設,本安裝包刻意不開放成參數:調門檻幾乎一定是錯的解法,誤判該用規則排除處理。
waf/modsec-overrides/modsecurity-override.conf 在 crs-setup.conf 之前載入(第 1 篇第三節)。出廠內容兩段:一段生效中的方法白名單,一段註解掉的路徑排除範例。
SecAction \
"id:900200,\
phase:1,\
nolog,\
pass,\
t:none,\
setvar:'tx.allowed_methods=GET HEAD POST OPTIONS PUT DELETE PATCH'"
CRS 的 crs-setup.conf 裡這條規則預設是註解掉的,此時 911100 的預設白名單只有 GET、HEAD、POST、OPTIONS。任何 REST API 的 PUT/DELETE/PATCH 都會命中 911100,等級 CRITICAL,一條就 5 分,直接 403。症狀是「網站看起來正常,但所有修改與刪除操作都失敗」,而且從瀏覽器看只是一個 403。本安裝包把它補回並加入三個 REST 方法。要再加方法(例如 WebDAV 的 PROPFIND)就改這一行。
SecRule REQUEST_URI "@beginsWith /beakplatform/api/notes/" \
"id:900300,\
phase:1,\
nolog,\
pass,\
ctl:ruleRemoveTargetByTag=attack-rce;ARGS,\
ctl:ruleRemoveTargetByTag=attack-injection-php;ARGS,\
ctl:ruleRemoveTargetByTag=attack-xss;ARGS,\
ctl:ruleRemoveTargetByTag=attack-sqli;ARGS"
| 片段 | 意義 |
|---|---|
| SecRule REQUEST_URI "@beginsWith /…/" | 只對這個路徑前綴生效。REQUEST_URI 含查詢字串;只比對路徑用 REQUEST_FILENAME |
| id:900300 | 自訂規則 ID。CRS 保留 900000~900999 給使用者的前置規則,不會跟 CRS 撞號;多條排除就 900301、900302 往下編 |
| phase:1 | 在請求標頭階段就執行,趕在 phase 2 的攻擊規則之前把目標移除。排除規則寫 phase 2 會太晚 |
| nolog, pass | 這條本身不記 log、不影響流程 |
| ctl:ruleRemoveTargetByTag=attack-sqli;ARGS | 對所有帶 attack-sqli 標籤的規則,把 ARGS 從檢查目標移除。URL、標頭、Cookie 照常檢查,只有參數值不查。這是最常用也最保守的排除方式:SQLi 規則還在,只是不看這個路徑的參數 |
其他常見的 ctl 寫法,由寬到窄:
| 寫法 | 效果 | 建議 |
|---|---|---|
| ctl:ruleEngine=Off | 整個路徑不檢查 | 只給健康檢查或內部 API |
| ctl:ruleRemoveByTag=attack-sqli | 整組 SQLi 規則對此路徑停用(所有目標) | 過寬 |
| ctl:ruleRemoveById=942100 | 單一規則對此路徑停用 | 可接受 |
| ctl:ruleRemoveTargetByTag=attack-sqli;ARGS | 整組規則只不看 ARGS | 常用 |
| ctl:ruleRemoveTargetById=942100;ARGS:note_body | 單一規則只不看某一個參數 | 最窄,優先 |
audit.log 的 messages[].details.data 會標出命中的變數名(如 ARGS:note_body),最窄的那種寫法所需的資訊都在裡面。
三件會讓人白忙的事
- 覆寫檔是 .template 掛載,容器啟動時才展開。改完要 docker compose up -d --force-recreate waf-nginx waf-welcome 或 --reconfigure;docker compose restart 不會重新展開模板
- 語法錯誤時容器起不來,網站直接不通。改完看 docker compose logs waf-nginx,正常會印 rules loaded inline/local/remote: 0/8xx/0
- 排除規則不影響已經建立的案件,也不會通知平台。平台上把誤判案件結案是另一件事
| # | 步驟 | 做法 |
|---|---|---|
| 1 | 先確認是 WAF 擋的 | 使用者看到的是 nginx 的 403 頁(沒有網站自己的版型);audit.log 該筆 is_interrupted: true。網站自己回的 403 不在 WAF 的責任範圍 |
| 2 | 找到規則與變數 | 從 audit.log 或 ClickHouse 的 raw 欄位取 messages[]:ruleId、data 裡的 ARGS:xxx、tags 裡的 paranoia-level |
| 3 | 判斷是不是設計上的必然 | 「使用者可以輸入 SQL 範例的欄位」「筆記內容含 HTML」這類是應用本身的性質,用排除處理;「表單參數名剛好叫 select」這類是巧合,也用排除處理;兩者都不該改門檻或降 PL |
| 4 | 寫最窄的排除 | 路徑前綴 + ruleRemoveTargetById=規則;ARGS:參數,加進覆寫檔,重建容器 |
| 5 | 驗證 | 重打原本被擋的請求應該通過;用第 1 篇第六節的 SQLi 探測打其他路徑仍應 403,確認排除沒有寫過寬 |
大量誤判、或準備升 PL 時,先把 WAF_RULE_ENGINE 設成 DetectionOnly 跑一到數天。此時所有本來會被擋的請求都放行、都記 log、都送平台,SOC 在處置中心看到的案件量就是上線後會被擋的量。從 ClickHouse 依 finding_rule_id 與 target_url 分組,逐條決定排除,再切回 On。
「網站打得開」只證明 nginx 在轉發,不證明規則引擎在跑。三個層次,由淺到深:
層次:安裝包自帶的狀態檢查
預期:WAF 那兩列:/ 回 200/3xx/404 都算正常(404 表示後端根路徑沒頁面);SQLi 探測 必須是 403
指令:
sudo bash /opt/integrated-waf/install.sh --status
層次:手動探測(從 ADMIN_IPS 內的機器)
預期:200、403、403、405(最後一個 405 是後端回的,證明 PATCH 通過了 WAF)。若 SQLi 回 200 而 WAF_RULE_ENGINE=On,先看容器 log 有沒有載入規則
指令:
B=http://192.168.0.112:8080
curl -s -o /dev/null -w '%{http_code}\n' "$B/beakplatform/auth/login"
curl -s -o /dev/null -w '%{http_code}\n' "$B/beakplatform/?id=1%27%20OR%201=1--"
curl -s -o /dev/null -w '%{http_code}\n' "$B/beakplatform/?q=%3Cscript%3Ealert(1)%3C/script%3E"
curl -s -o /dev/null -w '%{http_code}\n' -X PATCH "$B/beakplatform/auth/login"
層次:看 audit.log 有沒有跟著多一行
預期:每個探測一行;SQLi 那筆有 942100 與 949110,is_interrupted True。client_ip 是你打的那台機器的 IP
指令:
sudo tail -n 3 /opt/integrated-waf/waf/log/audit.log | python3 -c '
import sys, json
for line in sys.stdin:
t = json.loads(line)["transaction"]
print(t["request"]["method"], t["request"]["uri"][:60], t["response"]["http_code"], t["client_ip"], t.get("is_interrupted"))
for m in t.get("messages", []):
print(" ", m["details"]["ruleId"], m["message"][:70])'
層次:看 Vector 有沒有讀走(密碼在 .env)
預期:探測後幾秒內出現。內網探測的事件會在這裡,但不會進平台(內網對內網被 intake_filter 丟掉),要看端對端建案請用安裝包的 --test-event 或從 Internet 打
指令:
docker compose -f /opt/integrated-waf/docker-compose.yml exec clickhouse
clickhouse-client --user secstack --password "$CLICKHOUSE_PASSWORD" --query
"SELECT event_time, finding_rule_id, IPv6NumToString(actor_ip), target_url
FROM secstack.events WHERE source_system='coraza'
ORDER BY event_time DESC LIMIT 5"
| 掛掉的是 | 網站還通嗎 | 還會擋嗎 | 其他影響 |
|---|---|---|---|
| waf-nginx 容器 | 不通 | — | inline 的代價:WAF 是請求路徑上的一站,它掛了就是斷線。cloudflared 回 502/530,內網 8080 連線被拒。restart: unless-stopped 會自動拉起來,起不來多半是覆寫檔語法錯或後端位址格式錯 |
| 被保護網站 | 502 | 會 | WAF 對每個請求回 502 並記 audit.log(5xx 是相關狀態)。平台會收到一批 rule_id 空、severity Low、actor 各不相同的事件,量由封頂控制。這些案件不該封 IP,訪客是無辜的;單人版的自動封鎖門檻是 severity ≥ 4,這類到不了 |
| Vector | 通 | 會 | audit.log 照寫。Vector 重啟後從 checkpoint 續讀,中間的行不會丟;但若停機超過一小時,輪替會把還沒讀的行搬到 .YYYYMMDD-HH 檔,Vector 不讀輪替檔,那一小時的事件只剩 gz 裡的原始 log |
| od-bridge 或平台 | 通 | 會 | WAF 完全不受影響。事件進 ClickHouse,送平台那一路失敗即丟(sink 無緩衝)。攻擊當下的阻擋一件都不會少,少的是案件與封鎖 |
| cloudflared | 對外不通,內網 8080 通 | 會 | WAF 只是收不到 Internet 流量。熱備切換是另一份文件的主題 |
| 沒做的事 | 理由或現況 |
|---|---|
| 速率限制、登入暴力破解偵測 | CRS PL1 沒有速率規則,本安裝包也沒開 nginx limit_req。長期的機械式攻擊(撞庫、爬蟲)建議從 ClickHouse 統計後封 IP,在防火牆丟掉比在規則引擎逐請求比對便宜。平台端只能靠併案計數看出重複 |
| TLS 終結 | 容器有 8443 與自簽憑證,但沒發布。走 Cloudflare 時 TLS 在 edge 終結;不走 Cloudflare 的內網部署本來就是明文。要自己終結 TLS 的讀者可在 compose 發布 8443 並換憑證,但那不在本安裝包的測試範圍 |
| 規則自動更新 | 映像檔是浮動 tag owasp/modsecurity-crs:nginx,安裝時拉到什麼就是什麼。更新規則=docker compose pull waf-nginx && docker compose up -d waf-nginx,會同時換 nginx、引擎與 CRS 版本,升級後要重跑第四節的探測,因為新版 CRS 可能多出新規則造成新的誤判 |
| 把平台的封鎖清單推回 WAF | WAF 逐請求判斷,沒有「這個 IP 直接拒絕」的清單。封鎖落地點是 nftables/CrowdSec/EDL。要在 WAF 前面擋掉已封鎖的 IP,靠的是邊界防火牆抓 EDL(原系列第 6 篇) |
| GeoIP 封鎖 | 映像檔有 nginx 的 geoip 模組,但沒有資料庫也沒有設定。國別資訊在 Cloudflare 的 Cf-Ipcountry 標頭,平台事件的 actor.country 從那裡來,要依國別處置在平台流程裡做 |
| JSON 回應的外洩檢查 | SecResponseBodyMimeType 只含 text/plain、text/html、text/xml。API 的錯誤訊息若含 SQL 錯誤或堆疊,WAF 看不到。這是 CRS 的預設,本安裝包沒有擴充 |
| CRS 外掛 | setup.conf 有掛載點但沒裝任何外掛(如 WordPress/Nextcloud 的排除包)。被保護網站是這類應用時,讀者可自行放進 plugins/ |
| 症狀 | 原因與處理 |
|---|---|
| SQLi 探測回 200 | 先確認打的是 8080 不是後端埠;再看 .env 的 WAF_RULE_ENGINE 是不是 DetectionOnly;再看 docker compose logs waf-nginx 有沒有 rules loaded |
| 正常請求全部 502 | WAF 連不到 WAF_BACKEND_URL。在主機 B 上直接 curl 那個位址;被保護網站若有 IP 白名單要放行主機 B |
| 正常請求回 000(無回應) | 後端對根路徑不回應,改測實際頁面路徑。BeakPlatform 的根路徑就是這樣 |
| 所有 PUT/DELETE/PATCH 都 403 | 覆寫檔的 900200 沒生效,多半是改了檔案但沒重建容器。audit.log 會看到 911100 |
| 從內網打 8080 連線逾時 | 來源不在 ADMIN_IPS,被 nftables drop。改 .env 後 --reconfigure。逾時與 403 是兩件事:逾時代表根本沒到 WAF |
| ClickHouse 一堆 920350 事件 | 有人用 IP 直打 8080。不是攻擊,改用 hostname 或接受它 |
| 平台上沒有內網探測的案件 | 預期行為:內網 actor 對內網 target 在 Vector 就被丟掉。端對端驗證用 --test-event(走合成事件注入口)或從 Internet 打 |
| 改了覆寫檔後容器一直重啟 | 規則語法錯誤。docker compose logs waf-nginx 會印出是哪一行;常見是反斜線續行後面多了空白 |
| 升級映像後突然多了誤判 | 新版 CRS 加了規則。從 audit.log 的 ver 欄位確認版本,依第三節處理 |
| audit.log 擁有者顯示成奇怪的帳號名 | 容器內 nginx 的 uid 101 對應到主機上剛好占用 101 的帳號,正常 |