使用 Lego 申请 Let's Encrypt SSL 证书

TLS/SSL

HTTPS 是基于 SSL/TLS 的 HTTP 协议,在目前的场景下,应该尽可能地提供 HTTPS 访问,它可以提供:

  • 流量加密: 避免第三方截取通信内容
  • 身份验证: 确保用户访问你的域名时,一定是你的站点提供服务,而不是劫持站

(尽管 SSL/TLS 连接的建立可能会略微增加消耗的资源)

系统内置了一些可信的根证书公钥,根证书机构通过私钥为一些大型机构签发他们证书,这些机构通过某种方式验证你的身份后,就可以为你签发属于你得证书。这样,任何人要确保你是你,只需要用公钥解密你用自己私钥签发的内容。

证书申请

一般来说,如果服务数量不多,各大站点都提供了免费一年的证书,用户可以很方便地在云服务控制台进行证书申请、部署

但是,这种一年的证书往往只支持一个域名,如 a.example.com,如果同时包含多个服务 a.example.comb.example.comc.example.com 时,就需要分别进行申请。
要解决这种问题,可以使用泛域名证书 *.example.com。一般来说,免费的泛域名证书使用 Let's Encrypt 申请

Let's EncryptLet's Encrypt

Lego

Let's Encrypt 使用 acme 协议进行控制。显然,用户并不想知道具体的细节,因此有很多工具对该协议进行封装,对外只暴露必须的接口。Lego 就是一个使用 Go 开发的 Let's Encrypt 客户端 —— Le(t's Encrypt) Go

既然懒惰的用户不想知道细节,那么他也不想考虑怎么编译、安装。所以懒惰的用户决定用 Docker

按照前面提到的,证书机构需要对用户身份进行验证,在域名这里,需要验证的就是域名所有权。你需要按照要求,在域名解析内添加 TXT 记录,或是在网站根目录放置指定文件。
这里使用 DNS 记录作为验证方式,因为各大厂商都有 DNS 解析的接口以供使用。


理一下思路,需要做的任务如下:

  1. 使用 Lego 调用云厂商接口更新 DNS 解析,验证域名所有权
  2. 向 Let's Encrypt 申请证书
  3. 把证书部署到各个服务

下面是一个最基本的证书申请命令

首先是使用 goacme/lego Docker 镜像,挂载对应的目录,并将云厂商的 AK&SK 以环境变量形式传入。后面则是设定需要申请的域名及验证方式(由于 example.com 并不匹配 *.example.com,因此还需要单独申请一级域名证书)
如果是第一次申请,需要使用 run 创建用户(使用邮箱),后面直接使用 renew 即可

DNSPOD_API_KEY="123456,12341234123142341234"
ALICLOUD_ACCESS_KEY="LT123412341234"
ALICLOUD_SECRET_KEY="12341234123414"

docker run \
    --rm \
    -e DNSPOD_API_KEY=\"${DNSPOD_API_KEY}\" \
    -e ALICLOUD_ACCESS_KEY=\"${ALICLOUD_ACCESS_KEY}\" \
    -e ALICLOUD_SECRET_KEY=\"${ALICLOUD_SECRET_KEY}\" \
    -u ${UID}:${UID} \
    -v $(pwd):/.lego \
    goacme/lego \
    --email="oyohyee@oyohyee.com" \
    --domains="*.oyohyee.com" \
    --domains="oyohyee.com" \
    --accept-tos \
    --dns="dnspod" \
    --accept-tos \
    --key-type rsa2048 \
    run

更新脚本

目前的两个域名,分别在阿里云和腾讯云,因此需要分开两次验证,下面是一个封装好的脚本,如果需要部署到服务器,可以直接在 deploy 里添加命令(如 scp 替换掉服务器上的证书)
不过貌似各个厂商目前还不支持直接通过接口更新证书,所以还需要手动上传证书并部署到各个服务中
(看上去可能并没有比用服务商提供的一年免费证书方便,但是考虑到很多服务都是通过 Nginx 直接部署的,且服务商上也需要手动执行各个服务的部署操作,因此在 Nginx 后挂载多个服务时,还是有优势的)

#!/bin/sh
DNSPOD_API_KEY="**********"
ALICLOUD_ACCESS_KEY="**********"
ALICLOUD_SECRET_KEY="**********"

debug () {
    if [[ -n $DEBUG ]]; then echo $@; fi
}

ssl () {
    # concat args from 2 to end

    cmd=$(echo "docker run \
        --rm \
        -e DNSPOD_API_KEY=\"${DNSPOD_API_KEY}\" \
        -e ALICLOUD_ACCESS_KEY=\"${ALICLOUD_ACCESS_KEY}\" \
        -e ALICLOUD_SECRET_KEY=\"${ALICLOUD_SECRET_KEY}\" \
        -u ${UID}:${UID} \
        -v $(pwd):/.lego \
        goacme/lego \
        ${@:2} \
        --accept-tos \
        --key-type rsa2048 \
        ${1}" | tr -s " ")
    debug $cmd
    `$cmd`
}

help() {
    echo "$0 - tool to generate ssl certification"
    echo " use \"$0 run\" or \"$0 renew\""
    echo ""
    echo "    run               lego run"
    echo "    renew             lego renew"
    echo ""
    echo "    -d --debug        debug mode"
    exit 0
}

main() {
    # oyohyee.com
    ssl $1 \
        --email="oyohyee@oyohyee.com" \
        --domains="*.oyohyee.com" \
        --domains="oyohyee.com" \
        --dns="dnspod" 
        
    # ohyee.cc
    ssl $1 \
        --email="me@ohyee.cc" \
        --domains="*.ohyee.cc" \
        --domains="ohyee.cc" \
        --dns="alidns"
}

deploy() {
    echo ""
    echo "Update ssl in aliyun:"
    echo "    https://yundun.console.aliyun.com/?&p=cas#/certExtend/upload"
    echo ""
    echo "Update ssl in tencent cloud:"
    echo "    https://console.cloud.tencent.com/ssl"
    echo ""
    echo "Update ssl in qiniu cloud:"
    echo "    https://portal.qiniu.com/certificate/ssl#cert"
    echo ""
    echo "Update ssl in server:"
    echo "    scp ./certificates/_.* ubuntu@vps.oyohyee.com:/etc/nginx/ssl"
    echo ""
}

# parse flags
command=""
while [[ $# -gt 0 ]]; do
    key="$1"
    shift
    case $key in
        -d|--debug) DEBUG=1         ;;
        run)        command="run"   ;;
        renew)      command="renew" ;;
        deploy)     command="deploy" ;;
        *)          help            ;;
    esac
done

if [[ -z $command ]]; then help; fi

debug "DEBUG mode"
debug "command: $command"

if [[ $command -ne "deploy" ]]; then
    deploy
else
    main $command
    deploy
fi

Nginx

如果有多个服务,可以考虑使用 Nginx 的 include 来统一引入根证书,每次更新证书后,只需要覆盖证书文件并重启 Nginx 即可