iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
自我挑戰組

一鍵完成六套開源防禦系統整合系列 第 14 篇

WAF第 2 篇 一個請求在 WAF 容器內的旅程

  • 分享至 

  • xImage
  •  

從 nginx 接到請求,到回 403 或轉給被保護網站、再把回應送回去,中間有五個檢查階段。本篇說明每個階段看什麼、在哪一點決定攔下、放行時往後端帶了什麼標頭、以及為什麼 real_ip 模組被刻意設成不會生效。

一、五個階段

ModSecurity 把一筆交易切成五個 phase,connector 把它們掛在 nginx 處理請求的不同時點上。規則各自宣告自己屬於哪個 phase;同一個 phase 內的規則依載入順序執行。
https://ithelp.ithome.com.tw/upload/images/20260928/20184261kuqT1tr7lt.png

階段 引擎拿到什麼 本安裝包的規則在這裡做什麼
phase 1 請求標頭 方法、URI、查詢字串、所有請求標頭、Cookie。本文還沒讀
phase 2 請求本文 ARGS(查詢字串與表單參數合併)、JSON 解析後的鍵值、multipart 各段、原始本文
轉送 — nginx 依 includes/proxy_backend.conf 把請求送到 BACKEND,第五節有實際內容
phase 3 回應標頭 後端回的狀態碼與標頭
phase 4 回應本文 text/plain、text/html、text/xml 的前 1 MB
phase 5 記錄 整筆交易

二、403 是誰回的

949110 的動作是 deny,引擎把交易標成 interrupted,connector 讓 nginx 以 403 結束這個請求。三件事值得寫清楚:

事實 說明
後端完全不知道 被擋的請求沒有到過 proxy_pass。被保護網站的 access log 不會有這筆,平台自己的限流、稽核也不會看到它。要查被擋的請求只能看 WAF 的 audit.log 或 ClickHouse
403 頁面是 nginx 的,不是後端的 映像檔在 cors.conf 用 more_set_headers -s 403 對 403 補了 Content-Type: text/html 與幾個 CORS 標頭,本文是 nginx 內建的 403 頁。攻擊者看到的回應裡沒有任何後端的痕跡,也沒有 CRS 規則編號
audit.log 的 is_interrupted 會是 true 這是分辨「命中規則但放行」與「真的被擋」最直接的欄位。Vector 目前沒有把它抽出來,平台端要看被擋與否得看 response.http_code 是不是 403 或 messages 裡有沒有 949110

三、nginx 自己先擋掉的東西

有些請求連 phase 1 都到不了,因為 nginx 在解析階段就拒絕了。這類請求不會出現在 audit.log,也就不會變成平台事件。實測到的一個:

請求 回應 誰回的、為什麼 log 沒有
TRACE / 405 nginx 本身不支援 TRACE,解析到方法就回 405,ModSecurity 沒有介入。CRS 的 911100 對 TRACE 本來會記 CRITICAL,但輪不到它。2026-09-26 實測 audit.log 沒有這筆
請求本文 > 12.5 MB 413 這條是引擎回的(SecRequestBodyLimitAction Reject),會進 audit.log,因為 413 符合 RelevantStatus
畸形到 nginx 解析不了的 HTTP 400 nginx 回,不進 audit.log。走 Cloudflare 時這類請求多半在 edge 就被擋了

四、放行時往後端帶什麼

這是容器內 includes/proxy_backend.conf 生效中的內容(後端位址已換成文件用的示範值):

proxy_set_header Host $host;
proxy_set_header Proxy "";
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_buffering off;
proxy_connect_timeout 60s;
proxy_read_timeout 36000s;
proxy_redirect off;
proxy_pass_header Authorization;
proxy_pass http://192.168.0.111:8000;
set_real_ip_from 127.0.0.2;
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
標頭 經 Cloudflare 時的值 意義
Host 對外 hostname(如 app.example.com) 原樣轉給後端。後端若依 Host 判斷 vhost 或組對外網址,看到的是對外名稱,不是 WAF 的內網 IP
X-Real-IP 172.18.0.250(cloudflared 容器) $remote_addr,也就是直連 WAF 的對象。因為 real_ip 模組被設成不生效(第六節),這裡不是攻擊者 IP
X-Forwarded-For 203.0.113.42, 172.18.0.250 nginx 把自己看到的 $remote_addr 附加到已有的 XFF 後面。Cloudflare 已經放了真實訪客,所以後端看到「訪客, cloudflared」兩段
X-Forwarded-Proto http WAF 收到的是 tunnel 內的明文 HTTP,所以是 http,不是 https。後端若據此組 redirect 網址會得到 http://,BeakPlatform 用系統設定的對外網址而不是這個標頭,正是為了避開這件事
CF-Connecting-IP、Cf-Ipcountry 等原始標頭 203.0.113.42、TW nginx 預設把沒有被 proxy_set_header 覆寫的請求標頭原樣透傳,所以後端仍拿得到 Cloudflare 寫的真實來源
Authorization 原樣 proxy_pass_header 確保 Basic/Bearer 認證能過
Upgrade / Connection 依請求 WebSocket 升級可以通過。proxy_read_timeout 36000s 是為了長連線不被切

被保護網站看到的 TCP 來源永遠是主機 B(docker MASQUERADE 後的實體網卡 IP)。要知道真實訪客,後端得讀 XFF 或 CF-Connecting-IP,而且只該在來源是主機 B 時才信。BeakPlatform 這一側的做法是 TRUSTED_PROXY_IPS 設成防禦節點的 IP、只信任該來源送的 CF-Connecting-IP;其他後端要自己做等價的事。

