背景
Let's Encrypt是由互联网安全研究小组(ISRG)在2015年推出一个HTTPS证书的免费解决方案,旨在提供一个免费开放的CA。配合电子前哨基金会(EFF)提供的自动化脚本certbot,只需一条命令就能够申请/更新HTTPS证书。
Let's Encrypt签发的证书有效期三个月,需要频繁地更新。虽然certbot提供了一键更新证书的功能,但其工作方式决定了它很难应用在容器集群中。
- HTTP验证方式在更新证书时会在本地生成一个验证文件,Let's Encrypt服务器通过HTTP路径访问该验证文件。容器集群中服务都是多点部署,而certbot只能在其中一个节点运行,无法保证验证请求总是被导入certbot运行的节点
- 使用DNS验证没有上述问题,其缺点在于难以自动化,在更新过程中需要人为创建对应的DNS记录
以下是我们针对这两点问题探索的一些解决方案。
单域名证书
certbot支持HTTP和DNS两种方式来验证域名所有者。
- HTTP
Let's Encrypt服务端以HTTP方式访问域名下指定的验证文件
- DNS
Let's Encrypt服务端查看域名下是否有创建指定的TXT记录
certbot对HTTP验证的支持度更好,几乎是开箱机用。但使用HTTP就会遇到上面提到的问题1,如何将流量导入我们指定的节点呢? 我们最终使用SLB的转发策略加虚拟服务器组来解决这个问题。
第一步,在SLB的80端口监听中,添加一条特殊的转发策略,匹配该规则的URL转发至一个特殊的虚拟服务器组,这个组只有一个节点,指向运行certbot的容器。
- 假设域名为abc.example.com
- URL设置为
/.well-known/acme-challenge/
,Let's Encrypt的验证请求总是以这个地址开头 - 虚拟服务器组cert-refresh-group指向节点ecs-refresh的30080端口
第二步,在ecs-refresh运行容器:
docker run -it --rm -p 30080:80 -v ~/letsencrypt:/etc/letsencrypt certbot-image bash -c "\
service nginx start && \
sleep 30 && \
source /root/certbot/venv/bin/activate && \
certbot renew -a webroot --webroot-path=/var/www/html -d abc.example.com"
-
~/letsencrypt
是节点上存放证书的目录 - image
certbot-image
是一个安装了certbot和nginx的docker image,Dockerfile如下:
From ubuntu:16.04
## install certbot
RUN apt-get install -y git && \
cd /root && \
git clone https://github.com/certbot/certbot.git && \
cd /root/certbot && \
./certbot-auto --os-packages-only --non-interactive && \
./tools/venv.sh
## install nginx
RUN apt-get update && \
add-apt-repository -y ppa:nginx/stable && \
apt-get update && \
apt-get install -y nginx && \
chown -R www-data:www-data /var/lib/nginx && \
mkdir -p /var/www/html
ADD nginx.conf /etc/nginx/nginx.conf
- nginx需要监听80端口,并且设置root为
/var/www/html
。nginx.conf如下:
server {
listen 80;
root /var/www/html;
location ~ /.well-known/acme-challenge {
allow all;
}
}
- 启动nginx后等待30秒是为了等待SLB的健康检查生效,从而能够导入流量
- 由于Dockerfile中使用virtualenv安装certbot,所以运行certbot前需要先设初始化环境:
source /root/certbot/venv/bin/activate
这条命令开始更新证书:
certbot renew -a webroot --webroot-path=/var/www/html -d abc.example.com
- certbot将验证文件
<authorization_file>
放入目录/var/www/html
- Let's Encrypt访问
http://abc.example.com/.well-known/acme-challenge/<authorization_file>
- 如果
<authorization_file>
内容通过验证,则签发新的证书
第三步,删除第一步配置的转发策略和虚拟服务器组
我们将这三步动作集成起来,放到chronos中每月运行一次。certbot会判断证书的到期时间,如果当证书有效期小于30天时才会执行更新操作。
通配型域名证书
Let's Encrypt对通配型的支持已于半月前(2018年3月13日)上线,申请通配型证书有以下几点需要注意:
- 由于通配型证书刚上线不久,certbot的stable版本还不支持,需要安装0.22或以上版本的certbot
- 默认情况下,certbot会请求Let's Encrypt的ACME v1接口,而v1接口不支持通配证书,需要使用参数
--server https://acme-v02.api.letsencrypt.org/directory
指定使用v2接口 - 申请通配型证书只能使用DNS验证方式,之前基于HTTP的验证方案不再有效
certbot certonly --manual -d '*.example.com' --server https://acme-v02.api.letsencrypt.org/directory
运行这条命令即可申请通配证书,中途会提示添加DNS TXT记录,添加之后回车继续。
Please deploy a DNS TXT record under the name
_acme-challenge.example.com with the following value:
v3YCTNw96mfgqMuQA80-McBf1MLvg3G4vXUDnLmlbz
Before continuing, verify the record is deployed.
整个流程还是非常简单的,相比HTTP验证方式不用配置SLB来转发验证请求,其缺点在于不容易自动化。certbot中虽然集成了一些DNS验证的插件,但只支持国外几个DNS服务商:
- certbot-dns-cloudflare
- certbot-dns-cloudxns
- certbot-dns-digitalocean
- certbot-dns-dnsimple
- certbot-dns-dnsmadeeasy
- certbot-dns-google
- certbot-dns-luadns
- certbot-dns-nsone
- certbot-dns-rfc2136
- certbot-dns-route53
如果使用的DNS服务商恰好名列其中,那就省事不少,也算是开箱即用。小博无线使用DNSPod管理DNS,要将这套流程自动化起来还需要额外的工作。我们参照已有的插件,写了一个DNSPod的验证插件certbot-dns-dnspod
。
class Authenticator(dns_common.DNSAuthenticator):
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
self.credentials = None
def _perform(self, domain, validation_name, validation):
self._get_dnspod_client().add_txt_record(domain, validation_name, validation, self.ttl)
def _cleanup(self, domain, validation_name, validation):
self._get_dnspod_client().del_txt_record(domain, validation_name, validation, self.ttl)
def _get_dnspod_client(self):
return _DnspodClient(self.conf('credentials'))
插件开发不算麻烦,只需要实现对应的_perform()
和_cleanup()
方法。只是目前certbot对DNS插件的引用都是hard code在代码中,需要修改certbot的代码才能使新插件正常工作。有四个文件包含了对插件的引用代码:
- certbot/cli.py
- certbot/constants.py
- certbot/plugins/disco.py
- certbot/plugins/selection.py
安装插件
certbot目前的stable版本还不支持通配证书,所以我们使用v0.22.0分支安装certbot,插件certbot-dns-dnspod
也在virtualenv环境中安装。
## install certbot
RUN apt-get install -y git && \
cd /root && \
git clone https://github.com/certbot/certbot.git && \
cd /root/certbot && \
git checkout v0.22.0
ADD modified_code/certbot/cli.py certbot/cli.py
ADD modified_code/certbot/constants.py certbot/constants.py
ADD modified_code/certbot/plugins/disco.py certbot/plugins/disco.py
ADD modified_code/certbot/plugins/selection.py certbot/plugins/selection.py
RUN ./certbot-auto --os-packages-only --non-interactive && \
./tools/venv.sh
## install certbot-dns-dnspod
RUN bash -c 'source /root/certbot/venv/bin/activate && \
cd /root/certbot-dns-dnspod && \
python setup.py install'
申请证书
在申请证书时,我们将域名指定为*.example.com
,证书支持所有子域名,但不包含根域名(example.com
)。如果希望也支持根域名,将根域名也一同放入参数-d
:
docker run -it --rm -v ~/letsencrypt:/etc/letsencrypt certbot-image bash -c "\
certbot certonly \
--dns-dnspod \
--dns-dnspod-credentials ~/access_token.json \
--dns-dnspod-propagation-seconds 30 \
-d 'example.com,*.example.com'"
更新证书
docker run -it --rm -v ~/letsencrypt:/etc/letsencrypt certbot-image bash -c "\
certbot renew \
--dns-dnspod \
--dns-dnspod-credentials ~/access_token.json \
--dns-dnspod-propagation-seconds 30 \
-d 'example.com,*.example.com'"
Rate Limits
Let's Encrypt严格限制了签发证书的次数,一个注册域名(*.example.com)每周最多签发20个证书,一个子域名(abc.example.com)每周最多签发5个证书。
我们实践发现,即使是验证错误也会计算一次。所以在正式运行以前,一定加上--staging
参数使用Let's Encrypt的测试接口。测试接口只用于测试证书的签发流程,其签发的证书为无效证书。
如果是申请通配证书,使用了--server
参数,便不能再使用--staging
参数,将--server
地址指定为https://acme-staging-v02.api.letsencrypt.org/directory
即可。