CKH 網誌

Ubuntu 24.04 Cloudflare Tunnel+Caddy:對外測試網站與真實 IP 驗證

實測環境與硬體規格

項目實際測試配置
測試日期2026-10-02
作業系統Ubuntu 24.04.5 LTS
主機數量1 台新建隔離 VM;下列資源為每台配置
CPU2 vCPU
記憶體4 GiB
系統磁碟32 GiB;HDD 儲存、VirtIO、ext4
Linux 核心6.8.0-139-generic
Caddy2.6.2
cloudflaredcloudflared version 2026.9.3 (built 2026-09-24-16:07 UTC)

用暫時 Cloudflare Tunnel 接到本機 Caddy,再轉發到 loopback 測試網頁;實測外部 HTTPS、真實來源 IP 與偽造標頭拒絕,完成後關閉入口。

本文以全新 Ubuntu 24.04 隔離主機和合成資料實測。套件來自經過簽章驗證、符合 Noble 的套件來源;沒有使用正式資料庫或正式網站的帳號。讀者請依自己的環境確認來源與版本。

圖中是測試主機實際 bash PTY 的執行紀錄,由瀏覽器呈現;提示符統一為 lab$,執行結果未改寫。文字命令另行保留,方便複製。

一、此次實測架構

外部瀏覽器
  → Cloudflare HTTPS 邊緣
  → cloudflared 建立的暫時 Tunnel
  → Caddy 127.0.0.1:8080
  → Python 範例網站 127.0.0.1:8081

這次三個程式在同一台新建隔離 VM,以 loopback listener 避免內網其他主機直接偽造來源標頭。對外只回傳「連線成功」與通用教學文字,不提供管理登入、檔案瀏覽或來源 IP 查詢。公開入口在完成測試後已關閉。

本篇驗證的是免帳號、免網域設定的 Quick Tunnel。它供測試使用,沒有生產可用性承諾,且有並行請求與 SSE 等限制;正式服務應用具名 Tunnel 與自己的網域。Quick Tunnel 文件

二、安裝受信任的原生套件

sudo apt update
sudo apt install caddy python3 curl ca-certificates gnupg

# 一般 Ubuntu 24.04 使用官方 Cloudflare Noble 來源的寫法
sudo install -d -m 0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg \
  | sudo gpg --dearmor --yes -o /usr/share/keyrings/cloudflare-main.gpg
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared noble main' \
  | sudo tee /etc/apt/sources.list.d/cloudflared.list >/dev/null
sudo apt update
sudo apt install cloudflared
caddy version
cloudflared --version

本次受控環境由鏡像主機驗證 Cloudflare 目前有效簽章,再核對 Packages 與 deb 的完整 SHA-256 鏈,以自己的簽章發布給測試 VM。一般官方來源寫法如上;有套件鏡像政策的環境應沿用已核准的來源,不另行繞過。沒有下載或使用正式網站的 Tunnel token。官方套件來源

三、建立最小、只綁 loopback 的後端

以下範例示意與實測相同的後端處理方式。它把來源欄位存到私有檔案,對外完全不回傳這些值;目錄交給專用非登入帳號,日誌權限 0600。

sudo useradd --system --home /var/lib/demo-tunnel --shell /usr/sbin/nologin demo_tunnel
sudo install -d -o demo_tunnel -g demo_tunnel -m 0700 /var/lib/demo-tunnel
sudo tee /usr/local/lib/demo-tunnel-backend.py >/dev/null <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
import ipaddress, json, os, pathlib
class Handler(BaseHTTPRequestHandler):
    def log_message(self,*args):
        pass
    def do_GET(self):
        client=self.headers.get('CF-Connecting-IP','')
        try:
            ipaddress.ip_address(client)
        except ValueError:
            self.send_error(403)
            return
        event={'path':self.path,'client':client,
               'forwarded':self.headers.get('X-Forwarded-For',''),
               'transport':self.client_address[0]}
        log=pathlib.Path('/var/lib/demo-tunnel/events.jsonl')
        with log.open('a') as f:
            f.write(json.dumps(event)+'\n')
        os.chmod(log,0o600)
        body='<h1>連線成功</h1><p>Temporary isolated tutorial service</p>'.encode()
        self.send_response(200)
        self.send_header('Content-Type','text/html; charset=utf-8')
        self.send_header('Content-Length',str(len(body)))
        self.end_headers()
        self.wfile.write(body)
HTTPServer(('127.0.0.1',8081),Handler).serve_forever()
PY
# /etc/systemd/system/demo-tunnel-backend.service
[Unit]
After=network.target
[Service]
User=demo_tunnel
Group=demo_tunnel
ExecStart=/usr/bin/python3 /usr/local/lib/demo-tunnel-backend.py
NoNewPrivileges=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/demo-tunnel
[Install]
WantedBy=multi-user.target

四、設定 Caddy:listener 和 Host matcher 是兩件事

# /etc/caddy/Caddyfile
:8080 {
    bind 127.0.0.1
    @edge header CF-Connecting-IP *
    handle @edge {
        reverse_proxy 127.0.0.1:8081 {
            header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
        }
    }
    handle {
        respond "Missing trusted edge header" 403
    }
}

