在Docker容器環境中用Let's Encrypt部署HTTPS

周自恆發表於2016-07-05

昨天嘗試了用Let's Encrypt給自己的部落格部署了HTTPS,感覺這個服務真的是非常方便。網上有很多關於Let's Encrypt的文章,不過本站因為部署在docker容器中,關於這種架構的文章不多,我把自己的思路寫下來吧。

先來說點基礎的話題。

HTTPS可以提供全面的密碼學保護,換句話說,訪問HTTPS網站時,使用者和網站之間傳輸的資料不會被第三方竊取和監聽。這裡的第三方包括黑客、運營商、監管部門,有你想得到的也有你想不到的。

HTTPS是基於公鑰密碼的,簡單說就是伺服器提供一個公鑰,使用者用這個公鑰加密資料後發給伺服器,然後伺服器用自己的私鑰解密。這裡面有一個問題,即如何確定伺服器的公鑰是真的,如果第三方攔截並偽造伺服器的公鑰,使用者用第三方的假公鑰加密資料,那麼這些資料就會被第三方解密並竊取。

為了避免這樣的問題,就出現了證照。證照本身是個數字簽名,就像給伺服器的公鑰蓋個公章,使用者看到公章就知道公鑰是真的了。但是,這個問題沒有根本解決,怎麼知道公章本身是不是真的呢?因為公章的本質是數字簽名,於是瀏覽器裡面內建了一份“可信公章清單”,凡是在這份清單裡的簽名都認為是可信的。

因此,原則上說所有人都可以自己頒發證照,但自己頒發的證照不被瀏覽器信任(不在可信公章清單裡),瀏覽器就會報錯(比如12306網站遇到的錯誤)。這時有三種解決方法,第一是讓使用者強制認為自己的證照是可信的(12306的做法),第二是請在可信公章清單裡的機構給自己的網站發證照,第三是讓瀏覽器把自己列到可信清單裡去(即成為可信的CA)。

第一種做法簡單粗暴,風險也很大,因為使用者安裝的證照如果是偽造的,那在訪問偽造的釣魚網站時,就會誤認為訪問了可信的網站。第三種做法很難,因為成為可信的CA需要嚴格的條件。大部分網站用的第二種做法,即請可信CA來頒發證照。

但是CA作為盈利機構不會免費給你提供這種服務,因此請CA頒發證照是要收費的,這個費用現在越來越便宜,但還是不太便宜。此外,簽發和更新證照都要通過人工完成,這對於越來越要求自動化運維的網際網路行業成了一個絆腳石。

於是2015年誕生了一個叫做Let's Encrypt的專案,這個專案的目標是實現全網際網路加密。先不管這個目標是否現實,從技術層面上,Let's Encrypt推出了兩個革新:第一是免費簽發可信的證照,第二是實現證照籤發和更新的完全自動化

傳統的CA在簽發證照的時候必須驗證申請人的身份,每個證照只能繫結特定的域名使用,換句話說,你得證明你擁有這個域名的控制權。Let's Encrypt可以採用多種方式自動完成驗證,有多種客戶端程式支援Let's Encrypt的方案,還提供了各種主流伺服器的外掛。不過本站的環境不是典型的環境,關於各種典型環境下的部署方法請參見certbot(官方推薦的客戶端)的官方教程。

先介紹一下我的環境配置。伺服器是Digital Ocean的一臺VPS,系統是CoreOS,所以這是一臺只能跑容器的伺服器。伺服器上執行了3個網站,每個網站都是一個單獨的容器,這些容器是基於PHP官方映象(with Apache)定製的,另外有一個反向代理容器(基於nginx官方映象)負責根據域名將訪問分配到每個網站。

由於CoreOS只能執行容器,因此無法在CoreOS中直接安裝certbot,還好certbot有個官方的docker映象,我們可以直接pull:

docker pull quay.io/letsencrypt/letsencrypt:latest

執行這個映象就可以申請簽發證照,在執行之前首先確保你要申請證照的域名能直接解析到你當前這臺伺服器上。

docker run -it --rm -p 80:80 -p 443:443 \
    -v /etc/letsencrypt:/etc/letsencrypt \
    quay.io/letsencrypt/letsencrypt auth

解釋一下這裡都做了什麼事。首先-it選項表示開啟互動輸入,因為在申請證照的過程中需要使用者輸入一些資訊;--rm選項表示容器執行結束後自動刪除,因為這是一個一次性容器;兩個-p選項表示對映主機的兩個埠,因為certbot需要通過這兩個埠來做驗證,這裡需要注意的是,我的nginx容器已經佔用了這兩個埠,因此在申請證照之前,需要先停止nginx容器;-v選項表示對映磁碟資料卷,因為certbot會將所有資訊儲存在/etc/letsencrypt目錄中,我們需要讓這個目錄的內容持久化並可以從主機以及其他容器(主要是nginx容器)訪問它。

第一次申請證照需要註冊賬號,過程很簡單,先同意裡面的協議,然後輸入一個郵件地址作為ID就可以了(不需要驗證這個郵箱,只是一個ID),以後執行時賬號會儲存在本地,就不需要再輸入郵件地址了。接下來需要選擇驗證方式,這裡選擇Temporary web server方式,也就是說讓certbot自己啟動一個臨時Web伺服器(因此需要開放80和443埠)完成驗證。最後輸入你要簽發證照的域名,程式自動完成認證之後,證照就簽發好了。

如果你不喜歡這樣一步一步的方式(比如我就不喜歡),可以在命令列裡提供所有的引數,這樣就一步搞定了:

docker run --rm -p 80:80 -p 443:443 \
    -v /etc/letsencrypt:/etc/letsencrypt \
    quay.io/letsencrypt/letsencrypt auth \
    --standalone -m someone@email.com --agree-tos \
    -d your.domain1.com -d your.domain2.com

證照籤發之後,會存放到/etc/letsencrypt目錄中,剛才我們對映了資料卷,因此可以直接從宿主機中看到這個目錄中的內容,其中證照位於/etc/letsencrypt/live/your.domain1.com中。接下來需要讓nginx容器也能夠讀取這些證照,方法放簡單,把這個目錄對映給nginx容器就可以了:

docker run --name nginx -p 80:80 -p 443:443 \
    -v /etc/nginx/conf.d:/etc/nginx/conf.d \
    -v /etc/letsencrypt:/etc/letsencrypt \
    nginx

第一個-v是存放nginx配置檔案的目錄,第二個-v就是存放證照的目錄,接下來我們在網站的配置檔案裡把證照配上去:

server {
    listen 443 ssl;
    server_name your.domain1.com;

    ssl_certificate /etc/letsencrypt/your.domain1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/your.domain1.com/privkey.pem;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;

    location / {
        proxy_pass http://172.17.0.1:8001;
    }
}

server {
    listen 80;
    server_name your.domain1.com;
    return 301 https://$host$request_uri;
}

配置檔案主要就是ssl_certificatessl_certificate_key兩行,第一行是提供給客戶端的公鑰(證照),第二行是伺服器用來解密客戶端訊息的私鑰(私鑰不會,也不應該在網路上傳輸)。後面第二個server塊是將直接用HTTP的訪問重定向到HTTPS連線上。

修改好配置檔案之後重啟nginx容器,順利的話網站就可以通過HTTPS訪問了,可以通過瀏覽器看一下證照資訊,頒發者是Let's Encrypt Authority X3,它的根CA是DST,即IdenTrust,這是一個為銀行和金融提供證照的可信CA,通過和IdenTrust交叉驗證,Let's Encrypt的證照可以在各種瀏覽器上確保可信。不過Let's Encrypt簽發的證照是短效證照,有效期只有3個月,但沒關係,我們可以通過一個簡單的命令對證照進行更新,同樣是通過docker容器來執行:

docker run --rm -p 80:80 -p 443:443 \
    -v /etc/letsencrypt:/etc/letsencrypt \
    quay.io/letsencrypt/letsencrypt renew \
    --standalone

執行這個命令時,certbot會自動檢查確認證照有效期,如果過期時間在一個月之內,就會自動更新。在CoreOS中,由於沒有Cron,我們需要通過systemd的timer來做定時排程,比如每個月執行一次這個renew任務就可以了,不過記得執行之前先停止nginx容器,執行之後再啟動nginx容器。

除了standalone方式驗證之外,還可以使用wwwroot方式來做驗證,但在我的環境中,nginx容器只是反向代理,本身沒有wwwroot,因此standalone方式比較簡單,當然缺點是每次簽發和更新證照都要先停止nginx容器,這會造成網站服務中斷。如果需要保證服務不中斷,可以為nginx容器單獨配一個驗證用的wwwroot。

好了,祝大家加密愉快,祝Let's Encrypt不要那麼快被牆。

相關文章