0x01 前言
我博客使用wordpress作为基础并使用Newspaper这个主题(Newspaper),主要记录我实验性的技术文档,但因为图片和内容比较大,对加载速度造成了一定的影响。
所以在春节假期,我特意针对CDN、nginx、apache与PHP等中间件进行小小的改造。主要改造的内容如下:
- 优化腾讯CDN的配置;
- 腾讯CDN使用Let’s Encrypt数字证书替代trust asia的数字证书;
- 优化nginx配置文件,启用nignx缓存;
- 关闭博客评论;
- 禁用wordpress jetpack插件;
- 优化mod_pagespeed;
- 优化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。另外这个配置文件可以大大地降低后台的加载时间,毕竟静态文件都从缓存获取了: