iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
AI Engineering

AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄系列 第 23

Day 23|看得見也有邊界:AI 的 HTTPS 流量憑什麼讓我看

  • 分享至 

  • xImage
  •  

Day 23:AI 的 HTTPS 流量,憑什麼讓我看:側錄的三條路

簡短回顧

昨天我透過 mitmproxy 看見了 Claude Code 與模型 API 交換的內容,也在其中看見 Connector tool call 的紀錄。畫面很完整,卻很容易讓人產生一個錯覺:只要架起代理,HTTPS 裡的東西就都看得見。

其實不是。昨天看得到,不是因為 HTTPS 失效了,而是因為我控制了執行環境,也改變了 client 願意信任誰。若換成不接受代理憑證的程式、使用 certificate pinning 的 client,或根本不受我控制的 runtime,同一套方法可能立刻失效。

所以今天不是要補一堂 TLS 課,而是要回答 AI 治理中更實際的一題:當 agent 的操作都包在 HTTPS 裡,我手上的觀測方法究竟能交付什麼證據,又有哪些地方其實看不見?

「這樣不行吧?」

我在一場內部分享上問過大家一個問題:如果有人把你的流量整個錄下來,他看得到你在裡面傳了什麼嗎?

現場的答案是不行。有人講得很篤定:那是 HTTPS,加密的。

標準答案確實是不行,但世界就不是這樣運作的⋯⋯ 😅

先講標準答案為什麼是不行。HTTPS 保護的是傳輸內容,中間的人直接拿到的是一串亂碼;若是直接連線,中間節點通常仍看得到目的 IP,未使用 ECH 時也可能看得到 SNI。這些 metadata 有時足以推測目的服務,但共享 IP、CDN、VPN、加密 DNS 與 ECH 都會降低判斷精度。單純做行為管控時,它可以是一條線索,卻不能當成完整的內容證據。

要看到內容,得再往下一層。

憑證是一條鏈

你連上一個網站,它拿一張憑證給你,瀏覽器要判斷這張憑證可不可信。

判斷的方式是往上追。這張憑證由某個中繼憑證簽發,中繼憑證再一路連到 client 採用的信任庫裡某個根憑證。不同瀏覽器、runtime 與作業系統採用的信任庫不一定相同;能建立到受信任根憑證的完整鏈,才會通過這一關。

所以在預設信任設定下,自己簽一張憑證掛在網站上,瀏覽器通常會跳警告。原因不是它加密得比較弱,而是 client 的信任庫裡沒有能替它背書的根;若使用者或管理者另外安裝並信任那張 CA,結果就會不同。

這條鏈中間斷一節,下面全部跟著垮。2024 年 6 月,Chrome 宣布調整對 Entrust 的信任:自 2024 年 11 月 12 日起,符合公告條件的新憑證不再被 Chrome 預設信任,原因是長期未能符合憑證政策要求。這不是某個網站的私鑰突然被破解,而是上游信任政策改變,下游站點就得跟著換鏈。

單張憑證自己不合格也一樣會被擋。有幾個公開的測試網頁可以直接看:憑證過期的、憑證上寫的名字跟實際網域對不上的,還有一個我在現場問倒滿多人的「萬用字元」。

*.example.com 這張憑證,有沒有涵蓋 api.v2.example.com

答案是無法涵蓋。萬用字元只對得到一層。網址是用點分隔的,那個星號只能替代一段,中間多一層就不符合。若要涵蓋 api.v2.example.com,得把相應名稱列入 SAN、為該層級配置另一張萬用字元憑證,或使用另一張精確名稱的憑證;不存在一張能遞迴涵蓋任意深度的「所有層」萬用字元憑證。

這件事說明的是:憑證的驗證是逐段比對,不是「差不多像」。

但作業系統讓你自己加一個

上面那套的前提是「清單是別人決定的」。

作業系統這一層很有意思。Windows、macOS 與 Linux 都提供加入自訂信任錨點的機制;Android 也有使用者安裝的 CA,但 Android 7.0 起,target API 24 以上的 App 預設不會自動信任使用者新增的 CA,除非 App 明確選擇採用。能不能加入是一回事,某個程式最後讀哪一份信任庫則是另一回事。

塞進去之後,那台機器上確實採用這份系統信任庫的程式,才會相信你自己簽的憑證。這時就有機會坐在中間,把那些經過代理、沒有另外做 pinning 的 TLS 流量解開來看,再重新加密送出去;實際能看到的範圍仍取決於程式採用的信任設定與流量有沒有經過代理。

這不是漏洞,是設計。企業要做流量檢查、要在出口攔惡意連線,靠的就是這個能力:MDM 把根憑證推到每一台公司裝置上,出口擺一台解密再重新加密的設備。用比較白的講法,那是有正規管道、有管理規範的中間人。

