Let’s Encrypt logo

更新紀錄

2022.12.20: 整合另一篇筆記,關於缺少中繼憑證 (issuer certificate, intermediate certificate) 導致 curl 報錯,而瀏覽器沒事的 trouble shooting 過程。

介紹

Let’s Encrypt 是一個 ISRG 建立的非營利項目,免費提供 TLS/SSL Certification
除了有超多網路巨頭 贊助商 同時也是 Linux Foundation 合作的項目

本篇筆記會記錄使用 certbot 這款專門為 Let’s Encrypt 打造的 ACME client (Automatic Certificate Management Environment) 來產生免費的 TLS/SSL 憑證 (TLS certification)

系統環境

DNS hosting: CloudFlare (Free plan)
OS: CentOS 7.9.2009

安裝 certbot

2021.10.13 更新,不建議用 yum 安裝 certbot。原因: EPEL 內的版本過低,無法使用 Cloudflare API token。
建議使用 pip3 install 或者 Docker
Running with Docker

1
2
3
4
yum install python3-pip -y
pip3 install --upgrade pip
pip3 install --upgrade setuptools
pip3 install certbot
1
2
3
4
#yum install certbot nginx -y
certbot --version

certbot 1.11.0

從相依性套件我們可以知道 certbot 是由 Python 撰寫的。

[Manual] 申請 CA 憑證

certbot 有兩大功能: 1. 獲取憑證 2. 安裝憑證

The Certbot client supports two types of plugins for obtaining and installing certificates: authenticators and installers.

Getting certificates (and choosing plugins)

certbot-Document

1
certbot certonly --manual --preferred-challenges dns

上面的指令翻譯成白話文就是:

certonly => 我只要獲取憑證 (obtaining certification)
--manual => 我要手動進行域名擁有權驗證
--preferred-challenges dns => 偏好使用 DNS record 驗證 (預設使用 HTTP-01)

certbot_manual_0

▲ 輸入要申請的域名、拿到指定 TXT record。 如果要申請 wildcard 以上面範例來說輸入 *.xxzk.me 即可

certbot_manual_1

▲ 到 DNS hosting 設定 DNS record (證明網域擁有權)。這邊使用的是 CloudFlare。 對於驗證網域有問題的這邊請 Domain Validation

