SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-07

Ubuntu 24.04 自簽憑證正確設定:Chrome 可接受

在 Ubuntu 24.04 使用一個含 SAN 的 openssl 指令建立 Chrome 可接受的 TLS 憑證,接入 nginx 或 Apache,並正確設定信任,不必使用 curl -k。

您要建立的內容

建立一張現代瀏覽器與用戶端確實接受的自簽 TLS 憑證,設定正確的 subjectAltName、適當的金鑰權限,並將其接入 nginx 或 Apache。此外,還要處理幾乎所有指南都略過的部分:正確讓用戶端信任這張憑證,而不是讓使用者一直略過警告,或永遠在指令碼中硬編碼 curl -k。最後,當內部服務從 1 個增加到 6 個時,再建立一個只需 5 個指令的私有 CA。

先做判斷,因為自簽憑證實際上適用的情況,遠比常見的使用方式少。如果服務可透過真實 DNS 名稱從公用網際網路存取,請停止閱讀,改用 在 nginx 上使用 certbot 取得免費的 Let's Encrypt 憑證,或使用 Apache 的對應方法。這不需付費,會自動續期,而且全球每個瀏覽器都已信任它。在公用網站使用自簽憑證,會讓使用者養成略過安全性警告的習慣;這比使用純 HTTP 更糟。

沒有涉及公用網際網路時,自簽憑證才是合適的工具。例如,繫結至 VPS 上 WireGuard tunnel 位址的管理面板、私有網路中的 staging 主機、後端服務之間的服務對服務流量、家用實驗室設備,或取代 Webmin 在 10000 埠自行產生的預留位置憑證。Let's Encrypt 無法為 10.8.0.1git.internal.lan 核發憑證;公用 CA 也不會將私有 IP 或自訂 TLD 放入憑證。對這些名稱而言,您就是 CA。

以下所有操作都在全新的 Ubuntu 24.04 主機上執行。該版本隨附 OpenSSL 3.0.x(openssl version 用於確認)。這些操作不需要網際網路連線,在隔離網路環境中也能完整執行。

舊式單行指令為何產生 Chrome 拒絕的憑證

每篇 2017 年以前的教學都會提供類似以下的指令:

# Do not run this — shown so you recognise it in old guides
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout selfsigned.key -out selfsigned.crt

它會依序提出互動式問題,將主機名稱寫入 Common Name 欄位,並產生不含 subjectAltName 擴充功能的憑證。這種憑證一開始就無法使用。Chrome 在 2017 年 4 月的第 58 版停止讀取 Common Name;早在 2000 年,RFC 2818 就已淘汰以 CN 比對名稱的方式。Firefox、Safari、curl 與 Python 的行為也相同。憑證必須透過 SAN 擴充功能識別其伺服器,否則就無法識別;瀏覽器會以以下文字明確告知這點:

NET::ERR_CERT_COMMON_NAME_INVALID

This server could not prove that it is git.internal.lan; its security
certificate does not specify Subject Alternative Names.

無論如何調整 trust store,都無法修正這項錯誤,因為該憑證確實沒有指定任何名稱。如果您目前看到 NET::ERR_CERT_COMMON_NAME_INVALID,表示憑證沒有 SAN,或 SAN 不正確,必須重新簽發憑證。幸好只需一個指令即可修正。

讓瀏覽器接受的憑證:一個指令

OpenSSL 在 1.1.1 版加入了 -addext 旗標,因此不再需要舊版指南用來注入 SAN 的設定檔繁瑣步驟。在 Ubuntu 24.04 上執行:

sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 730 -noenc \
  -keyout /etc/ssl/private/git.internal.key \
  -out /etc/ssl/certs/git.internal.crt \
  -subj "/CN=git.internal.lan" \
  -addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"