作業系統也知道這件事需要被說出來。部分 Android 版本與裝置在安裝使用者 CA 後,會以「網路可能受監控」之類的提示告知使用者;實際字樣與呈現方式會隨版本及廠牌而異。

不過這件事要動手,前提是你能控制那台裝置或程序的信任設定;是否需要管理員權限,取決於平台與修改的範圍。技術上做得到也不等於取得授權,我這裡是在自己管理的容器裡做受控實驗。ARP Poisoning 則是在沒有合法觀測位置時偽造網路鄰居資訊,試著把別人的流量引到自己這裡,是另一種情境。

ARP Poisoning 是什麼?

同一個區域網路裡,實際送封包要用的是 MAC 位址,不是 IP。所以電腦送之前得先問一句:「這個 IP 的 MAC 位址是多少?」這個問答就是 ARP。

問題出在傳統 ARP 本身沒有驗證回覆來源的機制,主機也可能依收到的回覆或公告更新快取。攻擊者若與目標在同一個區網,便可能反覆送出偽造對應,宣稱「閘道器那個 IP 對應到我的 MAC 位址」,把部分流量引到自己這裡,再轉送出去。

受害者完全無感,因為網路照樣通、網頁照樣開。它搶的不是內容,是路徑。

這也是為什麼加密會變成必要的:你沒有辦法保證封包走的路是乾淨的,那就只能假設它不乾淨,然後讓走在上面的東西自己有保護。

這正是 HTTPS 要處理的核心威脅之一:沒有人能保證傳輸路徑永遠乾淨,因此內容本身必須有保護。

三種信任,我選了範圍最小的那個

回頭看我昨天做的事,其實可以分成三個層級:

作業系統的信任庫。 塞進去,採用這份信任庫的程式都可能受到影響。相較於只改單一程序,它的範圍更大、留存時間也更長。

執行環境採用的信任設定。 我昨天那條 Node/Claude Code 執行路徑沒有直接採用系統信任庫,所以我用 NODE_EXTRA_CA_CERTS 只替該程序補上一張 CA,程序關掉後這個設定也隨之消失。這不是所有 Node 版本與設定的通則;新版 Node 也提供 --use-system-ca 等選項。

程式另外做 pinning。 它可能固定信任特定憑證、公鑰或 SPKI hash,而不只依賴一般的憑證鏈驗證;實作也可能保留備援 pin,避免換證時把自己鎖死。

我用的是中間那層,而且不是因為方便。動系統信任庫是一個會留在機器上的改動,而我要的只是讓一個程序在一段時間內信任一張憑證。容器裡那張憑證更是每一場現產一把,容器收掉就沒了。真出事的話,範圍就是那一個容器。這個選擇落在 dev-container/entrypoint.sh 的第 179 到 181 行,註解寫的就是上面那個理由;同一支腳本再往下十行,才是全錄模式那條「連我沒預料到的客戶端也要能被錄到」的路。

它其實可以不讓我看

所以我錄得到,是因為 Claude Code 收 HTTPS_PROXY,也收 NODE_EXTRA_CA_CERTS

如果它另外做了 pinning,我上面這條替換伺服器憑證的代理路徑就不會通。只把憑證塞進系統信任庫或透過環境變數補 CA,都不足以讓它接受代理簽出的憑證。

它目前沒有用 pinning 擋住這條路。這只能證明在我測試的版本與受控環境中,這條觀測路徑技術上可行;不等於原廠授權任何人側錄任意資料。

Pinning 也不是萬能。在使用者完全控制、而且具有足夠權限的裝置上,仍可能有人修改程式檔或執行時行為;能否做到則取決於平台、簽章驗證與防護機制。Pinning 買到的是更高的繞過成本,而不是數學上的不可能;這同樣只是在描述技術阻力,不代表繞過行為取得授權。

還有一條不必改變信任的路:匯出連線金鑰

前面那些都是在講「怎麼坐到中間」。但要看到內容,其實有三種完全不同的路。

第一種是封包側錄。 tcpdump 這類工具可以在指定介面擷取它有權限看見、而且沒有被 capture filter 排除的封包,不只 TCP,也可能包含 UDP、ICMP、ARP。它不需要改變 TLS 信任,但仍受介面、權限與網路位置限制;即使抓得到封包,TLS 內容攤開來仍是亂碼,這也正是我一開始不能只靠它的原因。

第二種就是我做的中間人。 看得到內容,代價是我真的坐在中間:對方要信任我的憑證,而那條連線的另一端其實是我。

第三種我一直沒提:把金鑰要出來。

TLS 連線會衍生出這一場傳輸所需的 traffic secrets,而有些程式為了除錯,願意把這些 secrets 寫進 key log。Node 有 --tls-keylog;我這次使用的 curl 採 OpenSSL backend,設定 SSLKEYLOGFILE 後也確實產生了檔案。不同平台或不同 curl build 未必支援,所以執行前得先看 curl -V,再確認 key log 真的不是空檔。拿到這個檔案,就可以事後把同一場側錄下來的封包解開。

這條路的性質跟前面兩種都不一樣。我不在中間。 連線仍然是端到端加密的,憑證鏈一個 byte 都沒有被動過,中間也沒有多一台機器。我看得到,純粹是因為那個程式在我的機器上跑,而它願意把鑰匙給我。

我在一個一次性的 Docker 容器裡實際跑了一遍:一邊用 tcpdump 錄封包,一邊用 curlhttps://api.anthropic.com/v1/messages 送出不含 token 的 GET,並加上自訂標頭 x-demo-header: nathan-keylog-test,然後看三件事。

封包裡沒有明文。 這次的 capture.pcap 是 6,173 bytes;直接搜尋 nathan-keylog-test/v1/messages 都是零筆。這個大小只是本次結果,不是固定值。

但看得到這次連去哪。 這次 TLS 交握的 ClientHello 裡,SNI 仍是明文,api.anthropic.com 可以直接讀到。前面說 HTTPS 保護的是傳輸內容、擋不住目的地這件事,在這次封包上就長這樣。

SNI 是交握時 client 說明「我要連哪個網域」的欄位。在這次實測中,它仍以明文出現在 ClientHello。不過這不是永遠成立的通則;較新的 ECH(Encrypted Client Hello) 會加密包含真正 SNI 的 ClientHello,因此不能再把 SNI 視為必然可見。即使使用 ECH,IP 或 DNS 等其他線索仍可能暴露連線目的地。

然後把金鑰餵給解析工具。以 tshark 為例,可以用 -o tls.keylog_file:/tmp/tls.keys 指定 curl 透過 SSLKEYLOGFILE 寫出的 key log;若用 Wireshark,則在 Preferences → Protocols → TLS 的 (Pre)-Master-Secret log filename 選取同一個檔案。這次的 tls.keys 是 938 bytes;不給金鑰時,解析工具找到 0 個 HTTP/2 訊框,給了之後則解出 4 個,請求與回應都出來了:

GET  /v1/messages  api.anthropic.com     → 405
Header: x-demo-header: nathan-keylog-test

請求方法、路徑、目標網域、回應狀態、連我自己加的那個標頭,一個不漏。而我從頭到尾沒有動任何憑證,也沒有任何東西坐在中間。

三種放在一起看,差別很清楚:

動了什麼 看得到內容嗎
封包側錄 什麼都沒動 不行,是亂碼
中間人 動的是信任(讓它信一張我簽的憑證) 可以
要金鑰 取得 client 匯出的 TLS secrets 可以

Pinning 防的是替換伺服器憑證的中間人;如果 client 本身願意匯出這場連線的 TLS secrets,pinning 不會阻止我們拿這些 secrets 解密封包。但這不是保證:不是每個程式或 TLS library 都會提供這個能力。這也不是繞過 pinning,而是走了不同的觀察點:前者改變 server trust,後者取得 client 自己使用的 traffic secrets。

所以「它其實可以不讓我看」這句話有個底:程式在自己掌控的環境裡跑,而且願意提供 key log 或其他觀測介面時,加密不一定是觀測的終點。 真正把使用者擋在外面的,是讓程式根本不在使用者手上跑,也就是把它放到伺服器端。而那正是後面「還有一層是你連錄都錄不到的」要講的。

反過來想

我把這件事做通之後,在內部演示給同仁看。我當時想講的不是「你看我錄得到」。

我們自己也在交付軟體。今天一套系統送到使用者手上,就得假設有人會觀察它在本機留下的程式與流量,研究傳了什麼、怎麼傳、帶了什麼標頭。前端或客戶端的檢查通常也可能被觀察、修改或繞過。

所以客戶端驗證可以改善體驗,也能提高濫用成本,卻不能成為唯一、具權威性的安全邊界。真正影響權限與資料完整性的決策,伺服器仍得再驗一次。這句話大家都會講,但親眼看過自己的流量被攤開來,跟聽過這句話是兩件事。

還有一層是你連錄都錄不到的

Day 20 結尾那件補不起來的缺口,今天用兩個獨立的儀器量出來了。

有意思的是,我把整條路都錄下來之後,發現有些東西根本不在裡面。

側錄裡,我的容器只跟 api.anthropic.com 講過話。但搜尋工具中間確實查了外面的東西,也真的把結果帶了回來。

