0x01 前言

我博客使用wordpress作为基础并使用Newspaper这个主题(Newspaper),主要记录我实验性的技术文档,但因为图片和内容比较大,对加载速度造成了一定的影响。

所以在春节假期,我特意针对CDN、nginx、apache与PHP等中间件进行小小的改造。主要改造的内容如下:

  1. 优化腾讯CDN的配置;
  2. 腾讯CDN使用Let’s Encrypt数字证书替代trust asia的数字证书;
  3. 优化nginx配置文件,启用nignx缓存;
  4. 关闭博客评论;
  5. 禁用wordpress jetpack插件;
  6. 优化mod_pagespeed;
  7. 优化PHP-FPM配置;

今天对nginx的配置文件做个记录,方便日后复查。

0x02 SSL配置

SSL的配置没有特别需要说明的地方,具体的配置内容如下:

  ssl                         on;
  ssl_certificate             /usr/local/nginx/ssl/ngx.hk.crt;
  ssl_certificate_key         /usr/local/nginx/ssl/ngx.hk.key;
  ssl_buffer_size             16k;
  ssl_ciphers                 ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:DES-CBC3-SHA;
  ssl_prefer_server_ciphers   on;
  ssl_protocols               TLSv1 TLSv1.1 TLSv1.2;
  ssl_session_cache           builtin:20480 shared:SSL:10m;
  ssl_session_timeout         1h;
  ssl_stapling                on;
  ssl_session_tickets         on;

  add_header                  Strict-Transport-Security "max-age=31536000; preload; includeSubDomains" always;
  add_header 		      Public-Key-Pins 'pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="; pin-sha256="klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY="; max-age=2592000; includeSubDomains';

