把 Nginx 安装脚本重写了一遍,顺手接上了 WAF

Oct 16, 2024 | 9 min

起因

这份脚本不是一开始就想得很完整。最早只是图省事,想把 Nginx 的源码安装固定下来,免得每次重装都重新找模块、重新拼参数。后来机器装多了,升级做过几轮,回退也做过几次,才慢慢觉得,麻烦往往不在安装那一下,而在后面那些零零碎碎的收尾。

比如模块到底有没有带全,旧版本怎么切回去,配置应该跟着哪个目录走,WAF 那一套 Lua 文件是不是还得装完以后再补。这些事情第一次做时都不算大,隔一阵子再回来,才发现最容易忘的偏偏就是这些“最后几步”。

所以后来我干脆把脚本整个重收了一遍,先把后面维护时总会碰到的那几件事理顺。

相关代码在这里:

现在这脚本大概是什么样子

它直接走源码安装,不走 aptyum 这条路。Nginx 本体之外,OpenSSL、PCRE2、LuaJIT、libmaxminddb 这些依赖会一起处理,Lua 和 GeoIP2 相关模块也一并编进去。像我平时会用到的 ngx_devel_kitlua-nginx-modulestream-lua-nginx-modulengx_http_geoip2_modulengx_http_substitutions_filter_module,现在都已经放进脚本里了。

我真正想保住的其实是目录结构。默认路径下,大致是这样:

/usr/local/nginx/releases    # 每次编译出来的新版本
/usr/local/nginx/current     # 当前正在使用的版本
/usr/local/nginx-data        # 运行时配置、WAF、Lua 文件、日志目录等

这样分开以后,后面的动作就简单很多。新版本先编出来,配置先拿新二进制验一遍,确认没问题了再把 current 切过去;真要回退,也只是把链接拨回上一版。运行时的配置、WAF 和日志都还在原来的地方,不用重新从旧目录里一点点抠出来。

这也是我后来一直没把这套装法换掉的原因。过一段时间再回来,版本在什么地方,当前跑的是哪一个,运行时那份东西又放在哪,还是一眼能看明白。

WAF 为什么干脆一起并进去

以前我常干一件事:Nginx 装完以后,再把 WAF 那套东西慢慢补上。短期没什么问题,机器一多就开始烦了。今天漏个 include waf/waf.conf;,明天忘了 lua_package_path,后天又得确认运行时用的是不是那份 config.lua。这些都不是大活,但每一项都很容易在“装完以后再说”这一步里丢掉。

所以这次索性把 WAF 一起并进安装流程里,不再把它留成安装后的手工活。

脚本现在既可以直接用本地仓库里的 waf/ 目录,也可以从远端仓库拉;策略上也分成三种:必须带着 WAF 才继续装的、WAF 出问题就先给警告的、以及完全跳过 WAF 的。不同机器要求不一样,把这件事交给参数控制,比写死在脚本里省事得多。

运行时 WAF 默认会放在:

/usr/local/nginx-data/conf/waf

目录拷过去还不够,后面几处最容易漏的地方也得一起补上:nginx.conf 里的 include、lua_package_path / lua_package_cpath、运行时要用的 Lua 文件,以及最后那次配置校验。这样装完以后,WAF 就已经在运行结构里了,不需要再回头补最后几步。

平时我真正会动的地方

真到日常维护时,我碰得最多的还是 config.lua。这套 WAF 的结构我觉得还算清楚,README 里把主要文件拆开了:

  • config.lua:统一配置入口
  • rules.lua:规则加载和缓存
  • logger.lua:安全日志
  • challenge.lua:挑战页、校验和放行 cookie
  • wafconf/:规则目录

平时如果是调日志、改拦截页、动 CC 阈值,或者调整检查顺序,通常都还是从 config.lua 下手。这样做的好处很实际:过几个月再回来,不用先在一堆零散文件里找半天,先看这里基本就够了。

challenge 这块我也比较留意。页面能不能弹出来倒还是小事,后面的状态怎么收,才是真正麻烦的地方。原始回跳地址会先记在共享字典里,不会因为走了一趟验证就丢掉;旧的 cookie 也不能拿来绕过新锁。

如果要开 turnstile,我一般先确认这几件事:

  • config.lua 里已经填好了 site_keysecret_key 和 challenge secret。
  • waf.conf 里保留了 lua_shared_dict waf_challenge 20m;
  • 机器本身能访问 Turnstile 的校验接口。
  • 运行时已经有 lua-resty-http

这些前提不通,前端页面就算能打开,最后还是会卡在验证上。

我自己通常怎么跑

第一次上机时,我一般先做一次 dry-run,把路径和动作看一遍:

bash nginx/install_nginx.sh --dry-run

确认没问题后,再真正编译和发布。通常我会先保留服务不动,自己手工验一遍:

bash nginx/install_nginx.sh --waf-source online --waf-policy optional

编完以后,先拿活动二进制验证运行时配置:

/usr/local/nginx/current/sbin/nginx -t -p /usr/local/nginx-data/ -c conf/nginx.conf

确认通过,再决定要不要 reload:

systemctl reload nginx

如果只是想更新 WAF 规则或逻辑,不想把整套 Nginx 连带依赖再编一遍,脚本也留了单独入口:

bash nginx/install_nginx.sh --update-waf-only --waf-policy required

这个模式只处理运行时 WAF,同步完会重新校验配置并 reload。规则改动比较勤、但 Nginx 本体不需要动的时候,用起来会轻松很多。

还有两个地方我自己会额外留心。

一个是 --sync-waf 默认没开。脚本不会每次安装都强行覆盖运行时 conf/waf,这很正常,因为线上规则经常已经按机器情况改过。如果就是想拿仓库里的版本完整覆盖运行时 WAF,再显式加 --sync-waf 会更稳。

另一个是 Turnstile 真正容易卡住的地方,很多时候不在前端页面,而是在后端这几处:resty.http 有没有,LUA_PATH / LUA_CPATH 有没有进服务环境,服务器能不能访问 Turnstile 校验地址。这几处不通,前面看着都对,最后还是会卡在验证上。

最后

我一直把这份脚本留着,说到底就是图后面省事。隔一阵子再回来,还是知道版本在哪、当前跑的是哪一个、配置放在哪、WAF 又放在哪。

机器少的时候,这些事看着不起眼;机器一多,或者中间隔了很久再回来,这种清楚反而最值钱。

Comments

Email is only used for reply notifications and is never shown publicly.