數字憑證與 TLS 握手的底層機制

要在網際網路上建立安全的加密連線,用戶端(如瀏覽器)必須解決兩個核心問題:確認對端伺服器的真實身份,以及在不可信的網路通道中安全地協商出對稱加密金鑰。HTTPS 協定通過 TLS(Transport Layer Security)協定實現了這一目標,而 SSL/TLS 憑證則是這一信任體系的基礎。 TLS 憑證本質上是由受信任的憑證頒發機構(Certificate Authority, CA)數字簽名的電子檔案,遵循 X.509 標準。憑證內部包含了域名資訊、憑證所有者的公鑰、CA 的身份資訊、有效期限以及 CA 使用其私鑰生成的數字簽名。 當使用者在瀏覽器中輸入 https://example.com 時,用戶端與伺服器之間會觸發 TLS 握手流程:

Client                                  Server
|                                       |
|-------- ClientHello (TLS 1.3) ------->|
|                                       |
|<-- ServerHello, Certificate, Key -----| (服务器发送证书与公钥参数)
|    Exchange & Finished                |
|                                       |
| [校验证书链与数字签名]                  |
| [生成会话密钥]                         |
|                                       |
|-------- Finished (Encrypted) -------->|
|                                       |
|<====== 双向对称加密数据传输 ======>|

用戶端在收到伺服器傳來的憑證後,並不會無條件信任它,而是通過非對稱加密演算法執行嚴格的”憑證鏈校驗”。用戶端作業系統或瀏覽器內建了一組根憑證列表(Root CA),這些根憑證包含權威 CA 的公鑰。CA 會使用自己的私鑰對伺服器的公鑰和域名資訊進行雜湊並簽名。用戶端使用本地內建的 CA 公鑰解密該簽名,並將解密結果與憑證內容的實際雜湊值進行對比;若一致,則證明該憑證確實由權威 CA 簽發且未被篡改。校驗成功後,雙方基於 ECDHE 等金鑰交換演算法協商出臨時對稱金鑰,後續所有的 HTTP 請求和響應均通過該對稱金鑰進行高效率的加解密傳輸。 過去,從傳統 CA 申請憑證是一個高度依賴人工干預的過程。運維人員需要使用 OpenSSL 工具在本地生成私鑰和憑證籤名請求(Certificate Signing Request, CSR),將 CSR 提交給 CA 機構,通過郵件驗證或 DNS 記錄追加等方式人工證明域名所有權。CA 經過人工或半自動審核後頒發憑證,運維人員再手動下載檔案並寫入伺服器設定。這類憑證的有效期通常長達 1 至 2 年,一旦私鑰洩露或憑證過期,極易導致生產事故。

ACME 自動化協定與憑證生態格局

為了解決人工申請效率低下、成本高昂且容易出錯的問題,網際網路安全研究小組(ISRG)推出了 Let’s Encrypt,並主導制定了 RFC 8555 標準——ACME(Automatic Certificate Management Environment)協定。ACME 協定將”身份驗證 - 憑證籤發 - 部署 - 定期續期”的全流程完全 API 化,使得伺服器能夠以無人值守的方式自動化維護 TLS 憑證。 在 ACME 協定架構中,部署在伺服器上的用戶端(如 Certbot、acme.sh)與 CA 的 ACME 服務端通過 HTTPS REST API 進行通訊。當用戶端向 CA 請求籤發憑證時,CA 必須驗證請求者對目標域名的控制權。ACME 協定定義了兩類核心的驗證挑戰(Challenge):

  1. HTTP-01 挑戰:ACME 服務端生成一個隨機 Token,要求用戶端將其放置在 Web 伺服器的特定路徑下:http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>。隨後 CA 的驗證伺服器通過公網 HTTP(80 埠)訪問該 URL。若能成功讀取並匹配金鑰指紋,則驗證通過。這種方式設定簡單,但無法簽發泛域名憑證(Wildcard Certificate,如 *.example.com),且要求 80 埠對公網開放。
  2. DNS-01 挑戰:ACME 服務端要求用戶端在域名的 DNS 解析記錄中新增一條特定名稱的 TXT 記錄(通常為 _acme-challenge.<YOUR_DOMAIN>)。CA 查詢 DNS 伺服器獲取該 TXT 記錄的值,匹配無誤即完成驗證。DNS-01 不需要 Web 伺服器開放 80 埠,且支援申請泛域名憑證,但需要用戶端具備操作 DNS Provider API 的權限——第三、四節會分別用 Certbot 和 acme.sh 配合 Cloudflare API Token 完成這一流程。 在當今的憑證生態中,免費憑證服務方案主要呈現為三種不同的技術路徑:
  • Let’s Encrypt:純粹的開源非營利性 CA,也是 ACME 標準的奠基者。它簽發的憑證有效期僅為 90 天,以此強制運維體系實施自動化續期機制,從而大幅縮短私鑰洩露後的風險視窗。
  • ZeroSSL:同樣支援標準的 ACME 協定,可直接作為 Certbot 或 acme.sh 的後端替換選擇。其優勢在於提供視覺化的 Web 管理後臺,並在某些特殊網路環境下對老舊裝置的相容性更好,同時提供商業級支援與付費擴充選項。
  • Cloudflare:採用不同的邊緣架構。Cloudflare 作為反向代理 CDN 接入時,使用者無需在源站直接面對公網簽發憑證,Cloudflare 在邊緣節點統一使用自身的 CA 簽發 TLS 憑證並完成 HTTPS 解除安裝。但在邊緣節點到源伺服器(Origin Server)之間,仍然需要部署源站憑證(Origin CA Certificate)以確保端到端的嚴格加密。本文第三、四節簽發的憑證就屬於這個”源站憑證”角色——Cloudflare 只是在這裡充當 DNS Provider 來完成 DNS-01 驗證,與它自己簽發的邊緣憑證是兩回事。

