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

昨天尝试了用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_certificate和ssl_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不要那么快被墙。

在Docker容器环境中用Let's Encrypt部署HTTPS》上有1条评论

  1. ```
    ssl_certificate /etc/letsencrypt/your.domain1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/your.domain1.com/privkey.pem;
    ```
    中的路径默认应该是
    ```
    ssl_certificate /etc/letsencrypt/live/your.domain1.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your.domain1.com/privkey.pem;
    ```

发表评论

电子邮件地址不会被公开。 必填项已用*标注