容器

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

虽然个人博客不需要像电商、金融等交易型网站那样重视安全性,但在这个互联网时代,HTTPS 就像是网站的“门面”,百度、谷歌等搜索引擎也已经将 HTTPS 网站权重加大,开启https的网站更容易帮助站点搜索引擎优化和排名。

此外,HTTPS 会验证网站的真实身份,确保数据来源的正确,并且能增强用户对网站信任:Chrome、Firefox 等浏览器也会对 HTTPS 网站显示安全标志,HTTPS 不仅保护了网站数据安全,也解决了浏览器时不时跳出“不受信任”警告提示的尴尬场景,更容易获取用户的信任和提高浏览体验,从而在一定程度上提高了博客粉丝量。

昨天尝试了用Let’s Encrypt给自己的博客部署了HTTPS,感觉这个服务真的是非常方便。网上有很多关于Let’s Encrypt的文章,不过本站因为部署在docker容器中,关于这种架构的文章不多,我把自己的思路写下来吧。

先来说点基础的话题。

HTTPS可以提供全面的密码学保护,换句话说,访问HTTPS网站时,用户和网站之间传输的数据不会被第三方窃取和监听。这里的第三方包括黑客、运营商、监管部门,有你想得到的也有你想不到的。

HTTP 请求过程中,客户端与服务器之间没有任何身份确认的过程,数据全部明文传输,“裸奔”在互联网上,所以很容易遭到黑客的攻击。

图中可以看到,客户端发出的请求很容易被黑客截获,如果此时黑客冒充服务器,则其可返回任意信息给客户端,而不被客户端察觉,所以我们经常会听到一词“劫持”。

上图是 HTTPS 握手流程,可以看到相比 HTTP,HTTPS 传输更加安全。

  • 所有信息都是加密传播,黑客无法窃听。
  • 具有校验机制,一旦被篡改,通信双方会立刻发现。
  • 配备身份证书,防止身份被冒充。

HTTPS是基于公钥密码的,简单说就是服务器提供一个公钥,用户用这个公钥加密数据后发给服务器,然后服务器用自己的私钥解密。这里面有一个问题,即如何确定服务器的公钥是真的,如果第三方拦截并伪造服务器的公钥,用户用第三方的假公钥加密数据,那么这些数据就会被第三方解密并窃取。

为了避免这样的问题,就出现了证书。证书本身是个数字签名,就像给服务器的公钥盖个公章,用户看到公章就知道公钥是真的了。但是,这个问题没有根本解决,怎么知道公章本身是不是真的呢?因为公章的本质是数字签名,于是浏览器里面内置了一份“可信公章清单”,凡是在这份清单里的签名都认为是可信的。

因此,原则上说所有人都可以自己颁发证书,但自己颁发的证书不被浏览器信任(不在可信公章清单里),浏览器就会报错(比如12306网站遇到的错误)。这时有三种解决方法,第一是让用户强制认为自己的证书是可信的(12306的做法),第二是请在可信公章清单里的机构给自己的网站发证书,第三是让浏览器把自己列到可信清单里去(即成为可信的CA)。

第一种做法简单粗暴,风险也很大,因为用户安装的证书如果是伪造的,那在访问伪造的钓鱼网站时,就会误认为访问了可信的网站。第三种做法很难,因为成为可信的CA需要严格的条件。大部分网站用的第二种做法,即请可信CA来颁发证书。

但是CA作为盈利机构不会免费给你提供这种服务,因此请CA颁发证书是要收费的,这个费用现在越来越便宜,但还是不太便宜。此外,签发和更新证书都要通过人工完成,这对于越来越要求自动化运维的互联网行业成了一个绊脚石。

于是2015年诞生了一个叫做Let’s Encrypt的项目,这个项目的目标是实现全互联网加密。先不管这个目标是否现实,从技术层面上,Let’s Encrypt推出了两个革新:第一是免费签发可信的证书,第二是实现证书签发和更新的完全自动化

传统的CA在签发证书的时候必须验证申请人的身份,每个证书只能绑定特定的域名使用,换句话说,你得证明你拥有这个域名的控制权。Let’s Encrypt可以采用多种方式自动完成验证,有多种客户端程序支持Let’s Encrypt的方案,还提供了各种主流服务器的插件。不过本站的环境不是典型的环境,关于各种典型环境下的部署方法请参见certbot(官方推荐的客户端)的官方教程。

