iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
自我挑戰組

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

cloudflared:由主機 B 往外撥出的對外入口

  • 分享至 

  • xImage
  •  

抓圖方便,所以直接把寫作環境的部份IP直接透露,反正鐵人賽後會刪VM,真的別拿來說酸,文章就是要讓人看得懂得學得會

防禦節點(主機 B)上除了 WAF,還裝了幾個別人寫的系統。這一系列每篇只談一個,不深入它的設定細節,只回答三件事:它是什麼、它在主機 A 與主機 B 之間佔哪一段、少了它整體會缺什麼。本篇是 cloudflared。

先講結論。cloudflared 讓 Internet 上的訪客連得到主機 B 的 WAF,而主機 B 不必有公網位址、不必開任何入站埠、不必自己準備 HTTPS 憑證。做法是反過來:由 cloudflared 從主機 B 往 Cloudflare 撥出幾條長連線,訪客的請求沿這幾條連線送回來。它是選用的,安裝時沒有給 tunnel token 就不會有這個容器,其他元件照常運作,WAF 只能從內網連。它只負責搬運,不檢查內容,也不是封鎖決策的落地點。它停掉,對外服務立刻中斷,內網的一切不受影響。

一、它是什麼

cloudflared 是 Cloudflare 公司開源的一支程式,授權 Apache 2.0。它是 Cloudflare Tunnel 這項服務放在使用者這一端的那一半,Cloudflare 的文件稱它為連接器(connector)。本安裝包用的容器映像是 cloudflare/cloudflared:2026.8.3。

要把內網的網站公開到 Internet,傳統做法是由外往內:申請固定的公網位址,在路由器上做通訊埠轉發,再替網域申請憑證。Cloudflare Tunnel 的方向相反。cloudflared 啟動後主動連到 Cloudflare 的資料中心,把連線留著;訪客連的是 Cloudflare,Cloudflare 再把請求沿這些已經建立的連線交給 cloudflared。對主機 B 的防火牆來說,從頭到尾只有往外的連線。

它在本安裝包裡的樣子:

項目 內容
啟動條件 主機 B 的 .env 裡 CLOUDFLARE_TUNNEL_TOKEN 有值才啟動。沒有值時這個容器不存在,docker compose ps 也看不到它
對外的連線 往 Cloudflare 的 7844 埠,預設用 QUIC(UDP),連不上時改用 HTTP/2(TCP)。一共 4 條,分別連到至少兩個資料中心
入站埠 沒有。容器沒有發布任何埠到主機
容器位址 固定為 172.18.0.250。這是主機 B 內部容器網段的位址,只在主機 B 上存在,內網其他電腦連不到;其他容器的位址由系統分配,只有它是指定的。位址固定是有用途的,見第三節
主機 B 保管的東西 只有 token 一個值。這條 tunnel 接哪個網域、請求轉給誰,都存在 Cloudflare 的帳號裡,cloudflared 連上之後才取得
狀態端點 容器內的 20241 埠,沒有發布到主機的埠,在主機 B 上要用容器位址連
執行身分 非 root 的一般帳號
版本 固定在安裝包指定的那一版,啟動時帶 --no-autoupdate,不會自己更新

啟用的方式有兩種,都在安裝文件的「對外公開(選用)」一節。一是給安裝腳本一組 Cloudflare API token 與要用的網域名稱(--cf-api-token、--cf-hostname),腳本會建立 tunnel、設定分流規則、建立 DNS 記錄,再把取得的 tunnel token 寫進 .env。二是自己在 Cloudflare 的後台做完這三件事,只把 tunnel token 交給安裝腳本(--tunnel-token)。兩種方式的前提相同:要有一個由 Cloudflare 代管的網域。

二、它接在哪裡

https://ithelp.ithome.com.tw/upload/images/20261007/201842614MKZiqpGbY.png

圖上有三件事要看。

  • 連線的方向和請求的方向相反。連線是 cloudflared 由下往上撥的,請求卻是由上往下走。主機 B 的防火牆因此不必為 Internet 開任何埠;訪客查網域得到的是 Cloudflare 的位址,查不到主機 B 在哪裡。
  • 請求交給誰,由分流規則決定,而規則不在主機 B 上。規則存在 Cloudflare 的帳號裡,cloudflared 連上之後取得、照著做。最簡單的情況只有一條:指定的網域一律轉給 http://waf-nginx:8080,不符合的回 404。啟用歡迎頁時多一條,指向另一個 WAF 容器。兩個 WAF 之間怎麼分,完全由這份規則決定。
  • cloudflared 到 WAF 這一段不經過實體網卡。兩個容器在同一個網段裡直接通。主機 B 的防火牆對 8080 埠的來源限制只管從網卡進來的連線,管不到這一段;Suricata 抄的是網卡上的封包,也看不到這一段(搭配元件篇 1 第二節)。訪客的請求在主機 B 上第一次被檢查,就是在 WAF 手裡。

