The Issue of the Day Before

讓 HAProxy 也可以 Let's Encrypt

越來越多的服務要求使用 https, 在網路上混的 HAProxy 當然也要聲援一下. HAProxy 自從 1.5 版後就支援 SSL. 但每次到期要手動申請 renew 也是一項不人性的作業. 不出意外, 只要有這種長時間後才要重複作業的工作, 一定會有 莫非 的出現. 還好關於申請 ssl 證書有人推出了自動化服務, 特別它是免費的. Let’s Encrypt 在 2015 年底正式提供這個服務. 所以我們的目標便是將兩者整合在一起, 期快樂無比.

Let’s Encrypt 首頁大大寫著 Let’s Encrypt is a free, automated, and open Certificate Authority. 不知你最先注意到的是 free, open 還是 automated . free 跟 open 應該是這個網路網路時代的顯學. 但很多人沒注意到的是 automated(自動化) 才是幕後功臣.

然而畢竟是自動化, 總不能要自己還要複製貼上的輸入資料吧. 所以, 要讓你的 Let’s Encrypt 申請自動化, 首先你的 Web Server 要支援 Automatic Certificate Management Environment (ACME 目前還是草稿階段).

Diagram

而現在要讓 HAProxy 支援 ACME, 但(臣妾)辦不到(目前). 或許你可以利用崁入式語言 Lua 為他寫一個支援 ACME 的程式. 使用 haproxy -vv|grep Lua 檢查你的 HAproxy 是否支援 Lua. 不想如此麻煩, 就必須另外使用工具. Let’s Encrypt 推薦的首選工具是 certbot.

現在我們要借用 certbot 來達成 HAProxy 支援 ACME 的功能.

Diagram

首先下載 certbot, 你可以簡單的使用 sudo apt-get install certbot 下載安裝現有的封裝版本. 或註冊 certbot 的個人套件庫取得最新的版本.

$ sudo apt-get update
$ sudo apt-get -y install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get -y install certbot

理所當然的你應該有一個合法的註冊網域和正在執行的 HAProxy 伺服器.

要求新授權證書

因為是要求新授權, 理所當然認為你現在沒有證書, 也就沒有辦法使用 https, 所以 Let’s Encrypt 是透過 port:80 與 Client 溝通. 我們可以將 HAProxy 設定成將透過 port:80 進來且路徑是 /.well-known/acme-challenge/request 傳給 certbot. 並選擇一個空的 port numbercertbot 使用. 這裡使用的是 port:8888.

/etc/haproxy/haproxy.cfg
frontend http
    bind    *:80

    acl acme path_beg /.well-known/acme-challenge/
    use_backend acme-backend if acme

backend acme-backend
    server certbot 127.0.0.1:16523

改好 HAProxy 的設定檔後, 重新啟動 HAProxy.

sudo certbot certonly --standalone -d your.domain.name \
  --non-interactive --agree-tos \
  --email yourAdmin@your.domain.name \
  --http-01-port=16523

這裡的 --http-01-port=16523 要跟 HAProxy 設定檔中 server certbot 127.0.0.1:16523port 相同.

成功會出現

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/your.domain.name/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/your.domain.name/privkey.pem
   Your cert will expire on yyyy-mm-dd. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"

之類的說明文字. 如果有出現錯誤, 請再依說明處理.

e.q.
IMPORTANT NOTES:
 - The following errors were reported by the server:

將產生的兩個檔案 /etc/letsencrypt/live/your.domain.name/fullchain.pem/etc/letsencrypt/live/your.domain.name/privkey.pem 合併成一個檔案. 要注意合併的先後順序.

e.q./etc/ssl/your.domain.name/your.domain.name.pem
$ sudo mkdir -p /etc/ssl/

$ sudo cat /etc/letsencrypt/live/your.domain.name/fullchain.pem \
/etc/letsencrypt/live/your.domain.name/privkey.pem \
| sudo tee /etc/ssl/your.domain.name/your.domain.name.pem

將該檔案設給 HAProxyssl crt.

因為現有有了 ssl 證書, 所以我們可以將 HAProxy 監聽 port 443 的設定加上. 同時現在也可以對 Let’s Encrypt 要求 renew 了. 當然因為是 renew 所以 ACME 是透過 443 port 來要求的.

/etc/haproxy/haproxy.cfg
frontend https
    bind    *:443 ssl crt /etc/ssl/your.domain.name/your.domain.name.pem

    acl acme path_beg /.well-known/acme-challenge/
    use_backend acme-backend if acme

改好 HAProxy 的設定檔後, 重新啟動 HAProxy.

要求更新授權證書

certbot 要求更新證書是很簡單的. 只要執行 certbot renew, 其他參數都會延續之前要求新授權證書時的設定. 但因為我們現在是透過 HAProxy 來轉給 certbot, 所以跟之前監聽 port 443 一樣, 必須把 443 port 下, 路徑是 /.well-known/acme-challenge/ 轉給 certbot 處理.

Let’s Encrypt 發的證書, 有效是 90 天, 而 certbot 並不是系統常駐程式, 而是做完工作就結束了. 所以我們必須在到期前, 再執行一次 certbot renew, 取得新證書, 並讓 HAProxy 載入新證書, 才不會過期.

$ sudo /usr/bin/certbot renew
  --force-renewal \ // (1)
  --tls-sni-01-port=16523 \ //(2)
  --renew-hook <renew 後要執行 service haproxy reload> // (3)
  1. --force-renewal 要求強迫更新證書, 提前強迫更新證書可以避免有證書斷層.

  2. --tls-sni-01-port=<port> 指示 certbotHAProxy 導過來的 port 工作.

  3. --renew-hook 指示當 renew 成功後要執行的 script. 也就是合併檔案, 重新啟動 HAProxy.

同樣的將產生的兩個檔案合併, 然後重新啟動 HAProxy.

最後將上述指令設定給 crontab 來定時執行, 即可. 例如, 在每個月的1號凌晨1點執行 renew.sh.

*m h dom mon dow   command
0 1 1 * * renew.sh

crontab renew 成功後要執行的 script.

hook.sh
domain="api.accunix.net"

fullchain="/etc/letsencrypt/live/${domain}/fullchain.pem"
privkey="/etc/letsencrypt/live/${domain}/privkey.pem"
target="/etc/ssl/${domain}/${domain}.pem"

if sudo [ -f ${fullchain} ] && sudo [ -f ${privkey} ]; then
    dir="/etc/ssl/${domain}"
    if sudo [ ! -d ${dir} ]; then
        sudo mkdir -p ${dir}
    fi
    sudo cat ${fullchain} ${privkey} | sudo tee ${target}

    sudo service haproxy reload
fi

crontab 要執行的 script.

renew.sh
#!/bin/bash

cwd=$(cd `dirname $0`; pwd -P)

port="16523"
hook="${cwd}/hook.sh"

sudo /usr/bin/certbot renew --force-renewal --tls-sni-01-port=${port} --renew-hook ${hook}
閱讀在雲端