Share Notes

chundev

View the Project on GitHub latteouka/share-notes

TLS 信任派送實戰:自簽 CA 與公信 CA 的部署差異

日期:2026-04-29 主題:TLS / PKI / 企業內 CA 部署


TL;DR

TLS 信任是單向的 — 驗證 cert 的是 client,不是 server。Server 即使把 root CA 包進 chain 一起送,client 也會忽略,root 必須是 client 預先就信任的。企業內派送 CA 信任有兩條路:自簽 CA(要自己想辦法把 root 送到每台機器)、公信 CA(OS 廠商已經幫你內建,零派送)。


背景

很常見的場景:企業內架了一個 API 服務,憑證用內網 AD CA 簽。另一台機器要打 API 過來,於是冒出問題:

那張 root 憑證要不要加到「被打的那台」(Server)的 OS 信任清單裡? 如果我把 root 包進 server cert chain 一起送過去,是不是 client 就不用裝了?

兩個問題都對 TLS 信任模型有誤解。下面拆解清楚。


誤解一:Server 需要信任自己的 CA?

不需要。看 TLS 握手的方向:

Client ────HTTPS 請求────▶ Server
                              │ 出示 server cert(CA 簽的)
       ◀───── server cert ────┘
       │
       └─ 「我憑什麼信你?」用自己的信任清單比對 root

驗證憑證的是收到憑證的那一方,不是出示憑證的那一方。Server 的 cert 是它自己的身分證,它不用驗證自己。

→ 結論:Server 那台 不需要 把 root 加到 OS 信任清單。要加的是「打 API 過來那台 client」。

唯一例外:Server 本身也會主動呼叫其他內部服務(這時它變 Client),或啟用了 mTLS 要驗 client cert,這兩種情況才需要在 Server 端裝 root。


誤解二:Server 把 root 包進 chain 一起送,client 就會信?

沒用。 這是新手最容易踩的概念坑。

信任的本質

❌ 錯誤心智模型:
   Server: "我用這張 cert,順便附上簽我的 root,你信吧"
   Client: "好喔" ← 如果是這樣,整個 PKI 崩潰

✅ 實際信任模型:
   Server: "我這張 cert 是 X 簽的,X 是 Y 簽的,Y 是 root"
   Client: "我先看我自己的信任清單裡有沒有 Y"
            ├─ 有 → 過
            └─ 沒有 → 拒絕(你送過來的 root 我**忽略**)

如果 server 送什麼 root client 就信什麼,那任何攻擊者只要自己生一張 root + 簽張 fake server cert 就能假冒任何網站,HTTPS 形同虛設。

Trust anchor 必須透過 TLS 以外的安全管道事先送到 client(GPO 派送、手動匯入、OS 內建、image baking),靠 TLS channel 本身傳 root 是先有雞還是先有蛋的悖論。

RFC 慣例:別把 root 放進 server chain

依 RFC 5246 / 8446 的精神,server 送出的 chain 不應該包含 root。常見作法是 fullchain 只含 leaf + intermediate(s),不含 root:

# 在還沒匯入 AD CA root 的機器上
curl -v https://internal-api.example.local/

# 即使 server 把 root 包進 chain 一起送過來,你會看到:
# * SSL certificate problem: unable to get local issuer certificate
#                            ^^^^^ 注意是 "local"

「local」issuer — 找的是 client 本機的信任清單。


自簽 CA 派送的六種方式

自簽 CA(AD CS / 自建 OpenSSL CA)的痛點:root cert 必須事先透過某種方式送到每一台要驗的機器。常見方式:

1. GPO 派送(Windows AD 環境最主流)

GPO 路徑:

Computer Configuration
  └─ Policies
     └─ Windows Settings
        └─ Security Settings
           └─ Public Key Policies
              ├─ Trusted Root Certification Authorities  ← root 匯這裡
              └─ Intermediate Certification Authorities  ← 中間 CA 匯這裡

Enterprise CA 福利:選「Enterprise CA」(AD 整合版)時,root 自動寫入 AD 的 Configuration → Services → Public Key Services → Certification Authorities,所有網域成員開機 / gpupdate 後自動繼承,零手動。

GPO 涵蓋範圍:

對象 GPO 是否吃得到
Domain-joined Windows ✅ 自動
Domain-joined Mac (進階整合) ⚠️ 部分,通常要 Jamf 補
Linux server ❌ 完全不吃
行動裝置 ❌ 要 MDM
廠商來的非網域機器 ❌ 要手動
Container / K8s pod ❌ 要烤進 image

2. MDM 派送(Mac / iOS / Android / 非網域 Win)