圖右邊的「內網直連」是另一條進 WAF 的路:在管理者的電腦上直接連主機 B 的 8080 埠。本系列各篇的驗證指令走的都是這一條,所以沒有 tunnel 的節點一樣做得完。兩條路在 WAF 之後完全相同,差別只在 WAF 之前,下一節說明。

三、請求經過它之後多了什麼

對 WAF 來說,經 tunnel 進來的每一個請求,連線的來源都是同一個位址,就是 cloudflared 的 172.18.0.250。真正的訪客是誰,寫在 Cloudflare 加上的幾個標頭裡。下面是從 Internet 對一台有 tunnel 的節點送一個帶 SQL 注入字串的請求之後,WAF 的 audit.log 記下的內容(只列與來源有關的部分,位址與編號已去識別化):

連線來源          172.18.0.250

Host              app.example.com
Cf-Connecting-Ip  203.0.113.80
Cf-Ipcountry      TW
X-Forwarded-For   203.0.113.80
X-Forwarded-Proto https
Cf-Visitor        {"scheme":"https"}
Cf-Ray            8f2c61d4ab12c3d4-TPE
Cdn-Loop          cloudflare; loops=1
Cf-Warp-Tag-Id    6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f
標頭 內容與用途
Cf-Connecting-Ip 訪客連到 Cloudflare 時的位址。安裝包拿它當攻擊者位址
Cf-Ipcountry Cloudflare 依訪客位址判斷的國別。WAF 事件的國別只有這一個來源,所以從內網直連產生的 WAF 事件沒有國別
X-Forwarded-For 代理鏈上的位址清單。原文存進事件當證據,不拿來判斷攻擊者是誰
Host 訪客連的網域名稱。事件的目標主機欄位取的是它
X-Forwarded-Proto、Cf-Visitor 訪客與 Cloudflare 之間用的是 HTTPS。到了主機 B 這一段已經是 HTTP
Cf-Ray Cloudflare 給這個請求的編號,回應的標頭裡也有同一個值,可以拿去對 Cloudflare 那一側的紀錄。安裝包沒有取用
Cf-Warp-Tag-Id 經手的那一個 cloudflared 的編號,和第六節狀態端點回報的 connectorId 相同

同一個請求在 ClickHouse 裡的那一列(搭配元件篇 3 第三節的欄位):

event_time:      2026-10-06 13:52:17.959
ingested_at:     2026-10-06 13:52:22.967
source_system:   coraza
severity_id:     4
actor_ip:        ::ffff:203.0.113.80
actor_country:   TW
actor_ua:        curl/8.5.0
actor_xff:       203.0.113.80
target_host:     app.example.com
target_url:      /beakplatform/?id=1%27%20UNION%20SELECT%201--
target_service:  waf-nginx
finding_rule_id: 942100

攻擊者位址是訪客的,不是 cloudflared 的;國別有值;目標主機是網域名稱。規則欄位也和上一篇的例子不同:上一篇用 IP 位址連主機 B,排第一的規則是 920350(Host 標頭是數字位址);這裡 Host 是網域名稱,那一條不會命中,記下的是 SQL 注入的 942100。

標頭是可以偽造的,所以只信一個來源
任何人都可以在請求裡自己加一行 CF-Connecting-IP。如果 WAF 這一側看到標頭就信,能連到 8080 埠的人就能把攻擊者位址填成任意第三方,讓主機 A 去封一個無辜的位址。安裝包的做法是看連線的來源:只有來源是 172.18.0.250 的請求,標頭裡的訪客位址與國別才算數;其他來源一律是誰連進來就記誰。cloudflared 的容器位址之所以固定,就是為了這個判斷。

實測:在管理者的電腦(192.168.0.50)上直接連主機 B 的 8080 埠,自己帶兩個假標頭。

# 管理者的電腦
curl -s -o /dev/null -w '%{http_code}\n' \
  -H 'CF-Connecting-IP: 203.0.113.99' -H 'CF-IPCountry: ZZ' \
  "http://192.168.0.112:8080/beakplatform/?id=1%27%20UNION%20SELECT%201--"
403
actor_ip:        ::ffff:192.168.0.50
actor_country:
actor_xff:
target_host:     192.168.0.112:8080
finding_rule_id: 920350

事件記下的是實際連進來的那台電腦,假的位址與國別都沒有被採用。這個判斷是 Vector 在轉換事件時做的,不是 WAF 做的,細節在 WAF 深入篇第 2 篇。

