SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

如何將自訂 CA 加入 Ubuntu 信任儲存庫

透過 OpenSSL 建立私有 CA 並簽署憑證,將根憑證放入 /usr/local/share/ca-certificates 並執行 update-ca-certificates,即可讓 Ubuntu 系統信任您的內部 HTTPS 服務。

將自訂 CA 加入 Ubuntu 的信任儲存庫

若要將自訂 CA 加入 Ubuntu 的信任儲存庫,請將根憑證複製到 /usr/local/share/ca-certificates/ 目錄下,並確保檔案名稱以 .crt 結尾,接著執行 sudo update-ca-certificates。CA(憑證授權單位)是一組金鑰對,其憑證具備簽署其他憑證的權限。一旦機器信任您的根憑證,該根憑證所簽署的所有憑證皆會被接受,如此一來,您自架服務之間的 HTTPS 連線便不會再因驗證失敗而中斷。

本指南使用 openssl 在離線狀態下建立完整的憑證鏈。您將建立根金鑰與根憑證,為伺服器簽發一張終端憑證,接著安裝根憑證,並觀察相同的驗證指令如何改變其結果。此順序至關重要:在安裝前後進行驗證,能讓您確認安裝動作確實改變了驗證結果。

Ubuntu 24.04 預設映像檔已內建 OpenSSL 3 與 ca-certificates 套件,因此無需額外安裝任何軟體(於 2026 年 8 月確認)。

何時該自行架設 CA?

像 Let's Encrypt 這類公開 CA 需要在公開 DNS 中擁有名稱,且該伺服器必須可被存取。內部名稱並不符合此條件。位於私有網路的資料庫或綁定在隧道(tunnel)上的管理面板無法取得公開憑證,也不應為了取得憑證而將其暴露於網際網路。

Ubuntu 上的自簽憑證 僅能解決單一主機的問題。每個用戶端都必須信任該憑證,且每增加一台主機就得重複相同作業。私有 CA 將信任決策提升了一個層級。用戶端只需信任根憑證一次,後續由該根憑證簽署的所有憑證都會被信任,包含尚未建立的主機憑證。

這是有代價的。根金鑰可以簽署任何限制範圍內的事物,因此任何讀取 ca.key 的人都能簽發您的機器會接受的憑證。請務必像保護 SSH 金鑰管理 中的私鑰一樣保護它。如果服務擁有公開 DNS 名稱,請跳過上述所有步驟並使用公開 CA:搭配 nginx 與 Let's Encrypt 使用 Certbot 的工作量較少,且用戶端無需安裝任何額外軟體。

建立 CA 金鑰與根憑證

請在僅限您個人帳號可存取的目錄中操作。根金鑰絕對不可移出此目錄。

install -d -m 700 ~/ca
cd ~/ca
openssl genrsa -aes256 -out ca.key 4096
chmod 600 ca.key

-aes256 會以您設定的密碼短語加密金鑰,後續所有使用此金鑰進行簽署的指令皆需輸入該密碼。若省略 -aes256,金鑰將以明文儲存於磁碟中;如此一來,備份檔案或擁有存取權的第二個管理員帳號,便足以讓他人取得簽發受信任憑證的能力。

接著建立根憑證,由 CA 金鑰自行簽署。

openssl req -x509 -new -key ca.key -sha256 -days 3650 \
  -subj "/O=Example Internal/CN=Example Internal Root CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign" \
  -addext "subjectKeyIdentifier=hash" \
  -addext "nameConstraints=critical,permitted;DNS:internal.example" \
  -out ca.crt

請將 internal.example 替換為您實際使用的名稱後綴,並在保留該副檔名之前先閱讀下一節。

每個延伸屬性(extension)各司其職:

  • basicConstraints 搭配 CA:TRUE 是將此憑證定義為 CA 憑證的關鍵。若缺少此項,客戶端將拒絕任何由該金鑰簽署的憑證,即使簽章本身正確無誤。
  • pathlen:0 規定此 CA 僅能簽署終端憑證(leaf certificates),不得再簽署下層 CA。
  • keyUsage 限制該金鑰僅能用於簽署憑證與撤銷清單(revocation lists),防止該金鑰被誤用為 TLS 伺服器金鑰。
  • subjectKeyIdentifier 為根憑證提供識別碼,供終端憑證指向該簽發者;當客戶端的憑證庫中存有數百個憑證時,客戶端即以此識別正確的簽發者。
  • nameConstraints 限制此 CA 可擔保的名稱範圍。

請檢查您所建立的內容,切勿預設指令已達成您的預期。

openssl x509 -noout -subject -issuer -serial -dates -in ca.crt
openssl x509 -noout -text -in ca.crt

由於根憑證為自簽憑證,其主體(Subject)與簽發者(Issuer)欄位應顯示相同的字串。序號與兩個日期欄位均來自您剛建立的檔案,請直接從該輸出結果中確認,而非參考他人的指南。

限制 CA 的簽署權限

除非另有設定,否則系統憑證存放區中的根憑證會被信任以簽署網際網路上的任何名稱。這代表單一伺服器上的單一檔案擁有極大的權限。nameConstraints 可降低此風險。若根憑證中包含 permitted;DNS:internal.example,即使簽章正確,該 CA 對 internal.example 以外名稱所簽署的憑證鏈仍會被拒絕。

請透過測試來驗證此機制,而非僅僅信任它。

openssl req -new -newkey rsa:2048 -nodes -keyout /tmp/outside.key -out /tmp/outside.csr \
  -subj "/CN=www.example.com"
openssl x509 -req -in /tmp/outside.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 30 -sha256 \
  -extfile <(printf 'subjectAltName=DNS:www.example.com\n') -out /tmp/outside.crt
openssl verify -CAfile ca.crt /tmp/outside.crt
echo $?

該憑證會被核發,因為您的 CA 會簽署任何您要求簽署的內容。驗證過程才是阻斷點:退出狀態碼為非零值,且 OpenSSL 會指出觸發限制的項目。這正是此延伸欄位的價值所在。即便 CA 金鑰遭竊,攻擊者仍無法針對子樹以外的名稱產生有效的憑證。完成後,請使用 rm /tmp/outside.* 刪除剩餘檔案。

在套用限制前,有四點必須注意。此限制標記為 critical,因此若用戶端無法識別此延伸欄位,必須拒絕該憑證鏈而非忽略它;這是較安全的做法,但可能會導致舊版 TLS 程式庫出現異常。DNS 名稱的允許子樹(permitted subtree)不會限制 IP 位址 SAN,因為未列出子樹的名稱類型將保持不受限制,因此若您的憑證包含 IP 位址,請在同一個延伸欄位中加入 permitted;IP:10.0.0.0/255.255.0.0。子樹必須涵蓋您未來將核發的所有名稱,包括簡短的主機名稱,因此針對 app 這類裸名稱(bare name)的憑證,若對照上述範例將會驗證失敗。此外,此限制已寫入根憑證中,若要變更設定,意味著必須重新產生根憑證,並在所有用戶端上重新安裝。

簽發由您的 CA 簽署的葉憑證 (leaf certificate)

葉憑證是伺服器向客戶端出示的憑證。請從其專屬金鑰與 CSR (憑證簽署請求) 開始,CSR 包含公開金鑰與請求的名稱,並由葉憑證金鑰簽署,以證明請求者持有私鑰。

openssl req -new -newkey rsa:2048 -nodes \
  -keyout app.key -out app.csr \
  -subj "/CN=app.internal.example"
chmod 600 app.key

重要的名稱應放入延伸檔案 (extension file) 中,而非 CSR。客戶端會比對主機名稱與 subjectAltName (SAN),並完全忽略通用名稱 (common name)。因此,若憑證僅有 CN 而無 SAN,無論 CN 內容為何,所有現行客戶端皆會驗證失敗。

basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = DNS:app.internal.example, DNS:api.internal.example
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always

將其儲存為 app.ext,接著使用 CA 簽署該請求。

openssl x509 -req -in app.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 397 -sha256 -extfile app.ext -out app.crt

-CAcreateserial 會在 CA 旁寫入 ca.srl,其中存放下一個序號,確保此 CA 簽發的憑證不會重複。請將該檔案保留在 CA 目錄中。-days 397 僅為工具選項而非限制。相較於公開 CA,此處的憑證效期較短更為重要,因為私有 CA 沒有撤銷基礎設施:除非您自行建置,否則不會有 CRL 或 OCSP 回應程式,因此一旦葉憑證金鑰外洩,在憑證過期前皆可持續使用。

在將憑證加入信任儲存區 (trust store) 前,請先檢查結果。

openssl x509 -noout -subject -issuer -serial -dates -in app.crt
openssl x509 -noout -ext subjectAltName -in app.crt