各个指令的简介如下:

  • ssl:启用SSL;
  • ssl_certificate:PEM格式数字证书的路径;
  • ssl_certificate_key:数字证书所对应的KEY的所在路径;
  • ssl_buffer_size:发送缓冲区,我也没找到较优的数值,这里使用默认的16k;
  • ssl_ciphers:加密算法,建议在Mozilla(Modern compatibility)网站中寻找相关内容;
  • ssl_prefer_server_ciphers:服务端加密算法优先;
  • ssl_protocols:加密协议;
  • ssl_session_cache:会话缓存,使用builtin会增加内存碎片,需要注意;
  • ssl_session_timeout:用户会话缓存失效时间,对安全性有高要求的站点需要降低该值;
  • ssl_stapling:启用OCSP(rfc4366#section-3.6)可减少用户验证证书的时间;
  • ssl_session_tickets:为复用会话创建或加载ticket key;
  • add_header(Strict-Transport-Security):为主域与子域启用HSTS,缓存时间为1年;
  • add_header(Public-Key-Pins):启用HPKP,指定可信的数字证书颁发机构。

0x03 安全与其他

为了监控nginx 的健康状态,我有启用stub_status,这个肯定是不允许普通访客访问的,那么配置如下:

  location /nginx_status {
    stub_status               on;
    access_log                off;
    allow                     127.0.0.1;
    allow                     103.15.217.210;
    deny                      all;
  }

我通过zabbix监控nginx,所以只允许本机进行连接,而其他IP一律返还403。

然后是modsecurity,在需要启用的location中添加以下内容即可开启:

  modsecurity                 on;
  modsecurity_rules_file      /usr/local/nginx/modsec_includes.conf;

也可以放置在server块中,对整个server有效。

因为我的站点是wordpress,所有上传的图片及文件都存放在uploads这个文件夹里,也就是说,这个文件夹里不会包含可执行文件,那么在这里做些限制:

  location ~* /(?:uploads|files)/.*\.php$ {
    deny                      all;
  }

禁止访问uploads与files这些目录里的php文件,一律返还403。

另外还可能会有些隐藏文件,例如:.htaccess或MacOS的.DS_Store等文件,我也不希望让别人访问,所以在这里也做些限制:

  location ~ /\. {
    deny                      all;
  }

禁止一切以字符“.”开头的文件被访问,一律返还403。

最后是一些静态文件,因为我服务器的架构为LNMPA,在nginx后面是apache,所以访问没有被nginx缓存的文件都需要走proxy到apache。因此静态文件主需要允许GET这种HTTP方式即可,以此提高安全性:

  location ~ .*\.(js|css|jpg|jpeg|gif|png|woff|webp|ico|svg|ttf)$  {
    proxy_pass                http://127.0.0.1:8080;
    proxy_method              GET;

    modsecurity               off;
  }

在实际应用中我发现modsecurity在连接数较高的情况下会导致nginx负载居高不下,经过测试发现是因为CRS规则中存在大量的正则匹配规则所导致的。所以针对静态文件,我关闭了modsecurity。

为了精简响应header,我还在server块中增加以下内容:

  proxy_hide_header           X-Powered-By;
  proxy_hide_header           Vary;
  proxy_hide_header           Pragma;
  proxy_hide_header           Expires;
  proxy_hide_header           Last-Modified;
  proxy_hide_header           Cache-Control;
  proxy_hide_header           Set-Cookie;
  proxy_hide_header           link;
  proxy_hide_header           x-mod-pagespeed;

精简的同时还可以提高安全性,如果需要隐藏某些敏感的响应头,则可以使用这个指令。

最后还有一个sub_filter:

#在server块中引入sub_filter.conf
  include                     /usr/local/nginx/conf.d/sub_filter.conf;

#sub_filter.conf的内容
sub_filter_types    *;
sub_filter_once     off;
sub_filter  'http://$host' 'https://$host';
sub_filter  'http:\/\/$host' 'https:\/\/$host';

sub_filter  'https://ajax.googleapis.com' 'https://ajax.c4.hk';
sub_filter  'https://ajax.googleapis.com' 'https://ajax.c4.hk';
sub_filter  '//ajax.googleapis.com' '//ajax.c4.hk';
sub_filter  'http://ajax.c4.hk' 'https://ajax.c4.hk';

sub_filter  'https://fonts.googleapis.com' 'https://fonts.googleapis.com';
sub_filter  'https://fonts.gstatic.com' 'https://fonts.gstatic.com';

sub_filter  'https://i0.wp.com' 'https://i0.wp.com';
sub_filter  'https://i1.wp.com' 'https://i1.wp.com';
sub_filter  'https://i2.wp.com' 'https://i2.wp.com';

sub_filter  'parent=https' 'parent=httpss';

sub_filter  'https://stats.wp.com' 'https://stats.wp.com';
sub_filter  'http://widgets.wp.com' 'https://widgets.wp.com';
sub_filter  'http://www.bilibili.com' 'https://www.bilibili.com';

其实我站点在协议为HTTP,而SSL是在nginx中加入的,这样可以减少前后端的握手次数,也方便日后的拓展。

但这里有个问题,因为wordpress的缘故,当使用HTTP协议,所有的链接都是HTTP的,这时候我需要通过nginx的sub_filter模块将其替换为https。

不过这部分内容在日后会交由apache完成,nginx只做SSL加密、缓存与防火墙的工作。

0x04 301跳转

我的站点启用HSTS,强制使用HTTPS协议,所以需要将80端口进入的访客通过301响应引导至HTTPS协议:

server {
  listen                     80;
  server_name                ngx.hk www.ngx.hk;

  if ($remote_addr !~* "127.0.0.1|103.15.217.210") {
    return                   301 https://ngx.hk$request_uri;
  }

  location /nginx_status {
    stub_status               on;
    access_log                off;
    allow                     127.0.0.1;
    allow                     103.15.217.210;
    deny                      all;
  }

  modsecurity                 on;
  modsecurity_rules_file      /usr/local/nginx/modsec_includes.conf;
}

以上配置文件中包含一段判断语句,这是因为zabbix agent需要访问http端口,而其他IP的访客一律引导至HTTPS协议。同时我的博客禁止一些恶意的UA访问,在这里也启用了modsecurity进行防护。

另外我的站点使用一级域名作为主域名,还需要将通过www这个二级域名进入的访客通过同样的方法引导至一级域名:

server {
  listen                      443 http2;
  server_name                 www.ngx.hk;

  ssl                         on;
  ssl_certificate             /usr/local/nginx/ssl/ngx.hk.crt;
  ssl_certificate_key         /usr/local/nginx/ssl/ngx.hk.key;
  ssl_buffer_size             16k;
  ssl_ciphers                 ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:DES-CBC3-SHA;
  ssl_prefer_server_ciphers   on;
  ssl_protocols               TLSv1 TLSv1.1 TLSv1.2;
  ssl_session_cache           builtin:20480 shared:SSL:10m;
  ssl_session_timeout         3h;
  ssl_stapling                on;
  ssl_session_tickets         on;

  add_header                  Strict-Transport-Security "max-age=31536000; preload; includeSubDomains" always;
  add_header 		      Public-Key-Pins 'pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="; pin-sha256="klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY="; max-age=2592000; includeSubDomains';

  return                      301 https://ngx.hk$request_uri;

  modsecurity                 on;
  modsecurity_rules_file      /usr/local/nginx/modsec_includes.conf;
}

其实这个server块可以与ngx.hk这个server块合并的,但我倾向于尽可能减少判断语句,除非是无法避免的,所以我将这部分也单独提取出来。

0x05 缓存相关

nginx在默认情况下,会根据源服务器的header判断是否应该缓存,而我在wordpress中使用了wp super cache这款插件,所以apache返还的header中有以下内容:

cache-control:max-age=3

意思是缓存有效期为3秒!这肯定不科学的。

另外还有set-cookie这个header,因为每次的cookie都不同,也会导致nginx缓存命中率低下,总是MISS。

所以需要通过以下指令忽略会导致nginx缓存失败的header:

    proxy_ignore_headers      Set-Cookie Expires Cache-Control X-Accel-Expires;

然后是缓存的配置:

    proxy_cache               enginx;
    proxy_cache_valid         200 304 301 302 1d;
    proxy_cache_use_stale     error timeout invalid_header http_404 http_500 http_502 http_504;
  • proxy_cache:指定缓存zone;
  • proxy_cache_valid:设定不同响应码的缓存时间;
  • proxy_cache_use_stale:当源服务器返还错误时,使用旧的缓存放还给访客。

针对不同的页面,还需要设定不同的缓存过期时间:

    expires                   1d

最后这个部分比较重要,因为启用proxy_ignore_headers后会导致需要cookie的功能无法使用,影响最大的就是后台功能了,此外还有评论等动态功能。这时候需要使用proxy_pass_header这个指令:

    proxy_pass_header         'Set-Cookie';

对于一些文件,可以这样设置:

  location ~* ^/(wp-admin|wp-cron.php|wp-comments-post.php|wp-login.php) {
    proxy_pass                http://127.0.0.1:8080;
    proxy_pass_header         'Set-Cookie';
  }

但就算这样,后台功能也会有影响,我们可以使用proxy_cache_bypass与proxy_no_cache这两个指令。

首先设定默认变量:

  set                         $wordpress_auth "";
  set                         $proxy_cache_bypass 0;
  set                         $proxy_no_cache 0;
  set                         $requestmethod GET;

然后判断cookie是否包含“wordpress_logged_in_”字符,如果是,则修改变量的值:

  if ($http_cookie ~* "wordpress_logged_in_[^=]*=([^%]+)%7C") {
   set                        $proxy_cache_bypass 1;
   set                        $proxy_no_cache 1;
  }

以上内容主要是判断cookie中是否包含wordpress_logged_in_字符,因为这段cookie只在用户成功登入后才有,所以可以以此为依据确认用户已登入,然后将proxy_cache_bypass与proxy_no_cache两者设为1,也就是on。

这两个指令会使nginx bypass缓存的设定,且不使用proxy缓存。这部分内容我放置在root路径的location块中:

  location / {
    proxy_pass http://127.0.0.1:8080;

    expires                   1d;

    proxy_ignore_headers      Set-Cookie Expires Cache-Control X-Accel-Expires;

    proxy_cache               enginx;
    proxy_cache_valid         200 304 301 302 1d;
    proxy_cache_use_stale     error timeout invalid_header http_404 http_500 http_502 http_504;

    proxy_cache_bypass        $proxy_cache_bypass;
    proxy_no_cache            $proxy_no_cache;

  }

0x06 完整配置

server {
  listen                     80;
  server_name                ngx.hk www.ngx.hk;

  if ($remote_addr !~* "127.0.0.1|103.15.217.210") {
    return                   301 https://ngx.hk$request_uri;
  }

  location /nginx_status {
    stub_status               on;
    access_log                off;
    allow                     127.0.0.1;
    allow                     103.15.217.210;
    deny                      all;
  }

  modsecurity                 on;
  modsecurity_rules_file      /usr/local/nginx/modsec_includes.conf;
}

server {
  listen                      443 http2;
  server_name                 www.ngx.hk;

  ssl                         on;
  ssl_certificate             /usr/local/nginx/ssl/ngx.hk.crt;
  ssl_certificate_key         /usr/local/nginx/ssl/ngx.hk.key;
  ssl_buffer_size             16k;
  ssl_ciphers                 ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:DES-CBC3-SHA;
  ssl_prefer_server_ciphers   on;
  ssl_protocols               TLSv1 TLSv1.1 TLSv1.2;
  ssl_session_cache           builtin:20480 shared:SSL:10m;
  ssl_session_timeout         3h;
  ssl_stapling                on;
  ssl_session_tickets         on;

  add_header                  Strict-Transport-Security "max-age=31536000; preload; includeSubDomains" always;
  add_header 		      Public-Key-Pins 'pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="; pin-sha256="klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY="; max-age=2592000; includeSubDomains';

  return                      301 https://ngx.hk$request_uri;

  modsecurity                 on;
  modsecurity_rules_file      /usr/local/nginx/modsec_includes.conf;
}

server {
  listen                      443 http2;
  server_name                 ngx.hk;

  ssl                         on;
  ssl_certificate             /usr/local/nginx/ssl/ngx.hk.crt;
  ssl_certificate_key         /usr/local/nginx/ssl/ngx.hk.key;
  ssl_buffer_size             16k;
  ssl_ciphers                 ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:AES256-GCM-SHA384:DES-CBC3-SHA;
  ssl_prefer_server_ciphers   on;
  ssl_protocols               TLSv1 TLSv1.1 TLSv1.2;
  ssl_session_cache           builtin:20480 shared:SSL:10m;
  ssl_session_timeout         3h;
  ssl_stapling                on;
  ssl_session_tickets         on;

  include                     /usr/local/nginx/conf.d/sub_filter.conf;
  root                        /usr/local/html/ngx.hk/public_html/;
  access_log                  /usr/local/html/ngx.hk/logs/ngx_access.log main;

  gzip                        off;

  modsecurity                 on;
  modsecurity_rules_file      /usr/local/nginx/modsec_includes.conf;

  proxy_redirect              https://ngx.hk https://ngx.hk;

  add_header                  Vary "Accept-Encoding, Cookie";
  add_header                  Pragma "public";
  add_header                  Aache-Control "public, must-revalidate, proxy-revalidate";
  add_header                  cache-status "$upstream_cache_status";
  add_header                  Strict-Transport-Security "max-age=31536000; preload; includeSubDomains" always;
  add_header                  Public-Key-Pins 'pin-sha256="YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="; pin-sha256="klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY="; max-age=2592000; includeSubDomains';

  proxy_hide_header           X-Powered-By;
  proxy_hide_header           Vary;
  proxy_hide_header           Pragma;
  proxy_hide_header           Expires;
  proxy_hide_header           Last-Modified;
  proxy_hide_header           Cache-Control;
  proxy_hide_header           Set-Cookie;
  proxy_hide_header           link;
  proxy_hide_header           x-mod-pagespeed;

  set                         $wordpress_auth "";
  set                         $proxy_cache_bypass 0;
  set                         $proxy_no_cache 0;
  set                         $requestmethod GET;

  if ($http_cookie ~* "wordpress_logged_in_[^=]*=([^%]+)%7C") {
   set                        $proxy_cache_bypass 1;
   set                        $proxy_no_cache 1;
  }

  location ~ /\. {
    deny                      all;
  }

  location ~* /(?:uploads|files)/.*\.php$ {
    deny                      all;
  }

  location ~* ^/(wp-admin|wp-cron.php|wp-comments-post.php|wp-login.php) {
    proxy_pass                http://127.0.0.1:8080;
    proxy_pass_header         'Set-Cookie';
  }

  location ~ .*\.(js|css|jpg|jpeg|gif|png|woff|webp|ico|svg|ttf)$  {
    proxy_pass                http://127.0.0.1:8080;
    proxy_method              GET;

    modsecurity               off;

    expires                   30d;

    proxy_ignore_headers      Set-Cookie Expires Cache-Control X-Accel-Expires;

    proxy_cache               enginx;
    proxy_cache_valid         200 304 301 302 30d;
    proxy_cache_use_stale     error timeout invalid_header http_404 http_500 http_502 http_504;
  }

  location / {
    proxy_pass http://127.0.0.1:8080;

    expires                   1d;

    proxy_ignore_headers      Set-Cookie Expires Cache-Control X-Accel-Expires;

    proxy_cache               enginx;
    proxy_cache_valid         200 304 301 302 1d;
    proxy_cache_use_stale     error timeout invalid_header http_404 http_500 http_502 http_504;

    proxy_cache_bypass        $proxy_cache_bypass;
    proxy_no_cache            $proxy_no_cache;

  }

}

还有一些指令的简介如下:

  • listen:监听443端口并启用http2;
  • gzip:因为gzip与modsecurity不太兼容,所以禁用了modsecurity;
  • proxy_redirect:某些情况下,apache会返还301,而header中的地址为http,需要替换为https。

完成配置修改并重新加载nginx后,header的内容是这样的:

因为我的站点使用CDN加载所有静态资源,所以资源的加载情况是这样的:

可以看到因为没有使用gzip,所以整个页面加载时间用了将近5秒,大小为223kb。但从我服务器中加载的内容也只有页面源码,所以影响还不算太大。

0x07 结语

关闭gzip也确实是无奈,因为要使用modsecurity。另外这个配置文件可以大大地降低后台的加载时间,毕竟静态文件都从缓存获取了: