实践个人网站迁移HTTPS与HTTP/2

赶个时髦,把自己的博客进行了全站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自己的工具太过“全家桶”了,不够简洁明了。

主要的步骤如下:

  1. 生成一个帐号私钥
    $ openssl genrsa 4096 > account.key
  2. 生成一个域名私钥
    $ openssl genrsa 4096 > domain.key
  3. 生成证书签名请求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
  4. 在自己网站上配置一个可以从外部访问的目录,用来完成challenge。Let’s Encrypt会生成一个文件,你把它放在这个目录里,然后Let’s Encrypt如果能访问到这个文件,就证明了这个域名是属于你的。nginx配置类似于如下,配在80端口的Server里面:
    location ^~ /.well-known/acme-challenge/
    {
        alias /home/xxx/www/challenges/;
        try_files $uri =404;
    }
  5. 下载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
  6. 最后合并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: