# LiveMR · 內部參考文件 ## 先搞懂這些字,再看那份弱掃報告 這份手冊整理這幾輪討論「自簽憑證弱掃 finding」時用到的名詞,用白話解釋,並對照 LiveMR 專案裡實際的程式碼位置——不是通用資安教科書,是照著這個專案的實際做法寫的。 **適合對象**:不是資安背景、但需要看懂憑證/CA/CORS 這類詞在做什麼的人。 --- ## 01 HTTP 與加密傳輸 先分清楚「有沒有加密」這件事的幾個名字,它們其實講的是同一層東西的不同世代。 ### HTTP / HTTPS HTTP 是瀏覽器跟伺服器溝通的協定,內容用明文傳送,區網上只要有人在監聽封包就能看到。HTTPS 是「HTTP + 一層加密」,多的這層加密就是 TLS。 ### SSL / TLS (同一件事的舊名字 / 新名字) SSL(Secure Sockets Layer)是最早的名字,早就被 TLS(Transport Layer Security)取代,但業界口語還是常常講「SSL 憑證」「SSL 連線」,指的其實是 TLS。弱掃報告寫「弱 SSL/TLS 憑證」,也是這個混用習慣。 * **在這個專案裡**:`standalone.ts` 起 HTTPS server 時明確寫 `minVersion: 'TLSv1.2'`,並指定一份只含前向保密(ECDHE)AEAD 套件的 `ciphers` 清單,拒絕舊版 TLS 與已知偏弱的加密套件——這是弱掃「弱加密套件」那條 finding 的修正。 --- ## 02 憑證本身 憑證可以想成「一份寫明身分、由某方蓋章保證的文件」——瀏覽器要確認「我正在連的這個 IP/網域,公鑰真的是它的」,才敢建立加密連線。 ### 憑證 Certificate 一份包含這些欄位的文件:Subject(這張憑證是簽給誰的)、Issuer(誰簽發的)、有效期、公鑰,還有像 SAN(Subject Alternative Name,額外綁定的網域或 IP 清單)這類擴充欄位。瀏覽器連線時會核對「網址列上的位址」跟「憑證上的 Subject/SAN」是否一致。 * **在這個專案裡**:`ensureCert()`(`certs.ts`)產生的憑證,Subject 跟 SAN 綁的都是 `detectLanIp()` 偵測到的區網 IP,IP 一變就要重簽,不然瀏覽器會說「憑證跟你連的位址對不起來」。 ### 公鑰/私鑰、cert.pem / key.pem 憑證機制靠一組數學上配對的「公鑰、私鑰」運作。`cert.pem`(含公鑰)可以公開給任何連線的人看;`key.pem` 是私鑰,只能留在伺服器上——外流的話,任何人都能假冒你的伺服器。 ### 憑證鏈 Chain / Root / Leaf 一條鏈至少兩張:最底層是 Leaf(實際給伺服器用、綁 IP 或網域的那張),最上層是 Root(CA 自己的憑證)。驗證時要沿著鏈往上核對簽名,直到碰到一張「驗證端本來就信任」的憑證才算過。伺服器在交握時該把 Leaf 之後的整條鏈一起送出,不然對方會因為「找不到簽發者」而失敗。 * **在這個專案裡**:`standalone.ts` 的 `https.createServer` 把 `cert.pem` 跟 `ca-cert.pem` 串在一起當作 cert 選項送出,讓交握時鏈是完整的,這是這次順手補上的修正。 --- ## 03 CA 與信任模型 這一段是原本弱掃 finding 的核心:憑證是「誰簽的」,決定了陌生裝置連上來時要不要跳警告。 ### CA 憑證機構 (Certificate Authority) 負責「蓋章」保證憑證真實性的第三方。依信任範圍分三種: | 類型 | Issuer 跟 Subject | 陌生裝置連線的結果 | | :--- | :--- | :--- | | **公開受信任 CA**<br>(Let's Encrypt、DigiCert…) | 不同(CA 簽) | **不跳警告**<br>系統/瀏覽器內建信任清單已收錄 | | **內部 / 私有 CA**<br>(這次採用的方案) | 不同(CA 簽) | **跳警告,除非裝過**<br>沒人幫你把根憑證塞進別人的信任清單 | | **自簽憑證**<br>(Self-signed,原本的做法) | 相同(自己簽自己) | **跳警告**,且被弱掃判定為弱憑證 | 弱掃報告原文寫「未由受信任的公開憑證機構或公司內部 CA 簽發」——把內部 CA 跟公開 CA 並列為合格解法,這代表這條檢查看的是「Issuer 是否等於 Subject」這個結構性問題,不是要求連驗證機也要信任這張 CA。 --- ## 04 這個專案怎麼串起來 `launcher` 每次啟動實際發生的順序,對照 `backend/src/standalone.ts` 與 `backend/src/launcher/certs.ts`。 1. **偵測區網 IP** `detectLanIp()` 抓這台機器目前的區網 IP(Wi-Fi/網路線換了、DHCP 重配都可能改變)。 2. **確保有本機 CA** `ensureCa()` 檢查有沒有一份效期 10 年的本機 CA;沒有就產生一份,key/cert 都只留在伺服器的 `data/certs/` 資料夾。 3. **簽發綁定該 IP 的憑證** `ensureCert()` 用這張 CA 簽一張 Leaf 憑證:Issuer 是 CA、Subject/SAN 是偵測到的 IP、sha256 簽章。IP 若跟上次不同就重簽,CA 不用重建。 4. **啟動 HTTPS server** `https.createServer()` 拿 Leaf+CA 憑證與私鑰起服務,只允許 TLS 1.2 以上與前向保密加密套件。 5. **瀏覽器才願意開鏡頭** 使用者連上 `https://<IP>`;因為是「安全來源」(HTTPS),瀏覽器才允許 `getUserMedia`/`getDisplayMedia` 執行,鏡頭/螢幕分享等核心功能才能動——這其實是整條鏈路存在的根本原因,弱掃合規只是附帶結果。 --- ## 05 網域、DNS、Cloudflare 這幾個詞跟「區網 IP 直連」是不同的定址方式,專案文件裡有另外提到的部署路線會用到。 ### 網域名稱 Domain 人類好記的名字(如 `example.com`),透過 DNS 換算成一台機器的 IP。公開 CA 幾乎只簽發給網域,不簽單純的 IP 位址——這是這次選擇內部 CA、而不是換公開 CA 的關鍵原因之一:這個 launcher 走的是「動態偵測區網 IP」,沒有固定網域可以申請。 ### DNS / A 紀錄 把網域名稱換算成 IP 位址的系統。`docs/deployment-gcp.md` 裡教的雲端部署路線,就是在網域註冊商後台加一筆 A 紀錄,讓網域指向雲端主機固定的 IP——那是另一種部署方式,跟這次討論的「區網 launcher、動態 IP」是兩條不同路線。 ### Cloudflare 在這個專案裡出現在兩種不同角色,容易搞混: * **角色一:網域註冊商 / DNS 服務商** `docs/deployment-gcp.md` 提到可以在 Cloudflare 買網域、設定 DNS A 紀錄,功能上跟 GoDaddy 是同一類服務,只是剛好也是 Cloudflare 的產品線。 * **角色二:Cloudflare Tunnel** `.env.example` 裡提到的另一個東西,是把一台「跑在區網裡、外部連不到」的伺服器,透過 Cloudflare 建一條通道,變成一個外部也能連的網址(`*.trycloudflare.com`),不用自己開防火牆 port。這條路線通常會拿到一張被公開信任的憑證,不會跳自簽警告——但前提是連線會經過 Cloudflare 的伺服器中轉,跟目前「完全留在區網內」的設計方向不同,是另一種取捨。 --- ## 06 瀏覽器安全機制與弱點掃描 除了憑證,弱掃報告裡常見的另外幾類 finding,對照 `backend/src/launcher/security.ts` 跟 `standalone.ts` 的實際設定。 ### 弱點掃描 (Vulnerability Scan,如 WebInspect) 一種自動化工具,對網站發送各種探測請求,檢查已知的資安弱點(過期套件、弱加密套件、缺少安全標頭、憑證問題等),輸出一份 finding 清單。這幾輪對話討論的「自簽憑證」就是其中一條。 ### CORS (Cross-Origin Resource Sharing) 瀏覽器預設不准網頁對「不同來源」的伺服器發請求,除非該伺服器用回應標頭明講「我允許」。 * **在這個專案裡**:`cors({ origin: [https://${ip}], methods: [...] })` 只允許自己這個來源,且把預設會開的 PUT/DELETE 拿掉,只留實際會用到的方法。 ### CSP (Content-Security-Policy) 瀏覽器端的白名單機制,規定這個頁面「只能」向哪些來源載入腳本、樣式、圖片、開 WebSocket 連線,擋掉被注入的惡意外部程式碼。 * **在這個專案裡**:`security.ts` 的 `connect-src` 只允許連到自家網域、Google 登入服務,以及對應偵測到的 IP 的 `wss://`(給 LiveKit WebSocket 用)。 ### HSTS (Strict-Transport-Security) 告訴瀏覽器「以後永遠用 HTTPS 連我,不要再嘗試 HTTP」,避免連線被中間人偷偷降級成明文。 ### Cache-Control: no-store 告訴瀏覽器和中繼代理「這個回應不要存快取」,避免 token、房間狀態這類敏感資料留在快取裡外流。 --- ## 07 動手驗證指令小抄 改完憑證邏輯後,不用等掃描報告,先用這幾行指令自己核對。 ```bash # 這張憑證是誰簽的、簽給誰 —— Issuer 應該不等於 Subject openssl x509 -in cert.pem -noout -issuer -subject # 假設驗證端信任這張 CA,鏈驗不驗得過 —— 應該印出 OK openssl verify -CAfile ca-cert.pem cert.pem # 模擬一次真實連線,看實際送出的鏈與驗證結果 openssl s_client -connect <ip>:443 -showcerts ``` ### verify error 意思說明 | verify error num | 意思 | 看到時該怎麼想 | | :--- | :--- | :--- | | **0 ok** | 鏈完整、驗證通過 | 加 `-CAfile ca-cert.pem` 應該看到這個 | | **18** | self signed certificate | Leaf 本身就是自簽,沒有任何簽發層級 —— **原本的問題,已修掉** | | **19** | self signed certificate in certificate chain | 鏈頂端的 Root 本身自簽,且不在驗證端信任清單裡 —— **任何私有 CA 對陌生裝置都會這樣,是預期行為** | | **20** | unable to get local issuer certificate | 找不到簽發者憑證,通常是鏈沒送完整(如果只送 Leaf、沒附 CA 就會這樣) | | **21** | unable to verify the first certificate | 鏈中前面某一步驗證失敗,導致整條鏈都不算過 | > **備註**:不帶 `-CAfile` 的結果永遠會是「這台機器沒裝過我們的私有 CA」——這是刻意的設計取捨(不想在使用者裝置上多一道安裝步驟),不代表憑證本身有問題。真正能蓋棺論定的,還是實際重跑一次 WebInspect 掃描,看報告怎麼分類。 ```