實測找出一個容易忽略的問題:若站點寫成 http://127.0.0.1:8080,還會產生 127.0.0.1 的 Host matcher;Quick Tunnel 帶著暫時公開網域的 Host 進來時,可能不符合路由而得到空白回應。這裡以 :8080 接收該 listener 的 Host,再用 bind 127.0.0.1 限定網路綁定。不是開放 LAN listener。

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl daemon-reload
sudo systemctl enable --now demo-tunnel-backend caddy
sudo systemctl restart caddy
curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1:8080/

直接呼叫 Caddy 且沒有可信邊緣標頭時,預期是 403。此模型信任同機的 cloudflared 與管理者;不適合讓不受信任的本機工作共享 listener。若改成跨主機代理,必須另外限制來源及設定可信 proxy,不能原封不動相信任意傳入的 CF-Connecting-IP。

五、啟動暫時 Tunnel,再由外部 HTTPS 連入

cloudflared --no-autoupdate tunnel --url http://127.0.0.1:8080

cloudflared 會顯示本次產生的 HTTPS 入口。用外部瀏覽器打開後應看到「連線成功」。本篇不公開實際暫時入口;完成測試後停止程序,該入口即不再提供這個 origin。實測用 systemd 管理相同程序;若改用下面的 service,先停止互動程序。

# /etc/systemd/system/demo-cloudflared.service
[Unit]
After=network-online.target caddy.service demo-tunnel-backend.service
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/cloudflared --no-autoupdate tunnel --url http://127.0.0.1:8080
Restart=no
StandardOutput=append:/var/log/demo-tunnel/cloudflared.log
StandardError=append:/var/log/demo-tunnel/cloudflared.log
[Install]
WantedBy=multi-user.target
command -v cloudflared
sudo install -d -m 0700 /var/log/demo-tunnel
sudo install -m 0600 /dev/null /var/log/demo-tunnel/cloudflared.log
sudo systemctl daemon-reload
sudo systemctl start demo-cloudflared
systemctl is-active caddy demo-cloudflared demo-tunnel-backend

本次官方 deb 的執行檔位於 /usr/local/bin/cloudflared;以 command -v 核對你的實際路徑再設定 ExecStart。此 unit 是短期教學配置,沒有自動重試,也沒有啟用開機對外入口。日誌權限 0600,僅私有管理使用。

圖1:實際 Caddy、cloudflared 版本與服務狀態
圖1:實際 Caddy、cloudflared 版本與服務狀態。實際 PTY 紀錄,點圖可開啟原尺寸。

六、驗證真正的使用者 IP,而不公開 IP

後端的 TCP transport peer 是 127.0.0.1,不能把它當成使用者 IP;正確來源要從可信 Tunnel 路徑帶入的 CF-Connecting-IP 取得。本次檢查器先在呼叫端觀察自己的 IPv4 出口,再向暫時 HTTPS 入口提出不同的測試請求,以私有日誌核對下列條件:

條件實測結果
CF-Connecting-IP 等於呼叫端出口 IP通過
X-Forwarded-For 等於同一個使用者 IP通過
後端 transport peer 是 loopback通過
單獨偽造 X-Forwarded-For覆寫後仍是真正使用者 IP
偽造保留的 CF-Connecting-IP 標頭入口回傳 403,後端沒有該請求
缺少邊緣標頭的直接 Caddy 請求403 拒絕
圖2:外部 HTTPS 與真實來源 IP 比對;實際位址全部抑制
圖2:外部 HTTPS 與真實來源 IP 比對;實際位址全部抑制。實際 PTY 紀錄,點圖可開啟原尺寸。

截圖只呈現比較是否一致,不呈現真正 IP、入口或原始日誌。檢查器確實送出三組外部請求,包含單獨偽造 XFF 與偽造保留 CF 標頭。前者核對真實 IP,後者核對 403 與後端沒有請求,才列出 PASS;畫面不是人工填入的預期結果。

七、正式自訂網域怎麼辦

可以使用自己在 Cloudflare 管理的網域,但 Quick Tunnel 的暫時名稱不能直接改成自訂名稱。正式入口要建立具名 Tunnel、設定 Public Hostname/DNS 路由,再用最小權限保存 connector 憑證。這次沒有建立新正式網域、沒有公開 Tunnel UUID 或 token,也沒有修改既有網站的 Tunnel。Cloudflare Tunnel 建置文件

八、完成測試就關閉暫時入口

sudo systemctl stop demo-cloudflared demo-tunnel-backend caddy
systemctl is-active demo-cloudflared

互動啟動者使用 Ctrl+C 停止 cloudflared;以本篇測試 service 執行者使用上述停止方式。實測已確認程序停止,重新向暫時入口的全新路徑請求也不再取得原本的「連線成功」。保留私有測試記錄在隔離主機,不放到 WordPress 媒體庫。

這次驗證了什麼

驗證項目結果
外部 HTTPS 確實取得範例頁面通過
Caddy 與後端只綁 loopback通過
來源 IP 與呼叫端出口 IP 相同通過
外部偽造來源標頭未被採信通過
缺少可信標頭的直接請求 403通過
停止後暫時入口不再提供原頁面通過

實測範圍:實測 Quick Tunnel、同機 loopback Caddy/後端、真實 IP 相等與偽造標頭,以及入口關閉。未宣稱驗證新具名 Tunnel、自訂網域建置、跨主機 proxy 信任或正式容量/SLA。

官方參考資料

Quick Tunnel、官方套件來源、Caddy reverse_proxy