Certbot 原生運維實踐:HTTP-01 與 certonly 模式

Certbot 是 Let’s Encrypt 官方推薦的 ACME 用戶端。在實際部署中,Certbot 主要提供兩種模式:自動化託管模式(利用 --nginx--apache 外掛自動改寫 Web 伺服器設定)和 certonly 模式(僅負責申請憑證,設定由運維人員完全掌控)。 對於追求可控性與標準化的工程環境,強烈建議使用 certonly 模式。自動化外掛對 Web 伺服器設定檔的改寫往往過於侵入,可能打破原有的模組化設定結構。使用 certonly 配合 Webroot 驗證,可以在完全不打擾現有 Nginx 行程執行的情況下,安全地完成憑證申請與更新。

環境準備與 Nginx 驗證路徑設定

首先確保伺服器已安裝 Nginx,並開放 80 和 443 埠。在 Nginx 設定中設定通用的 .well-known/acme-challenge/ 訪問規則,將請求對映至獨立的靜態檔案目錄。 編寫 /etc/nginx/sites-available/example.com.conf 設定:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    # 将 ACME HTTP-01 挑战请求映射至统一的静态目录,避免干扰业务逻辑
    location /.well-known/acme-challenge/ {
        alias /var/www/certbot/.well-known/acme-challenge/;
        try_files $uri =404;
    }
    # 其他 HTTP 请求统一重定向至 HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}

執行設定檢查並生效:

# 创建 ACME 挑战验证文件存放目录并设置权限
sudo mkdir -p /var/www/certbot/.well-known/acme-challenge
sudo chown -R www-data:www-data /var/www/certbot
# 检查 Nginx 语法并重新加载配置
sudo nginx -t && sudo nginx -s reload

使用 Certbot certonly 模式申請憑證(HTTP-01)

通過 Linux 系統包管理器安裝 Certbot 後,使用 certonly --webroot 命令發起簽發請求:

sudo certbot certonly \
  --webroot \
  -w /var/www/certbot \
  -d example.com \
  -d www.example.com \
  --email [email protected] \
  --agree-tos \
  --no-eff-email

上述命令的參數定義如下:

  • certonly:指令 Certbot 僅向 CA 獲取憑證檔案,禁止自動修改 Nginx 設定檔。
  • --webroot:指定使用 HTTP-01 驗證外掛,並將驗證 Token 寫入現有的 Web 伺服器目錄。
  • -w /var/www/certbot:指定 ACME 挑戰檔案寫入的根路徑。Certbot 會在此目錄下建立 .well-known/acme-challenge/ 子目錄並生成校驗檔案。
  • -d:指定需要包含在憑證中的域名(Subject Alternative Name, SAN)。
  • --email:指定用於接收憑證過期提醒與安全警告的電子郵箱。
  • --agree-tos:顯式同意 Let’s Encrypt 服務條款。 成功簽發後,憑證檔案將儲存在 /etc/letsencrypt/live/example.com/ 目錄中。該目錄下包含以下核心檔案軟連結:
  • fullchain.pem:包含伺服器憑證以及中間憑證鏈(Intermediate CA)的完整文本。Nginx 的 ssl_certificate 指令必須指向此檔案,否則部分用戶端(如移動端 SDK)會因缺少中間憑證鏈而報信任錯誤。
  • privkey.pem:伺服器私鑰。權限嚴格受限(僅 root 可讀),絕對不能洩露或上傳至版本控制系統。