总结一下:而 HTTPS 主要从下面三个方面确保传输的安全。

1. 身份认证

传输之前首先通过数字证书来确认身份,各大 CA 厂商干的就是这个事情。这里涉及到一个名词:数字证书。数字证书分为公钥和私钥,CA 厂商会用自己的私钥来给证书申请者签发一套包含私钥和公钥的客户证书,客户的公钥证书谁都可以获取,里面包含了客户站点和证书的基本信息,用来确保访问者访问的就是他想要访问的站点。

这个证书不可以伪造吗?答案是真的不可以。

  • 原因一:系统早已内置了各大 CA 厂商的公钥来校验证书是否是对应的站点的证书,如果不是,就会给出证书不匹配的提示,除非你给别人的设备强行植入假的 CA 公钥。
  • 原因二:这个证书是 CA 厂商通过哈希并加密得到的,基本无法逆向破解并伪造一个新的,除非是你黑进 CA 获取了 CA 的私钥,那这家 CA 也基本可以倒闭了。

2. 数据保密
数据保密包括会话秘钥传输时候的保密和数据的加密传送。具体的加密方式就不再赘述了。

3. 数据完整
身份认证成功后,到了数据加密传输的阶段,所有数据都以明文(HTTP)收发,只不过收发的是加密后的明文。这时候也遇到了一个问题,虽然中间人很难破解加密后的数据,但是如果他对数据进行了篡改,那该怎么办?此时加密套件验证数据一致性的哈希算法就派上用场了,哈希算法有多种,比如 MD5 ,SHA1 或者 SHA2 等,上面举例的加密套件使用的是 SHA2 中的 SHA256 来对数据进行哈希计算。这样就使得任何的数据更改都会导致通信双方在校验时发现问题,进而发出警报并采取相应的措施。

SSL证书部署

目前 SSL 证书的应用已经非常普及,网站部署SSL 证书也非常的简单,只需三步即可完成:

  • 申请 SSL 证书,建议你选择免费的 SSL 证书即可,足够满足博客网站的证书需求。
  • 部署 SSL 证书,各平台部署方式不尽相同,你可以按照实际情况操作配置。
  • 将网站所有 HTTP 链接更换为 HTTPS。

先介绍一下我的环境配置。服务器是Digital Ocean的一台VPS,系统是CoreOS,所以这是一台只能跑容器的服务器。服务器上运行了3个网站,每个网站都是一个单独的容器,这些容器是基于PHP官方镜像(with Apache)定制的,另外有一个反向代理容器(基于nginx官方镜像)负责根据域名将访问分配到每个网站。

由于CoreOS只能运行容器,因此无法在CoreOS中直接安装certbot,还好certbot有个官方的docker镜像,我们可以直接pull:

下载letsencrypt镜像(不带tag标签则表示下载latest版本)

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

执行完上面命令以后,按照命令提示,一步一步的输入信息,确认就好了。

成功以后在主机的/etc/letsencrypt目录能看到生成的证书,在/etc/letsencrypt/live/http://www.example.com下生成四个文件。

    cert.pem: 服务端证书

    chain.pem: 浏览器需要的所有证书但不包括服务端证书,比如根证书和中间证书

    fullchain.pem: 包括了cert.pem和chain.pem的内容

    privkey.pem: 证书的私钥 

    一般导入fullchain.pem和privkey.pem就可以

解释一下这里都做了什么事。首先-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

// standalone表示容器内部会启动一个webserver来验证域名

证书签发之后,会存放到/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容器。

另外运行这个命令,需要把wordpress容器停止下来,把443端口释放出来。否则会出错。

停止wordpress容器以后,再次执行上面的命令,可以看出,因为证书还没到期,没有生成新的证书。

除了standalone方式验证之外,还可以使用wwwroot方式来做验证,但在我的环境中,nginx容器只是反向代理,本身没有wwwroot,因此standalone方式比较简单,当然缺点是每次签发和更新证书都要先停止nginx容器,这会造成网站服务中断。如果需要保证服务不中断,可以为nginx容器单独配一个验证用的wwwroot。

转载:https://www.jianshu.com/p/5afc6bbeb28c