沒有綽啦! 買到域名第一件事情就是設定 NS 讓 CloudFlare 代管
回想上一次使用 CloudFlare DNS hosting 已經是 2015/1/20 的事情了 (菸~

certbot_manual_2

▲ 驗證完成,成功拿到 Let’s Encrypt 簽發的 TLS Certification

這是 certbot 產生的 README。告訴使用者…

不要在這個路徑移動或者重新命名這些檔案,Web server 要使用 ln 過去即可

  1. privkey.pem 一看就知道是 private key 只不過習慣上會把附檔名改成 .key
  2. fullchain.pem 一看就知道是 CA Bundle
  3. chain.pem 用猜的也知道只包含 Root CA + issuer CA
  4. cert.pem 想都不用想,證書本人

上面能這麼囂張的原因是… 我都用 openssl 偷看答案了 ^__^

1
openssl x509 -in <*.pem> -text

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
This directory contains your keys and certificates.

`privkey.pem`  : the private key for your certificate.
`fullchain.pem`: the certificate file used in most server software.
`chain.pem`    : used for OCSP stapling in Nginx >=1.3.7.
`cert.pem`     : will break many server configurations, and should not be used
                 without reading further documentation (see link below).

WARNING: DO NOT MOVE OR RENAME THESE FILES!
         Certbot expects these files to remain in this location in order
         to function properly!

We recommend not moving these files. For more information, see the Certbot
User Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.

[CloudFlare Plugin] 申請 CA 憑證

certbot CloudFlare 的 Plugin 可以做到自動幫我們更換 DNS record 的目的。這在之後 使用 cron job 自動更新憑證 時會使用到。 Let’s Encrypt 頒布的憑證時效只有 3 個月,如果不使用自動更新 (renew) 的方式會很麻煩。

Welcome to certbot-dns-cloudflare’s documentation!

安裝 CloudFlare DNS Plugin

2021.10.13 更新,不建議用 yum 安裝 certbot-dns-cloudflare。原因: EPEL 內的版本過低,無法使用 Cloudflare API token。
建議使用 pip3 install 或者 Docker
Running with Docker
[Docker Hub] certbot/dns-cloudflare

1
2
3
4
yum install python3-pip -y
pip3 install --upgrade pip
pip3 install --upgrade setuptools
pip3 install cloudflare certbot-dns-cloudflare
1
#yum install python2-certbot-dns-cloudflare.noarch -y

cloudflare_plugin_0

▲ 到 CloudFlare Dashboard 申請 API token

範例

1
certbot certonly --force-renewal --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini  -d *.xxzk.me

1
2
3
4
cat cloudflare.ini

# Cloudflare API token used by Certbot
dns_cloudflare_api_token = -5I5fericishandsomeXXWjGnopeeking7JehahahasbNxx

--force-renewal 之前 cert.pem 的 SHA256SUM 是 da579f4e9d38db942ce66c2e9b5e4dbaec14272bbb6ccc873d151c688294bd87
拿到新的 cert.pem 之後在算一次 SHA256SUM 雜湊 08b65867e2007e047629aa41f53bcc13b47048afb8a7d36c61e75dc8e422e0ba
得證,有更新!

cloudflare_plugin_1

--force-renewal 成功

自動更新憑證 Automated Renewals

Automated Renewals

1
SLEEPTIME=$(awk 'BEGIN{srand(); print int(rand()*(3600+1))}'); echo "0 0,12 * * * root sleep $SLEEPTIME && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null

至於需要 random sleep 的原因是不想讓 server 在 00:00 接受 DDoS 攻擊 XD
Why need a random sleep in cron for certbot?

Let’s Encrypt Rate Limits

Rate Limits

Let’s Encrypt 基於公平合理使用原則,針對不同操作訂下上限值。

1. 主要限制: 單一註冊域名 (Registered Domain) 每周至多 50 次申請證書。(Renew 則不在此限)
2. 每張證書能夠容納 100 個子域名 (subdomain)。這種證書又稱為 SAN certificate
3. 基於上述兩點,每周至多能申請 5000 個子域名 (subdomain)
4. 每個相同證書 (certificate) 每周至多 5 次更新 (renew)

碰觸到 1. 錯誤訊息會顯示 too many certificates already issued
碰觸到 4. 錯誤訊息會顯示 too many certificates already issued for exact set of domains

Let’s Encrypt 有加入 Certificate Transparency 這個專案,所以所有簽發憑證的紀錄 (log) 都能透過 crt.sh 查詢。

[Trouble shooting] 缺少中繼憑證 造成 curl 顯示錯誤

簡單說明問題,就是當我們 curl 會產生 curl: (60) Peer’s Certificate issuer is not recognized 的錯誤,但使用 browser 直接開啟 URL 卻不會跳任何憑證相關錯誤。

思考邏輯 & 查找過程

  • 瀏覽器打開 URL 不會跳憑證錯誤 > 在沒有私自匯入自簽憑證前提下 ,代表這張證書是經過 Root CA 認可的。

從瀏覽器打開憑證頁面可以看到完整的 Trusted Chain ,而且知道:

  1. 公司買的、伺服器給的憑證是 wildcard certificate (*.xxxx-tech.com)
  2. issuer (又稱簽發者、中繼)Sectigo RSA Domain Validation Secure Server CA
  3. Root CAUSERTrust RSA Certification Authority

browser_certificate

  • 查找到這裡我就想說: 那會不會是 client 端系統的 Root CA 沒有包含 USERTrust RSA Certification Authority ?

(註:天真的以為憑證單靠 Root CA 做驗證,沒有 Full Trusted Chain 的概念)

那就先來更新 ca-cerificates 吧! yum update (更新 package index) yum install ca-certificates 結果系統告訴我們 ca-certificates 套件已是最新版

Umm.. 不然就搞剛一點 確認系統 Root CA 是不是真的有 USERTrust RSA Certification Authority

  • 透過 strace curl https://xxx.xxx-tech.com | less 查看 curl 會去哪個地方抓從放在系統的憑證 (Root CA 公鑰)

(關鍵字 Open),得知路徑為 /etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt,那就看看這個檔案有沒有包含 USERTrust RSA Certification Authority 吧!

1
grep -i -A10 'userTrust RSA' /etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt

grep_ca_in_linux

▲ 經過比對後不僅有,key 的內容完全一樣 (比對公鑰建議採用瞇牌法 看邊 單看開頭不準)

  • 使用 curl -v https//xxx.xxx-tech.com 得知 NSS error -8179 (SEC_ERROR_UNKNOWN_ISSUER) 確認發生原因 是因為 server 沒有給中繼 (issuer) 憑證

  • 後續開了一台 Nginx 起來測試,並且按照 這篇 製作 CA bundle

製作方式:

1
cat your_domain.crt intermediate.crt root.crt >> ca-bundle.crt
  • your_domain.crt: 網路組給我們的 WildCard 憑證
  • intermediate.crt: issuer 中繼憑證 (Sectigo 官網抓)
  • root.crt: Root CA 憑證 (Sectigo 官網抓)

修改後的 Nginx conf 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
upstream xxx {
	server 10.192.x.x:3000;
	server 10.192.x.x:3000;
}

server {
    listen       80;
    server_name  xxx.xxx-tech.com;

    return 301 https://xxx.xxx-tech.com$request_uri;
}

server {
    listen 443  ssl;

    server_name  xxx.xxx-tech.com;

    client_max_body_size 200M;

    ssl_certificate /etc/nginx/ssl/ca-bundle.crt;
    ssl_certificate_key /etc/nginx/ssl/xxx-tech_com.key;
    #ssl_trusted_certificate    /etc/nginx/ssl/ssl-bundle.crt;
    ssl_dhparam /etc/nginx/ssl/dhparams.pem;
    ssl_prefer_server_ciphers on;
    ssl_protocols SSLv2  TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;
    ssl_stapling               off;
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';
    ssl_session_timeout 1d;
    location / {
        proxy_pass http://xxx;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $http_host;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forward-Proto http;
        proxy_set_header X-Nginx-Proxy true;
        proxy_redirect off;
    }
}

▲ reverse proxy, 有 HTTP 轉 HTTPS 功能 (80 轉 443),ssl_certificate 直接遞出完整 trusted chain

參考資料

Q & A

  • (瀏覽器憑證圖) 就有包含整個 trusted chain 了,為什麼還要加中繼 ? 瀏覽器預設也只有 root ca 的憑證不是嗎?

因為 root CA 不是直接簽發給你,他是往下簽給中繼,中繼再簽給你。 server 沒傳中繼憑證就會炸開

  • root ca 簽 issuer ,然後我們跟 issuer 買, issuer 幫我們簽章,那為什麼 browser 沒有炸 ? 而且還抓的到 issuer 的資訊 (瀏覽器憑證圖)

browser 維護的 CA list 除了有包含 Root CA 還有 issuer 的

firefox_cert_manager

其它查找工具

1
2
3
4
5
## openSSL
openssl s_client -showcerts -servername www.example.com -connect www.example.com:443 </dev/null

## drwetter/testssl.sh
docker run --rm -ti drwetter/testssl.sh -S https://www.google.com

[GitHub] drwetter/testssl.sh

特別感謝