赶个时髦,把自己的博客进行了全站HTTPS改造,并升级支持了HTTP/2,总结在此,当作备忘。
很惭愧,虽然曾经做过几年Web安全产品,其实我自己并没有非常深入的去理解和思考Web安全更多内在的东西,所以可能文中的部分描述并不完全准确。很多内容参考了Jerry Qu的博客上的内容,都以参考文献的方式列在文章中,我这里只写结论,技术细节可以参考他的原文。
动因
HTTPS改造的好处当然是更安全。虽然对于一个博客网站来说,“安全”似乎并不是一个非常重要的因素,但是以国内现实的情况来说,使用HTTPS提供网站服务有一个好处就是可以避免网络运营商篡改网页内容(比如插入弹出广告)——其实吧,HTTPS以后,Chrome浏览器地址栏显示的绿色小锁才是吸引我迁移的真正原因,挺好看的。
HTTP/2从协议层面消除了传统HTTP的一些不足和缺陷,对我来说,直接的好处就是可以大幅度提高网页载入的速度。有关HTTP/2的前世今生,可以参考以下文章[1]。
HTTPS改造
证书
HTTPS改造的一个基本要素就是证书,在传统上有很多认证机构(CA)可以收费签发证书,比如大名鼎鼎的Verisign。现在也有很多公司可以提供免费或者廉价的证书,比如有名的StartSSL,以及最近很火的Let’s Encrypt。
我先是尝试了StartSSL的免费证书,但是它只能签发有效期一年的免费证书,每年都得手动去更新证书是一件很让人头痛的事情。所以后来选定了使用Let’s Encrypt,虽然Let’s Encrypt的证书有效期只有三个月,但是可以方便的通过脚本来实现自动更新。
使用Let’s Encrypt的证书有两种方式,一种是使用他的提供的工具脚本,另一种是使用ACME协议。我目前使用的是ACME协议方式,参考[2]。如果用Let’s Encrypt的工具,参考[3]。我个人比较喜欢ACME协议方式,因为很轻量级,Let’s Encrypt自己的工具太过“全家桶”了,不够简洁明了。
主要的步骤如下:
- 生成一个帐号私钥
$ openssl genrsa 4096 > account.key
- 生成一个域名私钥
$ openssl genrsa 4096 > domain.key
- 生成证书签名请求CSR文件,通常至少包含祼域名和带www主机名的两个域名。
$ openssl req -new -sha256 -key domain.key -subj "/"
-reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf
"[SAN]\nsubjectAltName=DNS:yoursite.com,DNS:www.yoursite.com")) >
domain.csr - 在自己网站上配置一个可以从外部访问的目录,用来完成challenge。Let’s Encrypt会生成一个文件,你把它放在这个目录里,然后Let’s Encrypt如果能访问到这个文件,就证明了这个域名是属于你的。nginx配置类似于如下,配在80端口的Server里面:
location ^~ /.well-known/acme-challenge/
{
alias /home/xxx/www/challenges/;
try_files $uri =404;
} - 下载acme_tiny脚本,并运行,里面用到了帐号私钥(account.key)、域名私钥(domain.key)、CSR文件(domain.csr)和ACME challenge的路径,生成签发的证书(signed.crt)。
$ wget https://raw.githubusercontent.com/diafygi/acme-tiny/master/acme_tiny.py
$ python acme_tiny.py --account-key ./account.key --csr ./domain.csr
--acme-dir /home/xxx/www/challenges/ > ./signed.crt - 最后合并Let’s Encrypt的中间证书和我们自己的证书:
$ wget -O - https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem
> intermediate.pem
$ cat signed.crt intermediate.pem > chained.pem
Web服务器
我使用nginx作为Web服务器,启用HTTPS服务,只需要在原来的HTTP服务上加几行配置就可以了:
listen 443 ssl;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:
EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:
EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_certificate /home/user/path/to/your/chained.pem;
ssl_certificate_key /home/user/path/to/your/domain.key;
ssl_session_timeout 5m;
其中需要用到你自己网站的私钥domain.key,还有Let’s Encrypt给你签发并合并了中间证书的证书文件chained.pem。
SSL协议版本与加密算法
我上面的配置用的是CloudFlare推荐的配置,详细的讨论可以参考[4]。我这么配基本上放弃了对Windows XP + IE6的支持,但可以让Qualys SSL Labs给出的评分提高到A级。
WordPress
我的博客是用WordPress构建的,WordPress在很多地方都会把带协议的网站URL给存下来,造成无法透明的把HTTP改成HTTPS。暴力解决方法就是去它的数据库做全文替换[5]。
先把WordPress设置->常规中的两个URL设置改为https,然后去WordPress的数据库对已有的文章进行全局字符串替换:
UPDATE `<wordpress_prefix>_posts` SET post_content=
(REPLACE (post_content, 'http://[domain name]',
'https://[domain name]'));
其它页面嵌入内容
如果在HTTPS的页面上嵌入了非HTTPS的内容,比如跨站的HTTP图片,浏览器上的绿色小锁就会变成灰色了。如果是跨站的CSS或者JavaScript,在现代浏览器上可能会直接被禁止加载[6]。
解决的方案就是把src的协议去掉,直接写成//domain.com/path/to/image这样的形式,可以兼容HTTP和HTTPS。
但是有些源站根本就不支持HTTPS,或者虽然提供了HTTPS服务,但证书不合法,这么做就行不通了。比如我的页面右侧的饭否的图片,虽然饭否有HTTPS服务,但证书过期了,直接嵌入就会有问题。
我的解决方案也是简单粗暴的,直接在nginx里为这些网站相关URL做一个反向代理。比如,为了解决饭否图片的问题,我在我的nginx里加了以下配置,然后把饭否图片的域名换成我自己的域名:
location /u {
proxy_pass http://b.fanfou.com;
proxy_set_header Host b.fanfou.com;
proxy_redirect off;
}
自动重定向HTTP请求
至此,HTTPS改造已经准备好了,重启nginx后就可以用HTTPS协议来访问网站,检查是否工作正常。主要检查证书是不是正常,另外还有看看有没有混杂非HTTPS资源造成页面加载不正常的。Chrome的Developer Tools可以帮助你排查这些问题。
如果一切正常,就可以考虑自动重定向所有的HTTP请求了,301跳转通常是最理想的方式。在nginx的80端口http服务器配置中添加以下的内容:
location / {
return 301 https://$server_name$request_uri;
}
其它要考虑的问题
SNI:如果在nginx用server_name实现了单主机的多虚拟站点,那就会出现一个IP地址上对应多个域名的情况,这时服务器和客户端都需要支持SNI,才能在HTTPS的情况下正常工作。较新版本的nginx版本服务器都是支持SNI的,但IE6之类的老旧浏览器不支持。所以如果放弃老旧浏览器支持的话,SNI不是个问题。否则就只能保证在同一个IP上只启用一个域名的HTTPS网站,才能确保客户访问无障碍。
HSTS:虽然启用了HTTPS,但是用户在访问时如果没有显式输入https协议,现在的浏览器默认还是会先选用http协议,再由服务器进行301/302跳转,这时就有可能被劫持。目前的解决方案是在自己的网站上输出HSTS头,并把自己的域名加入HSTS Preload List列表里[6]。我没有启用HSTS,因为一但启用没办法撤销,万一以后不能提供HTTPS服务了,想降级为HTTP都没有机会了。
其它:对于个人小网站来说,也许前面的讨论基本够用。但对稍大的网站来说,要考虑的问题还有更多。比如:CDN支持、各子域名证书的管理模式、SHA1不安全证书与老版本浏览器兼容性、大量非HTTPS外部资源的处理、特殊客户端不支持302引流等等。
HTTP/2改造
新版的nginx已经内置了对HTTP/2协议的支持,所以完成HTTPS改造后,启用HTTP/2支持是一件相对来说比较简单的事情。
在nginx里HTTPS的listen配置中加入http2即可,当然不要忘记重新reload nginx:
listen 443 ssl http2;
在Chrome中可以安装一个HTTP/2 and SPDY indicator插件,这时打开HTTP/2网站时,地址栏右侧就会出现一个蓝色的闪电标记,证明这个网站已经支持了HTTP/2协议。
新版的curl也可以用来帮助检查HTTP/2是否工作正常,在curl网站URL时,加上–http2参数即可。别忘了,必须是用HTTPS协议才能支持HTTP/2哦。
说得这么轻巧,事实上很多系统中的nginx都不是可以支持HTTP/2的新版本,所以还需要手动编译新版的nginx。
nginx -V可以列出当前nginx编译时使用的configure参数,可以作为重新编译时的参考,在它列出的参数的基础上加上–with-http_v2_module参数,就可以启用HTTP/2功能了。