各旗標的作用如下:

  • -x509 會直接產生自簽憑證,而不是產生簽署要求。
  • -newkey rsa:4096 會在同一個步驟產生新的金鑰。RSA 4096 不會影響舊版用戶端;如果所有連線端都使用現代版本,-newkey ec -pkeyopt ec_paramgen_curve:P-256 的檔案較小且速度較快。
  • -noenc 是 OpenSSL 3.x 對舊版 -nodes 的寫法:金鑰不使用密碼片語。兩種寫法都能運作。使用密碼片語的金鑰會讓 nginx 在每次開機時等待輸入,因此伺服器金鑰應使用此選項。
  • -days 730,效期為 2 年;該數字的詳細說明請見效期一節。
  • -subj 會直接在指令中回答互動式問題。CN 現在只是外觀資訊,但仍應設為主要名稱;部分工具會顯示它。
  • -addext "subjectAltName=..." 是關鍵旗標。請列出用戶端會輸入的每一個名稱與每一個 IP:主機名稱使用 DNS: 項目(可使用 DNS:*.internal.lan 等萬用字元),位址使用 IP: 項目。如果有人會瀏覽 https://10.8.0.1,就必須加入 IP:10.8.0.1 項目;只有 DNS SAN 會再次讓他們遇到 NET::ERR_CERT_COMMON_NAME_INVALID

在進行任何設定前,先確認 SAN 確實已寫入:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltName

正確輸出:

X509v3 Subject Alternative Name:
    DNS:git.internal.lan, IP Address:10.8.0.1

如果輸出的是 No extensions in certificate,表示憑證沒有 SAN。瀏覽器會拒絕此憑證,請重新產生,不要繼續進行設定。

鎖定金鑰

任何使用者都能讀取的私密金鑰,就不是私密金鑰。在 Ubuntu 上,/etc/ssl/private 已經是 710 root:ssl-cert,可避免一般使用者直接查看,但仍應明確設定檔案本身的權限:

sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.key

Nginx 和 Apache 都會先以 root 讀取憑證,再降權執行,因此使用 root:root 權限模式 600 即可。如果金鑰供以自身使用者執行、並自行載入金鑰的服務使用,例如 Node 應用程式、Gitea 或 Python daemon,chown 應改為將金鑰交給該服務使用者,權限仍設為 600。絕對不要使用 644 權限、將副本放在 git repository 中,或將副本放在 /tmp

將其接入 nginx

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name git.internal.lan;

    ssl_certificate     /etc/ssl/certs/git.internal.crt;
    ssl_certificate_key /etc/ssl/private/git.internal.key;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}
sudo nginx -t && sudo systemctl reload nginx

nginx -t 必須在重新載入開始生效前輸出 syntax is oktest is successful。如果輸出的是 SSL_CTX_use_PrivateKey_file() failed ... key values mismatch,表示憑證與金鑰來自兩次不同的產生作業,請參閱失敗模式一節。

將其接入 Apache

sudo a2enmod ssl proxy proxy_http

單獨使用 ssl 在這裡並不足夠:以下 vhost 會使用 ProxyPass,若缺少 mod_proxymod_proxy_http,設定檔測試會因 Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration 而失敗。將 vhost 儲存為 /etc/apache2/sites-available/git-internal.conf

<VirtualHost *:443>
    ServerName git.internal.lan
    SSLEngine on
    SSLCertificateFile      /etc/ssl/certs/git.internal.crt
    SSLCertificateKeyFile   /etc/ssl/private/git.internal.key

    ProxyPass        / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
sudo a2ensite git-internal
sudo apache2ctl configtest && sudo systemctl reload apache2

configtest 應能回應 Syntax OK。現在從用戶端機器進行測試:

curl -v https://git.internal.lan/

此時會收到錯誤:

curl: (60) SSL certificate problem: self-signed certificate

這不是錯誤。這表示 TLS 正常運作:curl 從未聽過你的憑證,因此拒絕連線至無法驗證的伺服器。下一節會提供真正的修正方式,而且不是目前網路上許多人採用的做法。