五、為什麼 real_ip 刻意設成不會生效

nginx 的 real_ip 模組原本的用途是:來源在 set_real_ip_from 清單內時,把 $remote_addr 換成 real_ip_header 指定的標頭值。直覺的設法是 set_real_ip_from 172.18.0.250(cloudflared),這樣 nginx 眼中的來源就會變成真實訪客。本安裝包沒有這樣做,而是設成 127.0.0.2,一個永遠不會是來源的位址。三段推理:

# 推理
1 ModSecurity 記進 audit.log 的 client_ip 來自 connector 餵給它的連線來源。如果 real_ip 生效,這個值會變成標頭裡的 IP,原始的直連來源就消失了,log 裡再也看不出這筆是 cloudflared 送的還是內網某台機器送的
2 「要不要相信標頭裡的 IP」是 Vector 在做的判斷:client_ip 在可信直連來源清單(TRUSTED_INGRESS_LIST,預設只有 cloudflared)內,才採信 CF-Connecting-IP/XFF;否則誰打的就記誰。這個判斷需要原始的 client_ip 還在,所以 nginx 這一層不能先換掉它
3 如果改成信 cloudflared 的 IP,且 real_ip 也吃 XFF,那任何能打到 8080 的內網機器帶一個假的 CF-Connecting-IP,就能讓 WAF 事件的攻擊者 IP 變成任意第三方,進而讓平台去封一個無辜的位址。nftables 的 8080 來源管制是第一道,Vector 的歸因比對是第二道,兩道都靠「client_ip 是原始值」才成立

代價是 nginx 的 access log 與 X-Real-IP 都是 cloudflared 的 IP,看起來像「所有請求都來自同一個位址」。這是刻意的,攻擊者歸因在 Vector 做、證據在 audit.log 的 request.headers 裡,兩邊都沒有損失。原系列第 5 篇第一節從整體信任邊界的角度談同一件事。

六、誰打得到 8080

來源 路徑 管制
cloudflared 容器 docker 網段內直連 waf-nginx:8080 不經實體網卡、不經 nftables forward chain。這是正式流量唯一的入口,攻擊者從 Internet 只到得了 Cloudflare
平台主機、ADMIN_IPS 內的機器 實體網卡 → DNAT → 容器 nftables ingest_guard_forward 放行。安裝後的內網驗證走這條
其他內網來源 同上 同一條 chain drop。症狀是連線逾時,不是 403;看到 403 代表已經進到 WAF 了
IPv6 來源 — compose 把埠綁在 0.0.0.0,只有 IPv4 會被發布,IPv6 連不進來(這是修過的坑:不寫位址時 docker-proxy 會同時聽 [::],繞過 forward chain 的來源管制)
任何來源打 8443 — 容器內有 TLS 監聽,但 compose 沒有發布,外面連不到

容器內另有兩個 nginx 自己回應、不經後端的路徑:/healthz(回 200 OK,compose 的 healthcheck 用)與 /metrics/nginx(stub_status,只允許 127.0.0.0/24)。它們在 location_common.conf 裡、關掉了 access log,但仍然經過 ModSecurity,用它們做驗證時一樣會有規則命中。

七、實測:五種請求的結果

2026-09-26 從 ADMIN_IPS 內的管理機用 IP 直打防禦節點 8080,被保護網站是 BeakPlatform。原始輸出在 /opt/tmp/verify/20260926-waf-doc-probe.log。

請求 回應 走到哪
GET /beakplatform/auth/login 200 phase 1 命中 920350(用 IP 打),3 分未達門檻 → 轉後端 → 後端回登入頁 → phase 4 無外洩 → audit.log 一行(有命中就記)
GET /beakplatform/?id=1' OR 1=1-- 403 phase 2 命中 942100,總分 8 → 949110 deny → 沒到後端 → audit.log 一行,is_interrupted: true
GET /beakplatform/?q=alert(1) 403 phase 2 命中 941100/941110/941160/941390,總分 23 → 同上
TRACE /beakplatform/ 405 nginx 解析階段拒絕,ModSecurity 沒跑,audit.log 沒有
PATCH /beakplatform/auth/login 405 覆寫檔的 900200 把 PATCH 列為允許 → 911 不加分 → 轉後端 → 後端回 405(Allow: HEAD, GET, OPTIONS, POST,回應標頭裡有平台自己的 CSP)→ 405 符合 RelevantStatus → audit.log 一行。這一列證明兩件事:方法白名單有生效,以及 4xx 即使不是 WAF 擋的也會記

八、兩個容器怎麼分流

cloudflared 的 ingress 規則決定請求進哪個 WAF 容器。cf_tunnel.py 產生的規則依序比對,同一個 hostname 帶 path 的排前面:

部署方式 ingress 規則(依序)
主站與歡迎頁不同 hostname app.example.com → http://waf-nginx:8080;www.example.com → http://waf-welcome:8080;其餘 → 404
同一個 hostname,用路徑前綴分(--backend-path /beakplatform) www.example.com 且 path 符合 ^/beakplatform(/或$) → http://waf-nginx:8080;www.example.com 其餘路徑 → http://waf-welcome:8080;其餘 hostname → 404
沒有歡迎頁 app.example.com → http://waf-nginx:8080;其餘 → 404

兩個容器都不做 hostname 比對(SERVER_NAME "_"),分流責任完全在 cloudflared。內網直打時沒有 cloudflared,8080 一律是主站、8082 一律是歡迎頁。


上一篇
WAF 第 1 篇 元件與規則引擎
下一篇
WAF第 3 篇 audit.log:WAF 與平台之間唯一的線
系列文
一鍵完成六套開源防禦系統整合 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言