完整 Nginx HTTPS 站點設定

獲取憑證後,修改 Nginx 設定檔啟用 TLS:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;
    # 配置 Let's Encrypt 签发的证书链文件与私钥
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    # TLS 协议及加密套件优化,停用存在安全隐患的旧协议
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    # TLS 会话缓存,减少重连时的非对称加密计算开销
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    # 业务根路径
    location / {
        root /var/www/html;
        index index.html;
    }
}

設定檔更新完成後,執行 sudo nginx -t && sudo nginx -s reload 使 HTTPS 生效。

憑證自動續期與 Hook 機制

Let’s Encrypt 憑證有效期只有 90 天,推薦在憑證到期前 30 天內觸發續期。Certbot 提供了 renew 命令,該命令會自動掃描 /etc/letsencrypt/renewal/ 目錄下儲存的所有憑證設定,僅對即將過期的憑證重新發起 ACME 挑戰。 在更新憑證後,已在記憶體中執行的 Nginx 行程並不會自動載入磁碟上的新檔案,必須執行 nginx -s reload。這可以通過 Certbot 的 --deploy-hook 自動化處理:

# 测试续期流程(dry-run 仅演练,不真正向 ACME 发起签发,也不触发 Hook)
sudo certbot renew --dry-run
# 配置 Cron 定时任务,每日凌晨 03:00 检查并自动续期
# 编辑 /etc/crontab 或运行 crontab -e 追加以下行:
0 3 * * * root certbot renew --quiet --deploy-hook "nginx -s reload"

當憑證成功續期後,--deploy-hook 指定的命令才會被觸發,從而在無感知的情況下完成服務熱重新載入。

用 DNS-01 簽發泛域名憑證:Certbot + Cloudflare API Token

4.1 為什麼需要 DNS-01

HTTP-01 需要 CA 能通過公網 80 埠訪問到你的伺服器,這在兩種場景下行不通:簽發泛域名憑證 *.example.com(HTTP-01 協定本身就不支援),或者源站根本不打算對公網開放 80 埠(例如源站只通過 Cloudflare Tunnel 或內網訪問)。DNS-01 用一條 TXT 記錄替代了”能被公網訪問到”這個前提,因此這兩種場景都能覆蓋。 代價是用戶端需要有權限替你在 DNS 裡寫記錄,所以下一步是發一個只能做這一件事的憑證,而不是把整個 Cloudflare 賬號的鑰匙交出去。

4.2 建立最小權限的 Cloudflare API Token

不要使用 Cloudflare 後臺裡那個”Global API Key”——它等於賬號全部權限的萬能鑰匙,一旦洩露,比如誤提交進了 Git 倉庫,攻擊者能操作賬號下所有域名和產品。正確做法是在 My Profile → API Tokens → Create Token 裡新建一個範圍收窄到最小的 Token:

  • 權限(Permissions):Zone / DNS / Edit
  • 資源(Zone Resources):Include / Specific zone / 選中你要簽發憑證的那個域名,而不是 All zones
  • 可選:在 Client IP Address Filtering 裡把 Token 鎖定到簽發憑證的這臺伺服器的出口 IP,進一步收窄可用範圍。

建立後 Cloudflare 只會顯示一次 Token 字串,立刻儲存好;這張 Token 之後要麼交給 Certbot 的憑證檔案,要麼交給 acme.sh 的環境變數,本身不需要、也不應該被寫進任何會被提交到版本控制的檔案。

4.3 安裝 certbot-dns-cloudflare 外掛並設定憑證

# Debian/Ubuntu
sudo apt install python3-certbot-dns-cloudflare
# RHEL 系
sudo dnf install python3-certbot-dns-cloudflare

建立憑證檔案,只存放上一步生成的 API Token:

