HTTPS 配置手记

国内的网络环境大家都懂的,ISP劫持然后给你的页面强插点 banner 或者悬浮的广告之类的都算小事,更夸张的有直接劫持京东淘宝,通过返利链接获利的,所以全站 HTTPS 基本已经是必选项了,好在现在有了免费提供证书服务的 Let's Encrypt 项目,以及随之衍生出来的一系列工具,单纯只是配置 HTTPS 的话,过程已经非常便捷了。

获取证书

在获取证书方面现在已经有了一些方便的工具,比如我使用的 acme.sh, 它实现了 acme 协议, 可以从 Let's Encrypt 生成免费的证书,而且支持证书自动续期等功能。安装也十分简单,只需要:

curl  https://get.acme.sh | sh

安装完成后就可以生成证书了,acme.sh 目前提供了 cloudflare, dnspod, cloudxns, godaddy 以及 ovh 等数十家域名解析服务的自动集成方案,以我使用的域名服务商 Gandi 为例,只需要先从网站获取 Api key , 然后在终端执行:

export GANDI_LIVEDNS_KEY="your Api key"
acme.sh --issue --dns dns_gandi_livedns -d example.com -d www.example.com

就可以完成整个证书生成过程,详细的用法请参考 api 文档

当然 acme.sh 也提供了通常的,通过 http 或 dns 记录来验证域名所有权生成证书的方式,操作上并不困难,这里就不多说了,详细可以参考 acme.sh 的文档

安装证书

生成好的证书会存放在 ~/.acme.sh/ 目录下,不推荐直接使用该文件夹里的证书, acme.sh 提供了相关命令来将证书复制到指定位置并让 nginx 重新加载证书

acme.sh  --installcert  -d  mydomain.com   \
        --key-file   /etc/nginx/ssl/mydomain.key \
        --fullchain-file /etc/nginx/ssl/mydonain.cer \
        --reloadcmd  "service nginx force-reload"

但是在我的机器上似乎并没有起效...于是只好乖乖手动复制 ORZ...复制完成后,更新一下 nginx 的配置

server {
    listen       443 ssl;
    server_name  mydomain;
    ssl_certificate "/etc/nginx/ssl/mydomain.cer";
    ssl_certificate_key "/etc/nginx/ssl/mydomain.key";
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout  10m;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass  http://127.0.0.1:8080;
        proxy_redirect off;
        proxy_set_header Host $proxy_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

server {
    listen       80;
    server_name  mydomain;
    return 301 https://$host$request_uri;
}

完成之后重启 nginx,检查一下网站的资源和请求是否都通过 HTTPS 方式请求和发送,检查确认无误后,再访问网页,应该就能看到代表安全的小绿锁啦。

后续

说是后续,其实到这里才只能算是开始,就像代码需要测试一样,HTTPS 也有专业的安全测试和评分网站。

来,不服跑个分

常用的有以下两家:

  1. Qualys SSL Labs's SSL Server Test
  2. HTTP Security Report

以下是本站刚部署完 HTTPS 之后,直接测试的结果: 唔,看起来并不乐观啊...

Weak Diffie–Hellman key exchange

于是我们一项一项来解决这些影响我们网站安全的问题,首先是 Qualys SSL Labs 提示的 Weak key exchange,看来我的服务器在通过DH算法进行密钥交换时使用的参数太弱了, Qualys SSL Labs 直接给出了解决方案, 首先,通过 openssl 生成一个更健壮的参数:

openssl dhparam -out dhparams.pem 2048

然后在nginx配置中添加如下条目就可以解决这个问题:

ssl_ciphers "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA";

ssl_prefer_server_ciphers on;

ssl_dhparam {path to dhparams.pem};

Server Certificate

很多 web server 会在请求头中添加它自己的名称和版本信息,但是这样其实不太好,这样直接把版本号暴露出来,别有用心的人可能就能够通过某些版本的 bug 来干些坏事,这些信息本来就用处不大,那我们干脆来进行一些限制, 在 nginx.conf 的 http 部分添加一行配置就可以解决:

server_tokens off;

Content Type Options

这一项其实主要针对的是 IE 的 MIME 嗅探,似乎是所有版本的 IE 都存在一个漏洞,容易导致 XSS 攻击,虽然我的网站上还没有任何给用户输入信息的地方,但是为了之后的评论系统考虑,还是先把这个窟窿填上把。解决方法依然简单,通过 nginx 在网页最终输出时添加如下的响应头:

add_header  X-Content-Type-Options  nosniff;

Frame Options

这一项主要针对的是网页是否允许被 iframe 嵌套,deny标示不允许,在 nginx 配置中添加如下响应头就可以解决:

add_header  X-Frame-Options  deny;

Content Security Policy

主要是指定页面可以加载哪些资源,以减少XSS的发生,依然是通过从 ngnix 中添加请求头的方式来修改,这里的配置允许了本站,本站使用的图床以及 google 统计的资源的加载,并且允许了内联 JS,在 JS 中使用 eval,以及内联 CSS.

add_header  Content-Security-Policy  "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: https://www.google-analytics.com https://touko-blog-photo.b0.upaiyun.com https://touko-blog-img.b0.upaiyun.com ; style-src 'self' 'unsafe-inline';

HTTP Strict Transport Security

简称(HSTS),启用后只要用户访问网站,就会告知用户的浏览器,在 max-age 内始终通过 HTTPS 来访问网站,这样即使用户访问的时候输入的是 HTTP 的地址,浏览器也为自动纠正为 HTTPS. 同样通过添加请求头实现:

add_header  Strict-Transport-Security  "max-age=31536000";

Public Key Pins

这一条主要参考了曲大的 HTTP Public Key Pinning 介绍,详细不是一两句话可以讲清的,可以仔细阅读曲大的文章,我这里就不赘述了

完工

完成上面的各项配置后,再测试一下。

看上去在安全性方面以及没啥大问题了,值得注意的是 HSTS Preloaded 一项,需要去 Chromium’s HSTS preload list 网站进行登录才能通过,我使用的证书因为不支持子域名,不符合登录的条件就没有再继续下去了,不过总体影响不大。这样本站的 HTTPS 配置就算是基本完成了。

参考文章