2018 年的最後一個月,我終於決定給自己 3 年前申請的域名搞個 SSL 證書,讓網站可以上車 HTTPS。免費、適合個人網站(玩票)的 SSL 證書方案,首選 Let's Encrypt。但是與一開始「一小時搞定」的預期不同,最終完成整件事情花了我一個週末的時間,還是有必要記錄下來。
背景
當然 HTTPS 是大勢所趨,但因為懶自己並沒有真正地搞過 SSL 證書、部署 HTTPS 網站。終於痛下決心部署 HTTPS 是源於自己想要在自己申請的騰訊雲 VPS 主機上部署一套 jenkins 站點,而因為我域名沒有在國內備案,導致使用域名訪問時被騰訊雲限制不可訪問。 從個人開發者手動部署一個可執行網站的方案說起:
- 事先準備
- 一個可通過公網 IP 訪問的雲主機作為伺服器,以我個人雲主機 IP 為例:118.24.121.85
- 在域名商所購買的域名,以我個人域名為例:myan.im
- 通過 ssh 登入到伺服器,手動安裝並啟動 Nginx 服務
- 在域名商網站上新增一條 A 型別的 DNS 記錄,將域名 c.myan.im 指向以上 IP 地址
- 配置成功後,通過 ping 命令即可檢查 DNS 生效
完成第 2 步後,我們就已經可以通過 IP 和埠(預設 80)直接訪問網站,驗證也通過。
curl http://118.24.121.85
複製程式碼
完成第 3 步後,本來期望的表現是訪問 http://c.myan.im
也有同樣的表現,但很不幸:
通過 curl 檢查也可發現,http 請求返回的是 302 頭,Location 頭為 dnspod.qcloud.com/static/bloc…
> curl http://c.myan.im -v
< HTTP/1.1 302
< Location: https://dnspod.qcloud.com/static/block.html?d=c.myan.im
複製程式碼
總算見識到了網站不備案的後果!可見我們把個人網站域名解析到國內主機,沒有問題。但如果網站沒有備案,對不起門都不讓你進?。
至於技術上如何實現,其實這就是典型的 HTTP 劫持問題,站在騰訊雲的角度思考:
- HTTP 請求發起,路由到 Web Server 的伺服器 IP,要經過騰訊雲的網路
- 請求鏈路到達騰訊雲主機前,騰訊雲可以 獲取 HTTP 請求詳情
- 騰訊雲讀取報文 Host,檢查是否備案;如未備案,則直接返回 302 重定向狀態,請求未觸發伺服器相應的 IP:PORT
- 瀏覽器收到 302 並跳轉到 Location 指定地址
問題出現在第 3 步,因為 HTTP 是明文傳輸協議,意味著不光騰訊雲,甚至客戶端到服務端網路鏈路上的任何一個環節,都可以讀取 HTTP 報文內容。
HTTP 劫持案例
- 運營商劫持網頁,插入流量包廣告
- Charles 代理修改請求,修改返回值
- 路由器修改 User-Agent 頭,導致 bug
HTTPS 反劫持
原理
從基本原理上講,HTTPS 可以說是 HTTP 與 SSL/TLS 協議的結合。在進行應用層的報文傳輸前,要通過 TLS 協議建立加密會話。
當然這個圖的握手原理並不是本文的重點,我們關注的是所謂證書在 SSL 連線中的作用。在 Let's Encrypt 的工具 certbot 文件頁,他們這樣解釋什麼是證書:
A public key or digital certificate (formerly called an SSL certificate) uses a public key and a private key to enable secure communication between a client program (web browser, email client, etc.) and a server over an encrypted SSL (secure socket layer) or TLS (transport layer security) connection. The certificate is used both to encrypt the initial stage of communication (secure key exchange) and to identify the server. The certificate includes information about the key, information about the server identity, and the digital signature of the certificate issuer. If the issuer is trusted by the software that initiates the communication, and the signature is valid, then the key can be used to communicate securely with the server identified by the certificate. Using a certificate is a good way to prevent “man-in-the-middle” attacks, in which someone in between you and the server you think you are talking to is able to insert their own (harmful) content.
可以總結為以下幾點:
- 證書使用一個金鑰對(公鑰、私鑰)用來加密客戶端與服務端通訊
- 證書作用包括:通訊初始化階段加密、表明伺服器身份
- 證書包含了公鑰資訊、伺服器身份資訊,以及證書籤發者簽名
除了我們日常在瀏覽器中訪問 HTTPS 網站,可以在點選位址列左側看到正在訪問網站的 SSL 證書,還有這些案例,我們可以感知到證書的存在的:
- 多年前的 12306 需要自己下載證書、安裝證書
- 採用 EAP-PEAP 認證的企業 WiFi 辦公網路,在客戶端(如手機)連上 WiFi 後,作業系統會提醒我們安裝公司企業證書並信任,這樣一來客戶端就可以訪問該證書保護的內網(Intranet)HTTPS 網站
- iOS 安裝企業 App,信任證書後,可以繞過 App Store 安裝應用
獲取自簽名 SSL 證書
從自簽名證書說起:我們可以自己簽名,自己生成一個證書,並在 nginx 部署。
瀏覽器一樣會發起請求,但在與伺服器端建立安全套接層過程中,瀏覽器會認為該證書不可信任。理論上:我們可以把自己的證書,內建在瀏覽器中,就可以解決這個問題。
以下是實踐步驟:
-
生成 key 與證書
openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem 複製程式碼
該命令生成的證書是 CA 根證書,使用
-
檢查證書
openssl x509 -text -noout -in certificate.pem 複製程式碼
-
有了證書檔案、key 檔案後,就可以在 nginx 裡使用如下配置,啟用 HTTPS 支援,並監聽 443 埠:
server { listen 80; listen 443 ssl; server_name fake.myan.im; root /usr/share/nginx/html; ssl on; ssl_certificate /path/to/certificate.pem; # 證書檔案路徑 ssl_certificate_key /path/to/key.pem; # key 檔案路徑 error_page 404 /404.html; location = /40x.html { root html; } location / { } # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } 複製程式碼
其後,使用通過
sudo systemctl restart nginx
命令重啟 nginx 程式。 -
瀏覽器訪問 fake.myan.im 被瀏覽器告警:證書無效
-
scp 下載證書檔案到本地,並雙擊開啟證書檔案,安裝並信任該證書
# remote.host 為遠端主機地址 # ~/local/path 為本地下載目錄 scp remote.host:/path/to/certificate.pem ~/local/path 複製程式碼
使用 Safari 瀏覽器檢視,即發現已經可以訪問了。
如果使用 Chrome 瀏覽器訪問,會發現還是會證書錯誤。這是因為 Chrome 58 後的版本,對自簽名證書的安全性要求更高,需要更復雜的自簽名過程。參考 serverfault.com/questions/8…
真實世界裡的證書申請和使用
現在我們知道,自簽名證書也好,真實有含金量的真實證書也好,歸根結底都只是一個證書檔案。我們需要把它放在我們的主機上,再配置一下 nginx 使用這個證書來 Serve HTTPS 服務。
免費的證書最流行的選擇是 Let's Encrypt,由國內雲服務商也有自己配套的 SSL 證書可使用,但 Let's Encrypt 勝在足夠通用。其限制是預設只有 3 個月有效期,到期後需要重新 renew 操作。
要了解 Let's Encrypt 的工作原理,參考:How It Works - Let's Encrypt - Free SSL/TLS Certificates
關於 Let's Encrypt 常見問題,參考:FAQ - Let's Encrypt - Free SSL/TLS Certificates。
Let's Encrypt 提供的核心服務是證書頒發的基礎服務,很多第三方服務在此基礎上做了封裝,給使用者提供免費證書服務。一個典型的例子是我們很多人耳熟能詳的 Github Pages 服務,Github Pages 支援繫結自定義的域名,啟用域名繫結後無需使用者自行配置,就可以通過 https:// 協議訪問。
要手動獲取證書,並自行部署 HTTPS,Let's Encrypt 推薦使用 eff.org 的命令列工具 Certbot。
安裝 certbot-auto 命令列
TL;DR:不要使用 yum 安裝的包,wget 下載指令碼即可
參考 certbot.eff.org/docs/instal… 說明文件,直接把命令列工具下載到本地。
> wget https://dl.eff.org/certbot-auto
> chmod a+x ./certbot-auto
> ./certbot-auto --help
複製程式碼
使用 certbot 自動模式獲取指定域名證書
certbot 有自動模式,可以幫 nginx,apache 等伺服器自動生成證書檔案並配置網站。
我們以 real.myan.im 為例,示範如何獲取該域名的證書。
-
首先需要配置 http 服務,關鍵配置如下。在 conf.d 目錄下新建檔案,並重啟 nginx 服務
server { listen 80; server_name real.myan.im; root /usr/share/nginx/html; location / { } } 複製程式碼
-
執行 certbot-auto 命令,預設即會使用自動模式進入 CLI 互動
-
~ > ./certbot-auto 複製程式碼
依次選擇指定域名,以下是互動式輸出:
Which names would you like to activate HTTPS for? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1: g.myan.im 2: real.myan.im 3: v.myan.im - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Select the appropriate numbers separated by commas and/or spaces, or leave input blank to select all options shown (Enter 'c' to cancel): 2 Obtaining a new certificate Performing the following challenges: http-01 challenge for real.myan.im Waiting for verification... Cleaning up challenges Deploying Certificate to VirtualHost /etc/nginx/conf.d/real.conf Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1: No redirect - Make no further changes to the webserver configuration. 2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for new sites, or if you're confident your site works on HTTPS. You can undo this change by editing your web server's configuration. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2 Redirecting all traffic on port 80 to ssl in /etc/nginx/conf.d/real.conf - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Congratulations! You have successfully enabled https://real.myan.im You should test your configuration at: https://www.ssllabs.com/ssltest/analyze.html?d=real.myan.im 複製程式碼
-
訪問 real.myan.im 見證奇蹟
certbot 工具自動模式,幫助我們傻瓜式地完成了證書申請、網站所有權驗證、nginx 配置三個過程。
輸出檔案儲存在指定目錄下,其中最重要的證書檔案和私鑰檔案,我們可以儲存到其他地方。
certbot 指令碼執行原理
要了解自動模式的工作原理,我們要來讀一下 log 輸出。
首先自動模式會去讀取 nginx 已有的網站配置,需要依賴我們實現配置好 http 格式的 Web 服務。
自動模式下,有一個自動完成的步驟:
Performing the following challenges:
http-01 challenge for real.myan.im
Waiting for verification...
Cleaning up challenges
複製程式碼
背後所做的事情,是向我們的 real.myan.im
傳送一個特定 URL 的 http 請求,期望得到某隨機值,該數值由 Lets Encrypt 指定。若匹配,則認為申請該證書的操作人員,擁有該網站所有權。
這裡意味著,如果我在國內的主機上使用 certbot 執行以上操作,命令執行到此會出錯。因為所有使用 HTTP 方式訪問的請求,都會被劫持,返回 302 重定向。
此後繼續執行,certbot 就獲得到了證書和金鑰檔案,並完成了指定 nginx 服務的 ssl 配置過程。
certbot 手動模式獲取萬用字元 SSl 證書
如果有多個子域名想要支援 https,一個個地使用萬用字元可以更加方便節省人力。
對於個人來說,萬用字元證書更加適合我們使用手動模式完成。
- 收集證書資訊
- 發起質詢(challenge),以確認操作人是否確實有其所聲稱的域名所有權
- 驗證通過,下發證書
以 *.subdomain.myan.im 為例,我們執行以下命令。
> ./certbot-auto certonly --manual --preferred-challenges=dns --email mamengguang@gmail.com -d *.subdomain.myan.im
複製程式碼
certbot 將會輸出:
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for subdomain.myan.im
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.subdomain.myan.im with the following value:
KCH1u2GEXay6HNkfbfQd5zqP4tvp-sMFtF9UXHmRRpU
Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
複製程式碼
此時,我們需要停下來,登入 DNS 服務控制檯,按照要求新增一條 TXT 型別的 DNS 記錄。
使用以下命令可以驗證
dig -t txt _acme-challenge.subdomain.myan.im
複製程式碼
點選 Enter,繼續向下執行:
Waiting for verification...
Cleaning up challenges
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
...
複製程式碼
到此我們就得到了一個支援萬用字元的 SSL 證書,為了驗證其有效性,我們可以新增多個符合萬用字元的子域名,部署在這臺主機上。
- 新增 DNS 記錄:x.subdomain.myan.im 到 118.24.121.85
- 在 host 機上新增 nginx 配置,
/etc/nginx/conf.d/x.subdomain.myan.im
對比
絕對域名 | 萬用字元域名 | |
---|---|---|
適用環境 | HTTP 網路通暢,無劫持 | A. 子網站無需分開管理 B. 操作者擁有域名 DNS 管理權 |
驗證方式 | 通過 HTTP 請求驗證 | 通過 DNS Record 驗證 |
certbot 操作 | 自動模式,傻瓜式操作 | 手動模式,需要自行切到 DNS 管理平臺新增 |
我們這裡為表達方便所說的所謂“絕對域名”和“萬用字元域名”,實際對應著 SSL 證書的 Common Names 資訊。但 Common Names 值與證書所適配的域名嚴格來說不是一個概念,因為 SSL 證書最新標準支援主題備用名稱(Subject Alternative Name,SAN)特性,該特性允許一個證書可以保護主域名和其他多個額外的備用域名。
一個典型的例子是 Google 的 *.google.com 證書,證書的 Common Names 為 *.google.com 但是它除了支援 google.com 下的各級子域名,也支援 google.cn,google.ca 等 Google 各國家頂級域名甚至 youtube.com 等 Google 旗下域名。
如下圖是 youtube.com 網站的 SSL 證書 *.google.com。
上面的例子中,我們使用 *.subdomain.myan.im 這樣的 “萬用字元證書” 保護 subdomain.myan.im 的子域名,也是因為 SAN 特性的支援。
證書續期
在實際使用 certbot 過程中,考慮 Let's Encrypt 免費證書 90 天的有效期限制,務必還要在到期前自行 renew 證書。
這裡可以配置 crontab 任務自動定時執行 renew 操作,不再贅述,可參考這一篇部落格文章 Let's Encrypt 終於支援萬用字元證書了。
小結
以上是我使用 Let's Encrypt 為個人網站部署 HTTPS 的踩坑記錄,順便還有部署過程中關於 HTTPS 的知識點探索:
- HTTP 劫持
- 通過 HTTPS 反劫持,理解 SSL 證書作用
- 使用 certbot 工具獲取免費 Let's Encrypt 證書方式
篇幅所限內容省去不少細節,僅偏向於基本概念與操作流程。
當然在公司的實際工作中,HTTPS 證書的維護和部署一般都由專業運維人員負責,但作為前後端程式設計師,自己上手部署一下 HTTPS,更有利於讓我們知曉 HTTPS 其然與其所以然。所以 Just Do It 吧!