sudo mkdir -p /root/.secrets/certbot
sudo tee /root/.secrets/certbot/cloudflare.ini > /dev/null <<'EOF'
dns_cloudflare_api_token = 你的CloudflareAPIToken
EOF
sudo chmod 600 /root/.secrets/certbot/cloudflare.ini

chmod 600 是必須的一步:這個檔案裡就是明文 Token,權限過寬會讓同一臺機器上的其他賬戶也能讀到它。

4.4 執行 DNS-01 簽發

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 30 \
  -d example.com \
  -d '*.example.com' \
  --email [email protected] \
  --agree-tos \
  --no-eff-email

--dns-cloudflare-propagation-seconds 讓 Certbot 在提交 TXT 記錄後先等待一段時間再觸發驗證,給 Cloudflare 的 DNS 傳播留出餘量,避免 CA 查詢時記錄還沒生效。簽發結果同樣落在 /etc/letsencrypt/live/example.com/,續期方式和第三節完全一樣——certbot renew 會自動識別這本是用 dns-cloudflare 外掛簽發的,重新走一遍相同的驗證方式,無需額外設定。

acme.sh:更輕量的 ACME 用戶端

acme.sh 是一個純 Shell 實現的 ACME 用戶端,不依賴 Python 或系統包管理器,安裝即用,尤其適合在資源受限的小型伺服器或者不想引入額外執行時依賴的場景。

5.1 安裝

curl https://get.acme.sh | sh -s [email protected]
source ~/.bashrc

安裝指令碼會把 acme.sh 放進 ~/.acme.sh/,同時自動註冊一條 cron 任務定期檢查續期,不需要像 Certbot 那樣手動設定定時任務。

5.2 HTTP-01:webroot 方式簽發

acme.sh --issue -d example.com -w /var/www/certbot

效果與 Certbot 的 --webroot 相同:把驗證檔案寫進現有 Web 目錄,不需要額外開放埠,也不支援泛域名憑證。

5.3 DNS-01:用同一個 Cloudflare Token 簽發泛域名憑證

acme.sh 內建了 Cloudflare 的 DNS API 鉤子(dns_cf),複用 4.2 節建立的同一個 API Token 即可,不需要單獨的憑證檔案——直接通過環境變數傳入:

export CF_Token="你的CloudflareAPIToken"
export CF_Account_ID="你的Cloudflare账号ID"   # 在 Cloudflare 域名概览页右侧可以找到

acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'

CF_Token/CF_Account_ID 只在這次簽發時讀取一次並寫入 acme.sh 自己的賬號資料目錄(~/.acme.sh/account.conf),後續 acme.sh --renew 會自動複用,不需要每次都重新匯出。

5.4 部署憑證到 Nginx

acme.sh 預設把憑證存放在自己的內部目錄結構裡,不建議直接引用;推薦用 --install-cert 把憑證複製(並在續期時自動更新)到 Nginx 實際使用的路徑,同時繫結重新載入命令:

sudo mkdir -p /etc/nginx/ssl/example.com
acme.sh --install-cert -d example.com \
  --key-file       /etc/nginx/ssl/example.com/privkey.pem \
  --fullchain-file /etc/nginx/ssl/example.com/fullchain.pem \
  --reloadcmd      "systemctl reload nginx"

之後 Nginx 的 ssl_certificate/ssl_certificate_key 就指向 /etc/nginx/ssl/example.com/ 下這兩個固定路徑;每次自動續期成功後,acme.sh 會自動重新執行一遍 --install-cert 的複製與 --reloadcmd,全程無需人工干預。

5.5 驗證自動續期

crontab -l | grep acme.sh   # 确认安装时自动写入的定时任务存在
acme.sh --cron              # 手动触发一次检查,用于验证整条链路是否工作正常

Docker 化容器環境部署實踐

在現代雲原生架構中,元件通常以容器化方式隔離執行。將 Nginx 與 Certbot 分別部署在獨立容器中,並通過 Docker Shared Volumes 共享憑證檔案與 HTTP-01 驗證路徑,是生產環境的最佳實踐。 為了避免複雜的依賴,可以直接使用 Compose 來編排容器。

目錄結構規劃

/opt/ssl-stack/
├── docker-compose.yml
├── nginx/
│   └── conf.d/
│       └── default.conf
└── data/
    ├── certbot/
    │   ├── conf/       # 挂载至 Certbot 的 /etc/letsencrypt,保存证书文件
    │   └── www/        # 挂载至 ACME 验证路径
    └── webroot/        # 业务静态资源路径

編排檔案設定

建立 docker-compose.yml 設定檔:

version: '3.8'
services:
  nginx:
    image: nginx:1.25-alpine
    container_name: production_nginx
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./data/certbot/conf:/etc/letsencrypt:ro
      - ./data/certbot/www:/var/www/certbot:ro
      - ./data/webroot:/usr/share/nginx/html:ro
  certbot:
    image: certbot/certbot:v2.8.0
    container_name: production_certbot
    restart: unless-stopped
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt:rw
      - ./data/certbot/www:/var/www/certbot:rw
    # 每隔 12 小时自动运行一次 check,若证书需续期则自动完成 renew
    entrypoint: "/bin/sh -c 'trap exit TERM; while true; do certbot renew; sleep 12h & wait $${!}; done'"

引導階段設定(Solving the Chicken-and-Egg Problem)

在首次啟動時存在一個經典的”先有雞還是先有蛋”邏輯困局:Nginx 啟動時如果設定檔中引用了 TLS 憑證檔案,而憑證尚未簽發,Nginx 行程會因找不到檔案而直接崩潰退出;但如果 Nginx 不啟動,ACME HTTP-01 校驗請求又無法被正確處理。 解決方案是採用兩步引導法。 第一步,編寫適用於初始簽發的純 HTTP Nginx 設定檔 ./nginx/conf.d/default.conf

server {
    listen 80;
    server_name example.com;
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    location / {
        return 200 'Initializing SSL...';
        add_header Content-Type text/plain;
    }
}

第二步,啟動 Nginx 容器並首次執行 Certbot 容器發起憑證籤發請求:

# 1. 启动 Nginx 提供 HTTP-01 校验服务
docker compose up -d nginx
# 2. 运行一次性 Certbot 容器申请证书
docker compose run --rm certbot certonly \
  --webroot \
  --webroot-path=/var/www/certbot \
  -d example.com \
  --email [email protected] \
  --agree-tos \
  --no-eff-email

第三步,憑證生成完畢後,修改 ./nginx/conf.d/default.conf,寫入完整的 HTTPS 代理或靜態資源配置:

server {
    listen 80;
    server_name example.com;
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}
server {
    listen 443 ssl http2;
    server_name example.com;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    location / {
        root /usr/share/nginx/html;
        index index.html;
    }
}

最後,熱重新載入 Nginx 容器並啟動常駐的 Certbot 定時任務容器:

# 重载 Nginx 加载 SSL 配置
docker compose exec nginx nginx -s reload
# 启动包括 Certbot 定时检查在内的全量服务
docker compose up -d

在此方案中,常駐的 Certbot 容器會在後臺每 12 小時執行一次 certbot renew。一旦憑證在磁碟掛載卷中被重新整理,可以通過宿主機的 Cron 觸發 Nginx 容器平滑重新載入:

# 在宿主机 Crontab 中添加,每周定期通知 Nginx 重新加载内存中的证书
0 4 * * 0 docker exec production_nginx nginx -s reload

工程抉擇與信任演進

理解 PKI 信任鏈、TLS 握手過程以及 ACME 協定的校驗機制,是掌握現代網路安全架構的必經之路。在工程落地的選擇上,雖然各種技術工具形態不同,但底層原理高度統一: 對於單機部署或簡單運維場景,基於宿主機的 certbot certonly --webroot 配套 Nginx 與 Cron,是入侵性最小、邏輯最清晰的方案;如果目標是泛域名憑證,或者源站根本不想對公網開放 80 埠,--dns-cloudflare 配合一個權限收窄到單個 zone 的 API Token 是更合適的路徑。偏好少一層 Python 依賴、又不想手動管理 cron 的場景,acme.sh 用同一張 Cloudflare Token 提供了幾乎等價的 DNS-01 能力,--install-cert 還順帶解決了續期後自動複製、重新載入的問題。而在雲原生容器化的運維體系中,利用 Docker Compose 隔離設定與憑證持久化掛載,能夠提供極其優雅的平滑部署與自動化續期體驗。 只要徹底解耦”憑證籤發”與”服務設定”兩個階段,就能抹平基礎設施的複雜性,建立起一套高可靠、零人工干預的自動化 HTTPS 運維體系。