發行者 (issuer) 欄位現在顯示的是 CA 名稱而非葉憑證本身。SAN 欄位列出了此憑證有效的名稱,客戶端僅會針對該列表進行比對。

在安裝任何項目之前,請使用明確的 -CAfile 進行驗證

openssl verify -CAfile ca.crt app.crt
echo $?

此步驟僅詢問一個狹義的問題:app.crt 是否鏈結至 ca.crt 中的憑證?由於您在指令列中直接指定了根憑證,此測試與本機信任設定無關。若此處驗證失敗,代表憑證本身有問題,請務必在繼續之前修正。

現在,請詢問本機系統。

openssl verify app.crt
echo $?

若未指定 -CAfile,OpenSSL 將會回退至其內建的憑證目錄。openssl version -d 會列印出建置時所使用的基礎目錄;在 Ubuntu 上,該目錄下的 certs 會解析為 /etc/ssl/certs。由於您的根憑證尚未放入該處,驗證將會失敗:憑證鏈追溯至憑證庫中不存在的簽發者,且已無其他路徑可供查詢。請注意此時的結束狀態碼,這將是兩個步驟後會發生變化的關鍵。

使用真實的客戶端進行測試比 openssl verify 更為準確,因為它除了檢查憑證鏈外,還會驗證主機名稱。請提供憑證並進行擷取測試。

openssl s_server -accept 8443 -cert app.crt -key app.key -www &
curl --resolve app.internal.example:8443:127.0.0.1 https://app.internal.example:8443/

--resolve 會將連線導向 127.0.0.1,同時請求 app.internal.example,因此 SAN 欄位會匹配,此時唯一的問題僅在於信任鏈。curl 會失敗並列印出無法驗證憑證鏈的原因。加入 -v 可取得更多詳細資訊。請保持測試伺服器持續執行。

將根憑證安裝至 /usr/local/share/ca-certificates

sudo cp ca.crt /usr/local/share/ca-certificates/example-internal-root.crt
sudo chmod 644 /usr/local/share/ca-certificates/example-internal-root.crt
sudo update-ca-certificates

決定此操作是否成功的細節如下:

  • 檔案名稱必須以 .crt 結尾。update-ca-certificates 手冊頁指出,位於 /usr/local/share/ca-certificates 目錄下且副檔名為 .crt 的憑證會被納入並預設為受信任。若檔案名稱為 root.pemroot.cer,系統會直接略過且不會產生任何提示。
  • 內容必須為 PEM 格式,即由 BEGIN CERTIFICATEEND CERTIFICATE 行所包覆的 base64 區塊。若將 DER 格式檔案直接重新命名為 .crt,該檔案仍為二進位格式,系統將無法讀取。請使用 openssl x509 -inform DER -in ca.der -out ca.crt 進行轉換。
  • 此處僅存放根憑證。CA 私鑰與終端實體憑證(leaf certificate)不應放置於信任儲存區中。

update-ca-certificates 會顯示新增與移除的憑證數量。若顯示新增數量為 0,請檢查副檔名或檔案格式是否正確。

請從系統層面確認變更,而非僅依賴該訊息。

ls -l /etc/ssl/certs/$(openssl x509 -noout -subject_hash -in /usr/local/share/ca-certificates/example-internal-root.crt).0
grep -c 'BEGIN CERTIFICATE' /etc/ssl/certs/ca-certificates.crt

第一個指令會根據憑證的主體雜湊值(subject hash)建立檔案名稱並列出該檔案。update-ca-certificates 會建立該符號連結,並指向您所安裝的檔案。第二個指令則會計算單一憑證組合檔中的憑證數量。建議在安裝前後分別執行此指令,即可觀察數量是否增加 1。

將此根憑證複製到其他機器時,請在安裝前確認複製後的檔案完整無誤。根憑證若發生錯誤,將對系統造成嚴重影響,因此請務必將其視為任何其他下載檔案,在使用前進行 校驗碼驗證

再次對照系統儲存區進行驗證

openssl verify app.crt
echo $?
curl --resolve app.internal.example:8443:127.0.0.1 https://app.internal.example:8443/

使用相同的指令、相同的憑證檔案,但結果不同。app.crt 的設定並未更動,伺服器也是稍早啟動的那一台。唯一的差異在於根憑證現已位於用戶端讀取的儲存區中,因此憑證鏈得以完整。這是必須牢記的機制:驗證過程即是在用戶端已信任的發行者中進行搜尋,而安裝 CA 憑證正是將發行者加入該搜尋位置的方法。

請使用 kill %1 停止測試伺服器。