讓用戶端信任它,以及應拒絕的反模式

先說明錯誤的修正方式,並直接指出它們的問題。將 curl -k(或 --insecure)直接寫入 script、在 Python requests 中使用 verify=False、在 Node 中使用 NODE_TLS_REJECT_UNAUTHORIZED=0,都無法讓用戶端信任憑證。這些做法會關閉憑證驗證,導致用戶端接受任何伺服器提供的任何憑證,包括攻擊者插入傳輸路徑中的憑證。這些做法保留了 TLS 的額外負擔,卻失去 TLS 原本提供的身分驗證。更糟的是,這些 flag 會擴散:先貼到一個 cron job,再貼到 deploy script,最後進入 production code,直到沒有人記得哪些連線原本只應暫時停用驗證。如果某個 verify=False 在觸發它的除錯工作結束後仍然存在,表示設計有誤。

正確的修正方式,是讓每個用戶端作業系統將此憑證視為受信任的根憑證。在 Ubuntu 和 Debian 用戶端上:

sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificates

輸出中重要的那一行(其後會接著一個 Running hooks in /etc/ca-certificates/update.d... 區塊):

Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

這些行中有兩個容易忽略的問題。檔案必須.crt 結尾;.pem 副檔名會被靜默忽略,最後得到 0 added,且不會顯示錯誤訊息。內容也必須是 PEM 格式,檔案開頭會是 -----BEGIN CERTIFICATE-----;如果原始檔是 DER 二進位格式,請先使用 openssl x509 -inform der -in file.der -out file.crt 轉換。將自簽憑證本身加入為根憑證即可運作,因為自簽憑證本身就是自己的根憑證。

完成後,curlwgetgitapt 以及其他使用 OpenSSL 搭配系統憑證包的程式,都會在不使用任何 flag 的情況下信任該伺服器。少數用戶端使用自己的信任存放區,因此需要個別處理:

  • Linux 上的 Chrome/Chromium 讀取 NSS 資料庫,而不是系統憑證存放區:sudo apt install libnss3-tools,然後為每位使用者執行 certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crt
  • Firefox 使用自己的憑證存放區:前往 Settings → Privacy & Security → Certificates → Import;或在 about:config 中將 security.enterprise_roots.enabled 設為 true,讓它讀取系統憑證存放區。
  • Python requests 內建自己的 CA bundle(certifi),不會使用系統憑證存放區:傳入 verify="/usr/local/share/ca-certificates/git.internal.crt",或匯出 REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
  • Node.js:匯出 NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt

在 Windows 用戶端上,連按兩下 .crt,並將它安裝到 Trusted Root Certification Authorities。在 macOS 上,使用 Keychain Access 將它加入 System keychain,並標記為 Always Trust

一個根憑證服務多個服務:小型私有 CA

逐一信任憑證很快就無法擴展:6 個服務乘以 4 台用戶端電腦,需要安裝 24 次信任設定,而且每增加一個服務,就會增加更多設定。解決方法是使用私有 CA,讓用戶端只信任一個根憑證,再使用該根憑證為每個服務簽署憑證。

較方便的選項是 mkcert。它已包含在 Ubuntu 24.04 的套件庫中,並能處理 update-ca-certificates 未涵蓋的 NSS 儲存區(Chrome、Firefox):

sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1

mkcert -install 會建立根憑證,並將其註冊到該電腦上的每個信任儲存區;第 3 個命令會產生 git.internal.lan+2.pemgit.internal.lan+2-key.pem,可直接放入前述的 nginx 或 Apache 設定片段。這項工具的設計前提是開發電腦:根憑證金鑰會留在執行 -install 的電腦上,因此非常適合開發用筆電,但不適合伺服器群組。

對伺服器而言,單純使用 OpenSSL,只需 5 個命令即可完成整個 CA:

openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -key lab-ca.key -sha256 -days 3650 \
  -out lab-ca.crt -subj "/CN=Lab Internal CA" \
  -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genrsa -out git.key 2048
openssl req -new -key git.key -out git.csr -subj "/CN=git.internal.lan" \
  -addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"
openssl x509 -req -in git.csr -CA lab-ca.crt -CAkey lab-ca.key \
  -CAcreateserial -days 730 -sha256 -copy_extensions copy -out git.crt

問題在最後一個命令:openssl x509 -req 預設會從 CSR 移除所有擴充欄位,包括你仔細加入的 SAN。-copy_extensions copy 是 OpenSSL 3.x 的選項,因此可在 24.04 上運作,能將這些欄位一併帶入;省略它時,簽署後的憑證不會包含 SAN,Chrome 又會顯示 NET::ERR_CERT_COMMON_NAME_INVALID。使用與先前相同的 openssl x509 -noout -ext subjectAltName 檢查進行驗證。

依照上述信任儲存區步驟,將 lab-ca.crt 分發到用戶端;每台電腦只需設定一次。請像保護最重要的憑證一樣保護 lab-ca.key:權限設為 600,最好存放在不屬於其簽署伺服器的另一台電腦上,因為持有它的人可以為用戶端信任的任何名稱簽發憑證。

到期與輪替

公開 CA 憑證的有效期限正在縮短。CA/Browser Forum 已在 2026 年 3 月將新簽發的公開信任憑證上限設為 200 天,低於原本的 398 天;2027 年將降至 100 天,2029 年 3 月再降至 47 天。但這些規則只適用於公開信任的 CA。您的私有 CA 不受這些規則約束,瀏覽器也不會對手動安裝的根憑證強制套用這些限制。實際上有一項限制必須注意:無論憑證由誰簽發,Apple 平台都會拒絕有效期限超過 825 天的 TLS 伺服器憑證。因此,如果 iPhone 或 Mac 會連線,終端憑證的有效期限應維持在兩年以內。-days 730 在所有環境中都符合這項限制;使用有效期限十年的根憑證搭配兩年的終端憑證,是適合內部環境的配置。

長效憑證只會以一種方式失效:在沒有人記得當初選定的日期,同時且無聲地失效。先查看目前的憑證:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddate

將續期排入實際的行事曆,或讓 cron 在到期前 30 天提醒您。當距離到期時間少於指定秒數時,openssl x509 -checkend 2592000 -in cert.crt 會以非零狀態結束。如果您已經使用 Uptime Kuma 進行狀態監控,其 HTTPS 監控功能會免費標示即將到期的憑證。

使用私有 CA 進行輪替相當簡單:重新執行 CSR 與簽署指令,替換檔案,重新載入 Web 伺服器。根憑證沒有變更,因此用戶端不會察覺任何變化。

錯誤模式與畫面中會看到的字串

NET::ERR_CERT_AUTHORITY_INVALID 是安裝信任設定前的預期狀態,不是憑證缺陷。若在安裝根憑證後仍持續出現:在 Linux 上,Chrome 讀取的是 NSS,而不是系統憑證存放區(請參閱 certutil 步驟);或複製的檔案結尾不是 .crt,且 update-ca-certificates 顯示 0 added;也可能是伺服器提供的憑證與您信任的憑證不同,請使用 openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256 比對指紋。

NET::ERR_CERT_COMMON_NAME_INVALID 表示憑證沒有 SAN,或 SAN 未涵蓋網址列中的名稱。典型情況是 SAN 列出 DNS:git.internal.lan,但使用者瀏覽的是 https://10.8.0.1。變更信任存放區無法修正此問題,請重新簽發憑證並加入缺少的項目。

curl: (60) SSL certificate problem: self-signed certificate 表示 curl 不信任此憑證。若憑證由您的私有 CA 簽署,變體 self-signed certificate in certificate chain 表示相同問題。一次性修正方式是 curl --cacert lab-ca.crt https://...;永久修正方式是設定信任存放區。不要使用 -k

