本博客现已启用全站 HTTPS 加密通讯

新的改变

这是本博客改用 hexo 引擎和更换主题样式之后的第三次重大更新。现在整个站点 bitmingw.com 以及其引用的所有资源已经强制使用了 HTTPS 加密传输。即使用户最初使用不安全的 HTTP 协议,也会被 301 重定向为 HTTPS 协议。





文章的后续部分会详细讲解本站点 HTTPS 通讯的设计与实现,供各位博主参考。


架构设计

这个博客站点是我利用 GitHub Pages 于 2014 年创建的。如今的 GitHub Pages 功能强大,自定义域名和 HTTPS 加密可谓是其两大特性。但恰恰由于一些技术难题,自定义域名和 HTTPS 这两个功能是互斥的,启用了其中的一个就无法使用另一个。我使用自己的域名已经有很长的时间,不可能弃而不用。若再想提供加密传输,则只能另辟蹊径:在自己搭建的服务器上提供 HTTPS 通讯。

新的架构可以概括为 “一套内容,两个访问点” 。我需要做的依旧是把生成的博客页面上传到 GitHub 代码仓库。不同的是,这次仓库的内容会自动同步到另一台我自己搭建的服务器上。这些页面既通过可以 GitHub 提供的链接 bitmingw.github.io 访问,也可以通过 bitmingw.com 在我自建的服务器上访问。由于我放弃了自定义域名,因此在 GitHub 的访问点可以使用他们提供 HTTPS 服务,同时,在自建的服务器上通过申请 CA 证书也能提供 HTTPS 服务。这样就实现了整个博客的加密传输。


内容同步

实现 “一套内容,两个访问点” ,关键在于保证两个节点间数据的一致性。在更新博客内容时,我只会更新 GitHub 仓库。自建的服务器会定时访问仓库,并将数据同步至本地。这个定时操作可以使用计划任务 crontab 实现。定时周期暂时取 15 分钟。

考虑到超过 99% 的情况下代码仓库都不会有变化,如果这个同步机制能够预先判定是否需要复制数据,就可以节省大量的资源。我专门写了一段 Python 脚本负责检测更新:它通过调用 GitHub API, 对比远程仓库当前的哈希值和本地仓库的哈希值,如果两个值不同,再将远程的修改取下来。这段脚本可以在 Gist 上查看(需要科学上网)。


申请 Let's Encrypt 证书

一个域名需要首先申请 CA 证书,其服务器提供的 HTTPS 通讯才能得到浏览器的认可。个人用户通过一些途径可以获取免费的证书,StartSSL 是其中较为知名的。但由于近期爆出 WoSign 丑闻,StartSSL 的证书已经不再可靠。我最终选择了 Let's Encrypt 作为证书提供商。该组织为非营利机构,由诸如 Facebook 这样的大公司运作,信誉可靠。此外,证书的申请流程也极为简单。它最主要的缺点是有效期仅为三个月,更新频率较高。

在 ubuntu 系统下使用 let's encrypt 非常简单。首先安装客户端

1
sudo apt-get install letsencrypt

取得证书最关键的一步是向 CA 证明自己对域名的控制权。客户端会首先从 CA 那里获取一段加密的信息,然后放置在一个特定的地方。CA 会通过用户指定的域名访问 web 服务器,并取得那一段信息。如果解密之后的信息无误,就说明当前服务器正在使用这个域名。为了能让 CA 访问 web 服务器上的隐藏文件,我们需要添加一个例外。以 nginx 为例,我们需要在配置文件中写入如下的信息

1
2
3
4
5
6
7
8
9
10
server {
# the default web directory
location / {
root /var/www/html;
}
# the exception for let's encrypt
location ^~ /.well-known/ {
allow all;
}
}

更改了 web server 配置之后,记得先重启服务

1
sudo nginx -s reload

然后就可以申请相应的证书了。如果你的域名是 example.com,那么对应的指令是

1
sudo letsencrypt certonly --webroot -w /var/www/html -d example.com -d www.example.com

期间会弹出窗口让你同意服务条款。填写了邮件地址并通过验证之后,证书会自动保存到服务器本地 /etc/letsencrypt 目录下。


nginx HTTPS 配置

nginx 默认提供 HTTP 服务。如果想要支持 HTTPS, 需要添加一些配置信息。继续以 example.com 为例,你可以按下面的模板填写配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
# listen to 443 HTTPS port
listen 443 ssl;

server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

# the default web directory
location / {
root /var/www/html;
}
# if you use git repository, hide it!
location ~ /\.git {
deny all;
}
# the exception for let's encrypt
location ^~ /.well-known/ {
allow all;
}
}

这样,对于浏览器向 443 端口发来的请求,nginx 就可以提供 HTTPS 加密会话了。

如果你想进一步提升安全性,拒绝不安全的 HTTP 协议,可以通过 301 跳转要求浏览器必须使用 443 端口通信,而不再受理 80 端口的请求。

结合了 301 跳转后的配置文件模板如下所示。这里我们要填写两个 server block, 一个用来返回跳转信息,另一个用来提供服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
server {
# listen to 80 HTTP port
listen 80;

server_name example.com www.example.com;

return 301 https://$server_name$request_uri;
}

server {
# listen to 443 HTTPS port
listen 443 ssl;

server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

# the default web directory
location / {
root /var/www/html;
}
# if you use git repository, hide it!
location ~ /\.git {
deny all;
}
# the exception for let's encrypt
location ^~ /.well-known/ {
allow all;
}
}


不足之处

在使用 HTTPS 的服务器上部署 301 跳转是业界的常规做法。但这不幸给 CA 证书的自动化更新制造了一点小小的困难。出于 bootstrap 等方面的考虑,CA 在验证服务器对域名的控制权时需要使用 HTTP 协议,301 跳转将会导致验证失败。因此每次更新证书时都要先修改 nginx 配置,重启服务,使用 sudo letsencrypt renew --agree-tos 更新,然后把配置改回去,最后再次重启服务。这比单纯执行 renew 指令复杂了很多。虽说 shell + crontab + sed/awk 还是可以实现全自动证书更新的,但实现起来就略微复杂了。