為什麼不該將檔案存放在 /etc/ssl/certs

/etc/ssl/certs 是自動產生的輸出。update-ca-certificates 會將其填入指向實際憑證檔案的符號連結,並在旁邊寫入串接後的憑證包 /etc/ssl/certs/ca-certificates.crt

手動複製到該目錄的憑證不會被任何程式識別。OpenSSL 的目錄查找功能僅會開啟以憑證主體雜湊值(subject hash)命名的檔案,因此名為 myca.crt 的檔案對其而言是不可見的。Ubuntu 上的 curl 會讀取憑證包檔案,而該憑證包是從已註冊的來源重建的,因此您手動複製的檔案也不會出現在該路徑中。執行 update-ca-certificates --fresh 時,目錄中的符號連結會被移除並重建,這會導致您手動建立的連結一併消失。

另一部分則是 /usr/share/ca-certificates,它屬於 ca-certificates 套件,並列於 /etc/ca-certificates.conf 中。套件更新會覆寫該檔案。/usr/local/share/ca-certificates 是保留給本機管理員使用的目錄,因此您的 CA 憑證在管理其餘憑證的套件進行升級時,仍能被保留下來。

哪些程式會忽略系統信任儲存區

安裝根憑證可修復所有呼叫 OpenSSL 或讀取 /etc/ssl/certs 的程式。這涵蓋了 curl、wget、git、Python 標準 ssl 模組,以及在 Linux 上讀取系統檔案的 Go 程式。自行封裝憑證清單的執行環境則不受影響,這也是安裝成功後大多數困惑的來源。

  • Node.js 使用編譯進去的清單。請透過 NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/example-internal-root.crt 指向您的根憑證,並在處理程序啟動前設定該環境變數,因為 Node 只會在啟動時讀取一次。目前的 Node 發行版本也提供讀取系統儲存區的選項;執行 node --help | grep -i system-ca 可確認您的版本是否支援。
  • Python 的 requests 程式庫使用 certifi 憑證包。請為該處理程序設定 REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt,或在呼叫時傳入 verify="/etc/ssl/certs/ca-certificates.crt"pip 則基於相同原因使用 --cert
  • Java 讀取的是 keystore。在 Ubuntu 上,ca-certificates-java 套件會在 /etc/ca-certificates/update.d/ 下安裝掛鉤,因此當該套件存在時,update-ca-certificates 也會一併更新 Java keystore。若無此套件,請使用 keytool -importcert 匯入根憑證。
  • Firefox 維護自己的儲存區,從不查看 /etc/ssl/certs。請透過其憑證設定進行匯入。Linux 上的 Chromium 讀取的是使用者專屬的 NSS 資料庫,您可使用 libnss3-tools 套件中的 certutil 進行編輯。
  • 容器擁有自己的檔案系統,因此宿主機的儲存區對容器內部無效。請將根憑證複製到映像檔中,並在建置期間執行 update-ca-certificates。若您的服務執行於 Docker Compose on a VPS,請務必納入此規劃。

當程式在乾淨安裝後仍拒絕憑證時,請在進行任何變更前,先找出該程式開啟了哪些檔案。strace -f -e trace=openat <command> 2>&1 | grep -i cert 是最直接的工具,只需執行一次即可釐清問題。

維持 CA 的長期可用性

重新簽發葉憑證(leaf)的過程與初始簽發相同,同樣包含 CSR 步驟與簽署步驟,並使用相同的 app.ext 檔案。用戶端無須採取任何行動,因為其信任的根憑證(root)並未變更。請務必保留 ca.srl 以及 CA 目錄下的所有 .ext 檔案,以便下次簽發時能直接重複執行已驗證過的指令,而非憑記憶重建。

請將 ca.keyca.crt 備份至機器以外的儲存空間,並維持加密狀態。若遺失金鑰,您將無法簽發任何新憑證:屆時必須建立第二個 CA,並將其根憑證安裝至所有先前部署過舊根憑證的地方。請維護一份清單,記錄所有已安裝該根憑證的機器與應用程式憑證庫;這份清單是後續進行憑證輪替與移除作業的必要條件。

當根憑證即將過期時,請提早產生替換用的新根憑證,並將兩者同時安裝。憑證庫中並存兩個根憑證並無問題,用戶端會接受其中任何一個。接著使用新根憑證重新簽發葉憑證,待確認無任何服務依賴舊根憑證後,再將其移除。

從信任儲存區移除 CA

sudo rm /usr/local/share/ca-certificates/example-internal-root.crt
sudo update-ca-certificates --fresh

--fresh 會移除 /etc/ssl/certs 中的符號連結,並根據剩餘的來源重新建立連結,因此刪除根憑證後,該目錄與憑證包會同步更新。請使用與驗證安裝相同的方式,確認憑證已移除。

openssl verify app.crt
echo $?
grep -c 'BEGIN CERTIFICATE' /etc/ssl/certs/ca-certificates.crt
ls -l /etc/ssl/certs/$(openssl x509 -noout -subject_hash -in ~/ca/ca.crt).0

驗證再次失敗,憑證數量回到初始狀態,且雜湊符號連結已消失。

該指令僅會更動系統儲存區,不會影響其他位置。請手動復原其他位置的安裝:清除 NODE_EXTRA_CA_CERTS、從任何 Java keystore 刪除別名、從各個瀏覽器設定檔中移除根憑證,並重新建置任何已內建該憑證的容器映像檔。移除根憑證並不會使該 CA 所簽署的憑證失效。在任何仍信任該 CA 的機器上,這些憑證依然有效;這正是為何私有 CA 必須維護一份根憑證部署清單的實際原因。若無法完全撤銷一個 CA,它將成為永久性的安全漏洞,因此請在設定當天,趁清單尚短時,先在一台機器上測試移除流程。

FAQ

我該將 CA 憑證放在 Ubuntu 的哪個位置?

請放在 /usr/local/share/ca-certificates/,檔案名稱需以 .crt 結尾且內容為 PEM 格式,接著執行 sudo update-ca-certificates。該目錄專供本機管理員使用,因此套件升級不會更動其中的檔案。/usr/share/ca-certificates 屬於 ca-certificates 套件,而 /etc/ssl/certs 則由上述兩者產生,因此若將檔案置於這兩個位置,將會被覆寫或忽略。

執行 update-ca-certificates 後,為何 curl 仍拒絕該憑證?

請依序排查原因。檔案名稱可能未以 .crt 結尾,或是 DER 格式而非 PEM,導致 update-ca-certificates 將其跳過而未加入任何內容。憑證可能缺乏與主機名稱相符的 subjectAltName,這屬於主機名稱驗證失敗而非信任問題;請使用 openssl x509 -noout -ext subjectAltName -in app.crt 進行檢查。伺服器可能僅傳送了終端憑證,但實際上需要中繼憑證。curl 可能透過 CURL_CA_BUNDLE--cacert 指向了不同的憑證包。此外,長時間執行的服務需要重新啟動,因為大多數程式僅在啟動時讀取一次信任儲存庫。

系統信任儲存庫是否涵蓋 Firefox、Chrome、Node 與 Java?

不涵蓋。curl、wget、git、Python 的標準 ssl 模組以及 Go 程式會讀取系統檔案,因此在執行 update-ca-certificates 後即可運作。Firefox 維護自己的儲存庫。Linux 上的 Chromium 使用每個使用者獨立的 NSS 資料庫,需透過 libnss3-tools 套件中的 certutil 進行編輯。Node.js 需要透過 NODE_EXTRA_CA_CERTS 指向您的根憑證檔案。Java 讀取的是金鑰儲存庫(keystore),僅在安裝 ca-certificates-java 套件時才會由 update-ca-certificates 更新。Python 的 requests 使用 certifi,且需要 REQUESTS_CA_BUNDLE

如何從 Ubuntu 的信任儲存庫中移除 CA?

/usr/local/share/ca-certificates/ 刪除該檔案並執行 sudo update-ca-certificates --fresh--fresh 選項會清除 /etc/ssl/certs 中的符號連結並重新建立,因此憑證會同時從雜湊符號連結與 ca-certificates.crt 憑證包中移除。請針對該 CA 簽署的憑證執行 openssl verify 並讀取結束狀態碼以進行確認。接著,請在您曾加入該憑證的所有其他儲存庫中重複移除步驟,因為該指令不會影響其他儲存庫。

我能為公開網站使用私有 CA 而非 Let's Encrypt 嗎?

不能。訪客的瀏覽器從未見過您的根憑證,因此會顯示全頁警告,且您無法在無法控制的機器上安裝您的根憑證。私有 CA 僅適用於由您自身機器解析的名稱,以及由您管理的用戶端。對於任何陌生人會造訪的網站,請務必從公開 CA 取得憑證。

#tls#certificates#openssl#ubuntu#security#pki