被保護的網站那一端也有同樣的問題:它看到的連線來源是主機 B,真正的訪客同樣要從標頭取。BeakPlatform 只在連線來自它信任的代理時才採信這個標頭。

四、它補了什麼,換來什麼

對外公開需要的東西 沒有 tunnel 時 有 tunnel 時
公網位址 要有固定位址,或自己處理位址變動 不需要。主機 B 只要能連出去
防火牆與路由器 要開入站埠、做通訊埠轉發 不必動。只有往外的連線
HTTPS 憑證 要自己申請、定期更新,WAF 也要改成聽 HTTPS 由 Cloudflare 處理。WAF 維持只聽 HTTP
主機 B 暴露的範圍 位址公開,開著的埠都會被掃 位址不公開。Internet 上的訪客只到得了分流規則指向的 WAF,碰不到主機 B 的其他埠
訪客的位址與國別 位址直接看連線來源,國別要另外查 由 Cloudflare 放在標頭裡帶進來

這些好處有三個連帶的結果,前幾篇各自提過,這裡放在一起:

  • 主機 B 的防火牆封不到走 tunnel 的訪客。封鎖決策落在主機 B 的防火牆時,擋的是從網卡進來、來源是那個位址的封包。走 tunnel 的訪客,封包的來源是 Cloudflare,而且根本不是新連線,這份封鎖清單對他沒有作用;他的請求仍然由 WAF 逐個檢查、逐個擋下。防火牆這個執行點擋得住什麼,見原系列第 5 篇第二節。
  • Suricata 平常是安靜的。tunnel 裡是密文,cloudflared 到 WAF 那一段又不經網卡,Suricata 看不到訪客的請求(搭配元件篇 1)。cloudflared 自己往 Cloudflare 的連線倒是會被 Suricata 看見,還會命中兩條「偵測到 Cloudflare Tunnel」的規則,安裝包已經把那兩條停用。
  • CrowdSec 的 SSH 偵測平常也是安靜的。訪客到不了主機 B 的 22 埠(搭配元件篇 2)。

五、它不做的事

限制 說明
不檢查內容 它只搬運,交給它的請求原樣轉給 WAF。擋不擋是 WAF 的事
不是封鎖的執行點 主機 A 的封鎖節點可以勾選 cloudflare 這個執行點,但防禦節點出廠不認領它,對應的程式也只是預留的空殼。勾了不會有任何東西落到 Cloudflare 上,也和 cloudflared 這個容器無關
Cloudflare 先擋下的,主機 B 不會知道 Cloudflare 自己也有防護。請求若在那一側就被擋下,不會進 tunnel,WAF、ClickHouse、主機 A 都不會有紀錄。主機 B 上的「全量」是到得了主機 B 的全量
加密不到主機 B 訪客的 HTTPS 在 Cloudflare 結束,Cloudflare 看得到請求與回應的明文。Cloudflare 到 cloudflared 之間由 tunnel 加密;cloudflared 到 WAF、WAF 到被保護的網站是 HTTP
分流規則不在主機 B 重裝主機 B 不會改變規則,改主機 B 的設定也改不到規則。要換網域或加路徑,得到 Cloudflare 的後台改,或用安裝包附的 cf_tunnel.py
token 就是鑰匙 拿到 token 的人可以在別的地方啟動另一個 cloudflared,接到同一條 tunnel 上,分走一部分訪客的請求。token 存在主機 B 的 .env,只有 root 讀得到(2026-10-06 之前安裝的節點例外,見第六節最後一段)。懷疑外流時,到 Cloudflare 的後台把 tunnel 刪掉重建
不會自己更新 版本跟著安裝包走。Cloudflare 出新版之後,cloudflared 會在紀錄裡寫一行提醒,但不會動作
只有一個 一個節點一個 cloudflared。它停了,對外就斷了
狀態只有「執行中」 安裝包沒有替這個容器設健康檢查,docker compose ps 顯示執行中,不代表連線是通的。要看連線,用第六節的方法

把它停掉會怎樣,實測過一次。在一台有 tunnel 的節點上停止 cloudflared 容器 21 秒:

  • 停止後 3 秒內,從 Internet 連對外網址就開始得到 HTTP 530,內文是 error code: 1033。這是 Cloudflare 回的,意思是這條 tunnel 目前沒有任何一個 cloudflared 連著。
  • 同一段時間,從內網直連主機 B 上的兩個 WAF,歡迎頁照常回 200,攻擊請求照常回 403。WAF、事件送往主機 A、封鎖決策的回程都不經過 cloudflared。
  • 重新啟動後 3 秒多,第一條連線註冊完成,對外恢復正常;四條連線全部回來,再多幾秒。不需要重設任何東西。