unable to load certificate ... Expecting: TRUSTED CERTIFICATE(或 Expecting: CERTIFICATE REQUESTno start line)表示 PEM 格式混淆。您提供給 OpenSSL 的檔案類型不正確:在需要憑證時提供了金鑰或 CSR,或在需要 PEM 時提供了 DER 二進位檔。head -1 filename 會告訴您實際擁有的檔案類型;憑證會以 -----BEGIN CERTIFICATE----- 開頭。若檔案是 DER,請使用 openssl x509 -inform der -in file.der -out file.crt 進行轉換。

nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch) 表示憑證與金鑰不相符,通常是因為產生指令執行了兩次,導致檔案混用。請以 openssl x509 -in git.internal.crt -noout -pubkey | sha256sumopenssl pkey -in git.internal.key -pubout | sha256sum 進行確認;雜湊值相同表示兩者是配對的。若兩者不同,請一併重新產生。

FAQ

為什麼建立自簽憑證後,Chrome 仍顯示「不安全」?

如果錯誤是 NET::ERR_CERT_AUTHORITY_INVALID,表示憑證本身沒有問題,只是 Chrome 尚未信任它。請將憑證(或私有 CA root)安裝到用戶端的 trust store。請注意,在 Linux 上,Chrome 會透過 certutil 使用 NSS database,而不是使用系統 trust store。如果錯誤是 NET::ERR_CERT_COMMON_NAME_INVALID,表示憑證缺少與 URL 相符的 Subject Alternative Name,必須使用 -addext "subjectAltName=..." 重新簽發。

如何讓 curl 信任自簽憑證,而不使用 -k?

將憑證(PEM 格式,副檔名為 .crt)複製到 /usr/local/share/ca-certificates/,然後執行 sudo update-ca-certificates;輸出必須顯示 1 added。之後,curl 會像驗證公開憑證一樣驗證它。若只需進行單次請求而不修改系統,curl --cacert /path/to/cert.crt 會只使用該檔案進行驗證;-k 則會完全停用驗證,不應放入任何人的腳本中。

自簽憑證可以有效多久?

從技術上來說,效期可以自行決定。CA/Browser Forum 的限制(目前為 200 days,2029 年將為 47 days)只適用於公開信任的 CA,不適用於私有 trust。實務上,請將伺服器憑證的效期上限設為 825 days,因為 Apple 裝置不論簽發者為何,都會拒絕更長的憑證。使用效期十年的私有 root,以及效期兩年的(-days 730)leaf 憑證,是合理的預設值。請務必排程續期,因為內部憑證過期可能在無人記得的日期,讓所有服務無預警中斷。

應該使用自簽憑證還是 Let's Encrypt?

如果服務具有公開 DNS 名稱,且可從網際網路連線,請一律使用 Let's Encrypt。它免費、自動化,且所有用戶端都已信任。Let's Encrypt 無法簽發的情況,才適合使用自簽憑證(或私有 CA),例如私有 IP、.lan 這類僅供內部使用的主機名稱、air-gapped network,以及刻意隱藏在 VPN 後方的服務。判斷依據是可連線性與命名方式,不是安全強度;兩者使用的密碼學技術相同。

為什麼將憑證加入 /usr/local/share/ca-certificates/ 後,仍遭拒絕?

請檢查以下 3 項。檔案名稱必須以 .crt 結尾;副檔名為 .pem 的檔案會遭靜默略過,而 update-ca-certificates 會回報 0 added。內容必須是以 -----BEGIN CERTIFICATE----- 開頭的 PEM 文字,而不是 DER 二進位檔。最後,應用程式必須確實使用系統 trust store。Linux 上的 Chrome、Firefox、Python requests、Node.js 與 Java 各自維護私有 trust store,必須分別加入該憑證。