nginx ssl 配置

canca canca
2017-02-14 20:11
2
0
过年休息闲得无聊,而我的 VPS 很凑巧地挂了,于是这两天就忙着配置新的 VPS 了。
其实上面就三个东西:我的博客、Shadowsocks 和 Dropbox(用来备份 Redis 数据文件),所以很快就恢复了。

顺便检讨一下,其实去年 7 月就因为 GAE 停止对 Master/Slave 类型的应用支持,而花了几天时间把博客重写了,让它能 跑在 Linux 上 了。拖延了 2 年最后还是得靠 deadline…
不过一直没空整理代码,所以现在还没开源,希望能有下一个 deadline 吧。

不过本文并不是聊 Doodle 2 的开发经验,而只是记录一些折腾 nginx 配置的收获。

先说下启用 HTTPS 吧。
其实迁移博客的时候,我就已经启用了 HTTPS,只是没强制跳转而已。这半年多来好像没遇到什么问题,所以应该可以放心用了。

要启用 HTTPS 自然需要一个证书。没什么需求的话,弄个免费的就行了。我阴差阳错地选择了  StartSSL
网上有教程,我就不详述怎么申请了,基本流程如下:
  1. 生成一个加密的 key 文件:
       
               
        
                de >openssl genrsa -des3 -out example.com_secure.key 4096
        
                de>
       
               
    需要输入密码,自己记住。
  2. 生成 CSR 文件:
       
               
        
                de >openssl req -new -sha256 -key example.com_secure.key -out example.com.csr
        
                de>
       
               
    需要输入上述的密码。
  3. 把 CSR 文件的内容输入到 StartSSL 的文本框,然后提交。
  4. 填入要使用的域名(需要验证拥有权),开始申请。
  5. 把申请好的证书下载下来,解压。其中,NginxServer.zip 可以解压得到一个类似 1_example.com_bundle.crt 的文件。
  6. 生成解密的 key 文件:
       
               
        
                de >openssl rsa -in example.com_secure.key -out example.com.key
        
                de>
       
               
    需要输入上述的密码。
  7. 然后修改 nginx 配置:
       
               
        
                de >
        
                server { 
        
                listen 
        
                443 ssl; 
        
                ssl_certificate /.../1_example.com_bundle.crt; 
        
                # 路径自己补完 
        
                ssl_certificate_key /.../example.com.key; 
        
                # 需要用解密的 key 文件,如果不解密的话,启动 nginx 时会要求手动输入密码 
        
                ssl_protocols TLSv1 TLSv1.
        
                1 TLSv1.
        
                2; 
        
                # 不支持 SSL,因为不安全 
        
                ssl_session_cache builtin:
        
                1000 shared:SSL:
        
                10m; 
        
                # 后面都是优化性能的 
        
                ssl_session_timeout 
        
                10m; 
        
                ssl_prefer_server_ciphers 
        
                on; 
        
                ssl_session_tickets 
        
                on; 
        
                ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; 
        
                # 禁用某些不安全的 ciphers }
        
                de>
       
               
  8. 重启 nginx,检查是否可以用 HTTPS 访问。
其实如上配置后, SSL Labs  就能拿到 A 级的得分了。
不过它还显示 OCSP stapling 为 No,这玩意是啥呢?
OCSP(Online Certificate Status Protocol)的中文翻译是在线证书状态协议,它是用来检查证书是否有效的。虽然证书一般是有效的,但也可能被撤销,这时就需要实时查询证书的状态,以确保它有效。
这个查询如果让浏览器去做,就比较浪费时间,而 OCSP stapling 技术则是让服务器自己查出来,再把状态信息(无法伪造)返回给浏览器,以加快验证过程。

启用 OCSP stapling 稍微有点麻烦,首先需要了解自己的证书链。
最简单的方式就是拿浏览器访问一下,然后点下绿色的小锁,查看证书。
我的博客有三层证书,分别是:
  1. StartCom Certification Authority
  2. StartCom Class 1 DV Server CA
  3. keakon.net

其中,第三个证书已经提供给 nginx 了,而前两个还需要自己下载回来,合并成一个:
 
         
  
          de >wget https://startssl.com/certs/ca.crt wget https://startssl.com/certs/sca.server1.crt cat ca.crt sca.server1.crt > ca-bundles.pem
  
          de>
 
         

再给 nginx 增加一点配置:
 
         
  
          de > 
  
          ssl_stapling 
  
          on; 
  
          ssl_stapling_verify 
  
          on; 
  
          ssl_trusted_certificate /.../ca-bundles.pem; 
  
          resolver 
  
          8.8.4.4 
  
          8.8.8.8 valid=
  
          300s; 
  
          resolver_timeout 
  
          10s;
  
          de>
 
         
然后 reload nginx 应该就行了。

以我的博客为例,执行如下命令:
 
         
  
          de >
  
          echo QUIT | openssl s_client -servername www.keakon.net -connect www.keakon.net:443 -status 2> /dev/null
  
          de>
 
         
可以看到 OCSP response 信息,说明已经生效了。

接下来顺便启用 HTTP/2。
我先看了下 nginx 的支持,发现是从 1.9.5 版才引入了 http_v2_module 模块,而且需要在编译时加上才行。
我再看了下自己的 nginx,还是 1.9.3,于是只好埋头自己弄了。

搜了下相关的介绍,发现用 LibreSSL 应该是最方便的,于是下了个 2.2.6 版,一路 ./configure && make && make install 搞定。
接着下了 nginx 1.9.10 的源码,配置时需要加上如下三个参数:
 
         
  
          de >./configure --with-http_v2_module --with-http_ssl_module --with-openssl=../libressl-2.2.6
  
          de>
 
         
自己注意修改 LibreSSL 的路径吧。

这里有个坑便是  LibreSSL 目前还不支持等价加密算法组 ,可以用 BoringSSL 取代,但它又不支持 OCSP stapling…

再改下 nginx 的配置:
 
         
  
          de >
  
          listen 
  
          443 ssl http2 fastopen=
  
          3 reuseport;
  
          de>
 
         
其中,http2 是启用 HTTP/2,后面两个是用来提升性能的。

最后运行新的 nginx,就可以测试了。
打开 Chrome 的 Developer Tools,翻到 Network 页,刷新一下,在 Name 那行点下右键,勾选 Protocol,可以看到已经变成 h2 了。

顺便再强制跳转 HTTPS。
考虑到很多爬虫啥的强制跳 HTTPS 也没啥意义,过时的浏览器我本来就不关心,而现代的浏览器会发送「upgrade-insecure-requests: 1」请求头,所以针对它处理下就行了:
 
         
  
          de > 
  
          location / { 
  
          if (
  
          $http_upgrade_insecure_requests = 
  
          "1") { 
  
          add_header Vary Upgrade-Insecure-Requests; 
  
          return 
  
          307 https://
  
          $host
  
          $request_uri; } }
  
          de>
 
         

结果这样配置后就循环重定向了……查看了一下才发现,Chrome 不管是不是已经启用 HTTPS 了,都会发送「upgrade-insecure-requests: 1」请求头……
所以只能再检查下是否已经启用 HTTPS,可惜 nginx 并不支持嵌套的 if,于是只好增加一个变量了。我把 $http_upgrade_insecure_requests 和 $https 拼起来赋值给 $redirect_to_https 变量。前者在 Chrome 下为 1,后者在 http 下为空,https 下为 on,拼起来有「」、「1」、「on」和「1on」这 4 种组合,而它为 1 时才需要处理:
 
         
  
          de > 
  
          location / { 
  
          set 
  
          $redirect_to_https 
  
          "${http_upgrade_insecure_requests}${https}"; 
  
          if (
  
          $redirect_to_https = 
  
          "1") { 
  
          add_header Vary Upgrade-Insecure-Requests; 
  
          return 
  
          307 https://
  
          $host
  
          $request_uri; } }
  
          de>
 
         

不过这种跳转仍然需要访问服务器,所以再加个 header:
 
         
  
          de > 
  
          location / { 
  
          add_header Strict-Transport-Security max-age=
  
          86400; }
de>
这样在访问过一次这个网站的 HTTPS 页面后,之后再访问它的 HTTP 页面,都会由浏览器自动跳转到 HTTPS。不过考虑到稳定性,万一我把 HTTPS 配置搞挂了,就没法访问了,所以我只设置了一个较短的时间,先观望一下再说。


再增加一些安全设置。
 
    
            
  
     
             de  >
  
     
             server {
    
  
     
             server_tokens 
  
     
             off; 
  
     
             # 不透露 nginx 的版本,不过我都写出来了…

    
  
     
             if (
  
     
             $host != 
  
     
             'www.keakon.net') {
        
  
     
             rewrite
  
     
              ^/(.*)$ 
  
     
             $scheme://www.keakon.net/
  
     
             $1 
  
     
             permanent; 
  
     
             # 不是我的域名都跳转
    }

    
  
     
             location
  
     
              ^~ /static/ {
        
  
     
             alias /.../static/;
        
  
     
             add_header Last-Modified 
  
     
             ""; 
  
     
             # 去掉 Last-Modified 头,有 ETag 就够了
        
  
     
             expires 
  
     
             5m;
        
  
     
             if (
  
     
             $request_filename 
  
     
             ~* \.(jpg|png|gif|ico|bmp)$) { 
  
     
             # 只给图片文件增加
            
  
     
             expires 
  
     
             1d;
            
  
     
             add_header X-Content-Type-Options nosniff;
            
  
     
             add_header X-XSS-Protection 
  
     
             "1; mode=block";
        }
        
  
     
             if (
  
     
             $query_string) {
            
  
     
             expires 
  
     
             7d;
        }
    }

    
  
     
             location / {
        
  
     
             add_header X-Frame-Options SAMEORIGIN; 
  
     
             # 只允许本站用 frame 来嵌套
        
  
     
             add_header X-Content-Type-Options nosniff; 
  
     
             # 禁止嗅探文件类型
        
  
     
             add_header X-XSS-Protection 
  
     
             "1; mode=block"; 
  
     
             # XSS 保护
    }
}
  
     
             de>
 
    
            
至于 Content-Security-Policy 我就懒得弄了,观察了一下用到好多 google 域名的 JavaScript 文件,老修改配置也麻烦,先将就着吧。

最后再优化一下性能。
 
    
            
  
     
             de  >    
  
     
             gzip 
  
     
             on;
    
  
     
             gzip_vary 
  
     
             on;
    
  
     
             gzip_http_version 
  
     
             1.
  
     
             0; 
  
     
             # 对 HTTP 1.0 启用,据说 30% 的移动流量是 HTTP 1.0,还好我用联通
    
  
     
             gzip_min_length 
  
     
             1000; 
  
     
             # 太小的没必要压缩
    
  
     
             gzip_comp_level 
  
     
             6; 
  
     
             # 其实大于 0 都行,CPU 够用就设大点吧
    
  
     
             gzip_disable msie6;
    
  
     
             gzip_types text/plain text/html text/css application/json application/javas
  
     
             cript application/x-javas
  
     
             cript text/javas
  
     
             cript text/xml application/xml application/rss+xml application/atom+xml;

    
  
     
             sendfile 
  
     
             on; 
  
     
             # 因为主要是小文件,就不启用 aio 了
    
  
     
             sendfile_max_chunk 
  
     
             512k; 
  
     
             # 较大的文件不要一次全读取了,浪费内存
    
  
     
             tcp_nopush 
  
     
             on; 
  
     
             # 对于 keepalive 连接,可以尽快发送响应
    
  
     
             tcp_nodelay 
  
     
             on; 
  
     
             # 包填满了再发,与 sendfile 组合用
  
     
             de>
 
    
            

差不多也折腾够了,可以睡觉了。

发表评论