訪客看到錯誤時,從狀態碼可以分辨是哪一段出問題:

訪客看到的 誰回的 代表什麼
HTTP 530,error code: 1033 Cloudflare cloudflared 沒有連上。看主機 B 上這個容器
HTTP 502 WAF,經 Cloudflare 轉達 cloudflared 與 WAF 都活著,但 WAF 連不到被保護的網站。看主機 A。實測被保護的網站離線時,從內網直連 8080 埠與從 Internet 連,得到的都是 502
HTTP 403,nginx 的錯誤頁 WAF 請求被規則擋下。整條路是通的

所以它在整體裡的位置是這樣的:檢查靠 WAF,判斷靠主機 A,cloudflared 只負責把 Internet 上的請求帶到 WAF 面前。少了它,偵測、建案、處置的每一段都還在,只是進來的只剩內網的流量。

六、怎麼確認它在做事

先看它有沒有連上。cloudflared 每建立一條連線就寫一行紀錄,正常是 4 行:

# 主機 B
cd /opt/integrated-waf
sudo docker compose logs cloudflared | grep 'Registered tunnel connection' | tail -4
INF Registered tunnel connection connIndex=0 connection=… ip=198.41.192.77 location=hkg09 protocol=quic
INF Registered tunnel connection connIndex=1 connection=… ip=198.41.200.193 location=tpe01 protocol=quic
INF Registered tunnel connection connIndex=2 connection=… ip=198.41.192.37 location=hkg01 protocol=quic
INF Registered tunnel connection connIndex=3 connection=… ip=198.41.200.23 location=tpe01 protocol=quic

location 是連到的資料中心,protocol=quic 代表用的是預設的 QUIC。安裝腳本的 --verify 在「對外入口」那一段檢查的就是這一行有沒有出現過。

紀錄只說曾經連上。要知道此刻有幾條連線,問它的狀態端點:

# 主機 B
curl -s http://172.18.0.250:20241/ready
{"status":200,"readyConnections":4,"connectorId":"6f1c2d3e-4a5b-4c6d-8e7f-9a0b1c2d3e4f"}

readyConnections 是 4 就是全部連著。少一兩條不影響服務,是 0 的時候訪客看到的就是上一節的 530。

最後走一次完整的路。找一個不在內網的連線(例如手機的行動網路),對對外網址送一個一定會被擋的請求,十秒後在主機 B 查 ClickHouse 最新一筆:

# 不在內網的電腦
curl -s -o /dev/null -w '%{http_code}\n' "https://app.example.com/?id=1%27%20UNION%20SELECT%201--"
403

# 主機 B,十秒後(CH_PW 的取法見搭配元件篇 3 第五節)
sudo docker compose exec clickhouse clickhouse-client --user secstack --password "$CH_PW" -d secstack \
  --query "SELECT event_time, actor_ip, actor_country, target_host FROM events ORDER BY ingested_at DESC LIMIT 1 FORMAT Vertical"

要看三個欄位:actor_ip 是你那個連線的公網位址,不是 172.18.0.250;actor_country 有值;target_host 是網域名稱。三個都對,就代表 Cloudflare、cloudflared、WAF、Vector 這一段,連同第三節的來源判斷,都是通的。這一筆的來源是公網位址,不屬於「內網打內網」,會照搭配元件篇 3 第四節的規則送往主機 A。

看紀錄時會遇到兩種不必處理的訊息。一是啟動時有一行 failed to sufficiently increase receive buffer size,說的是 UDP 緩衝區沒有調到它想要的大小,不影響運作。二是運作久了會零星出現 ERR 與 WRN,內容是某一條連線逾時、中斷;只要後面跟著同一個 connIndex 的 Registered tunnel connection,就是它自己重新連上了。四條連線輪流斷線重連是常態,訪客不會察覺。

在 2026-10-06 之前安裝的節點有一件事要處理。舊版安裝包把 token 當成命令列參數交給 cloudflared,而命令列參數會出現在主機的行程列表裡,主機 B 上任何帳號用 ps 都讀得到,不需要 root。新版改由環境變數傳入,行程列表裡不再有 token。更新方式是執行一次 sudo bash /opt/integrated-waf/install.sh --update,cloudflared 會重建,對外中斷幾秒。主機 B 上若有過其他人的帳號,更新之後再到 Cloudflare 的後台換一條 tunnel。


上一篇
ClickHouse:留在主機 B 的那一份全量
系列文
一鍵完成六套開源防禦系統整合 共 23 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言