Jamf / Intune / Workspace ONE 設 Configuration Profile,把 root cert 包成 .mobileconfig(iOS / Mac)或 SCEP / PKCS#12,員工登錄 MDM 時自動信任。

3. 設定管理工具(Linux / 大量 server)

# Ansible playbook 範例
- name: Trust internal CA root
  copy:
    src: internal-ca-root.crt
    dest: /usr/local/share/ca-certificates/internal-ca-root.crt
- command: update-ca-certificates

也可以用 Puppet / Chef / SaltStack / SCCM。

4. AIA / CDP — 寫對能省很多事

把 CA HTTP 下載點寫進 cert 的 AIA (Authority Information Access)CDP (CRL Distribution Points) 欄位:

常見錯誤:cert 簽出來但 AIA URL 內網外網不通,導致行動裝置 / 外部廠商驗不過。

5. 黃金映像 / 容器映像 baking

COPY internal-ca-root.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates

新機 / 新 container 開出來就已經信任。

6. 手動匯入(給廠商或非網域機器)

# Linux
sudo cp internal-ca-root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates

# Windows
certutil -addstore -f "Root" internal-ca-root.cer

# Java(常見遺漏!)
keytool -importcert -trustcacerts -alias internal-ca \
  -file internal-ca-root.crt \
  -keystore "$JAVA_HOME/lib/security/cacerts" \
  -storepass changeit

# Node.js
export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/internal-ca-root.crt

# Python (requests / httpx)
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt

公信 CA 為什麼幾乎不用派送?

買的公信 CA(DigiCert、TWCA、GlobalSign、Let’s Encrypt 等)的 root,OS 廠商和瀏覽器廠商已經內建:

你買 cert → 公信 CA 簽
              ↑
              這家 CA 的 root 早就被收進:
              ├─ Microsoft Trusted Root Program (Windows)
              ├─ Apple Root Program (macOS / iOS)
              ├─ Mozilla CA Certificate Program (Firefox / 多數 Linux)
              ├─ Chrome Root Program (Chrome 105+ 自己用)
              ├─ Android System Trust Store
              └─ Java cacerts / Node.js / Python certifi
              
              ↓ Windows Update / 系統更新自動同步
              
你的 client → 早就信任了 → 連線 OK

Chrome Root Program 是新變數

Chrome 105 起在 macOS 與 Windows 改用自己的 root store,不再讀 OS。後續推到 ChromeOS、Linux、Android(iOS 因 Apple 政策仍用系統 store)。實務影響:

公信 CA 仍然要做的事

  1. server 送 fullchain(leaf + intermediate,不含 root)。沒送中間 CA 是最常見錯誤,瀏覽器靠 AIA fetching 看似 OK,但手機 / Java / 老 client 全爆。
  2. SSL Labs / sslscan 驗證鏈完整性。
  3. 注意老舊系統死角:Windows 7 / Server 2008 R2 root store 早不更新;某些嵌入式裝置 / IoT 出廠後 root 永遠停在某年;Java 8 早期版本 cacerts 落後 OS 一截。這些情境下「公信 CA 也會壞」。

Wildcard vs SAN 的選擇

*.example.com 只能匹配一層 subdomain:

要多層或多個無關網域,有幾種選擇:

類型 涵蓋範圍 適用場景
標準 wildcard *.example.com 單層 subdomain 簡單統一網域
SAN cert(多列幾個 FQDN) 明確列出的每個 host 數量可控、安全度要求高
Multi-Domain Wildcard / SAN Wildcard 多個 wildcard 共存 跨多個網域
ACME 自動化(單 cert / 服務) 每個服務一張 DevOps、安全成熟度高

Wildcard 的安全代價

一張 wildcard cert 私鑰外洩 = 整個網域所有服務都被冒充。安全成熟度高的組織會避免 wildcard,改用大量 SAN 或自動化簽單 cert(ACME + DNS challenge)。


自簽 vs 公信 CA 對照表

情境 自簽 (內網 AD CA) 公信 CA
內網系統,員工都網域成員 ⭐ GPO 自動派送,最佳解 也行但要花錢
對外服務(公開域名) ❌ 外部不信你的 root ⭐ 必選
開發 / 測試環境 ⭐ 自簽夠用 浪費錢
給廠商串 API(非網域成員) ⚠️ 寄 root cert + SOP,麻煩 ⭐ 對方零配置
行動裝置 / BYOD ⚠️ 要 MDM ⭐ 內建信任
自動化 / DevOps ⭐ AD CS auto-enrollment 或內網 ACME ⭐ ACME (Let’s Encrypt)

學到的事


參考資料