工具出現在同一個介面裡,不代表它們在同一個位置執行。這次的證據顯示至少有三條不同路:搜尋由原廠後端取得結果;我測的網頁抓取會嘗試從本機連線;前面測過的 Google Drive Connector 則透過 Anthropic API 這條已允許的路徑呼叫帳號授權的服務,不是容器直接連到 docs.google.com。我的防火牆管得到「我的機器直接連出去」,卻管不到原廠後端替帳號執行的工具。

這件事可以直接量。我在限制模式下開著錄製跑了一次,只做兩件事:一次網頁搜尋、一次抓 https://example.com/

防火牆這一層的答案:搜尋成功,抓網頁失敗,錯誤訊息是 Socket is closed,試兩次都一樣。限制模式把一般 TCP/HTTP(S) 的出站收斂到 api.anthropic.com,搜尋通得過就代表那不是我的機器用 HTTP(S) 連出去的;抓網頁被擋死,代表那條是在本機執行。

流量這一層的答案:這次錄製共有七次 API 請求,我再看目標內容第一次出現在哪個方向。

第一次出現
伺服器端的工具呼叫紀錄 第 4 次,下行
搜尋結果的實際內容 第 4 次,下行
Socket is closed 第 6 次,上行

搜尋指令當然仍由我的機器往上送;真正沒有從本機上傳的是搜尋結果內容本身。它第一次出現就在伺服器回傳的第 4 次下行。抓網頁的失敗訊息則是我的機器往上回報:本機連不出去,再把結果告訴模型。

兩個獨立的儀器,同一個結論。

⚠ 結論的範圍要講準:這兩把尺量的都是 HTTP(S)(一把是 iptables 的 TCP 白名單,一把是 L7 側錄)。它們證得出「搜尋的內容沒有經過我這台機器的 HTTP(S) 路徑」,證不出「我這台機器沒有任何其他形式的出站」,因為 DNS 就不在這兩把尺的量程內(Day 20 文末那一節),走 proxy 的側錄也只涵蓋那一場 CLI 及其子行程。能下的結論是前者,那也正好夠回答今天這個問題。

這一層沒有辦法只靠本機防火牆補起來。api.anthropic.com 一定要開,不開就沒有 agent;只要那條路開著,伺服器端工具的能力就不在這道本機防火牆的量程內。相應的控制點得放在帳號、Connector 與工具權限設定裡。

七天下來,一張圖

Day 17 到今天,dev-container 這條線走完了。四道邊界各自出現過之後,可以把它們放回同一張座標裡:

dev-container 的拓撲與邊界:HOST 那一層的 wrapper 與掛載、session 容器裡 entrypoint 的六個步驟、右側 init-firewall 的規則順序、底下的三個對外端點與一格 REJECT

左半邊由上往下讀就是 entrypoint 的順序:憑證政策(值不進環境變數)→ 三道選單(人答,在 agent 起跑之前)→ 防火牆 → 側錄 → session id → 交棒給 CLI。右半邊是那道牆的規則順序,Day 20 的 Docker DNS NATREJECT/DROP 兩個坑都在上面:先撈出 NAT 規則再 flush(1 到 3),最後則使用 REJECT 而不是 DROP(7 到 8)。第 9 步自我驗證橫在底下那一條,因為它同時驗兩個方向。

我真正想指的是圖裡那幾行紅字。它們不是註解,是缺口:DNS 是通的(白名單本身要靠網域解析才成立)、直連網段那一條是全協定全埠而且排在 REJECT 之前、standalone 的那顆代理只管 API 而 HTTPS clone 沒有走它。一張只畫得出防線的架構圖,通常是還沒有被自己驗過的那一張。

本日小結

Day 22 攤開的是一次實際的 AI 流量;今天校正的是我對這份證據的信心。代理側錄、client 匯出的 TLS traffic secrets 與封包側錄,不是三種誰比較厲害的工具,而是三種不同的信任條件。真正重要的不是「我裝了哪套軟體」,而是報告能不能老實交代:這次看見了什麼、靠什麼看見,以及哪些部分仍在視野之外。

Day 17 到這裡,我一路把憑證、token、SSH、網路、trace 與 HTTP flow 變成可以檢查的邊界。最後得到的不是一個全知視角,而是一份有量程的證據:牆立在我的機器周圍,不是立在我的資料周圍;看不見的地方要明白揭露,再用伺服器紀錄、遙測或帳號權限補上,不能假裝它不存在。

下一個問題是,前面這二十幾天建起來的整套環境,目前只活在我這台筆電上。換一台裝置就要從頭裝一遍,而我想要的是打開瀏覽器,就能接回同一個受控環境。


上一篇
Day 22|一天 800 MB:用 mitmproxy 攤開 Claude Code 的流量
下一篇
Day 24|把 Claude Code 搬上瀏覽器:一場 Session 一顆容器
系列文
AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言