控制台点一次保存,四项定制全被抹掉:配置生成器的收尾

https 打不开、上传 413、http 不跳转、日志里全是网关 IP,同一类故障反复出现。根因是生成器整份重写配置,模板里没有这些定制,只能靠幂等收尾脚本补回去。

入口层的 WAF 有一个网页控制台,改站点配置挺方便。但用久了会发现一个规律:只要在控制台点过一次保存,过一会儿总有一项东西不对——有时候是 https 打不开,有时候是大文件上传报 413,有时候是 http 居然不跳 https 了。

每次都能手动修回来,所以一直没当回事,直到有一次同时出了三项,正好赶上业务高峰期。

一、先确认「是谁改的」

第一次遇到时,我先去查 nginx 配置的时间戳,发现整份 server 段被重写了一遍,不只是我改的那一行。配置里原本手写的几段定制全没了。

WAF 的控制台逻辑是「数据库是源,配置文件是产物」:每次保存都会触发生成器,把这个站点的配置整份重新渲染,覆盖磁盘上的文件。生成器的模板里只有标准项,没有我们后来手加的定制,于是每保存一次,定制就被抹一次。

这一点很关键:它不是「改坏了某一行」,而是「用模板重写整份」。所以排查时不要去找哪一行变了,要去找「模板里缺什么」。

判断是不是生成器干的,有一个很快的办法:看配置文件的 mtime,和操作时间对一下。

stat -c '%y %s %n' /etc/waf/sites-enabled/site-5.conf
# 2026-11-12 08:28:41 6217 /etc/waf/sites-enabled/site-5.conf

如果 mtime 正好是「有人点保存」的那一分钟,而且文件大小和你印象中的不一样,基本可以确定是被整份重写了。再看一眼文件里有没有你手写的注释——生成的配置不会保留人工注释,这是最直接的证据。

二、被抹掉的四项,症状各不相同

摸清规律之后,我把被抹掉的定制列了一张表,每项对应一个症状。这张表后来成了值班手册的一页:

定制项 被抹掉后的症状 应该待的位置
443 段的证书指令 整站 https 打不开,nginx -t 直接报错 server 的 443 块内
请求体大小(1024m) 大文件上传 413,小文件正常 server 或 location 内
80 跳 443 http 访问不跳转,页面能开但混合内容报警 80 段的 if ($scheme = http)
真实 IP 透传(XFF) 应用日志里全是网关 IP,风控失准 location 内的 proxy_set_header

这四项有个共同点:都不是「锦上添花」,缺了要么直接不可用,要么悄悄出错。尤其是 XFF,页面完全正常,只有看应用日志才发现所有请求都来自同一个 IP。

三、生成失败会整份回滚

比「定制被抹」更麻烦的是另一种情况:生成器渲染出来的配置语法不过关,nginx -t 失败,于是它把整份配置回滚到上一版——包括你刚才的修改。表现是「保存成功了,但什么都没变」。

我们遇到的触发条件是:站点配了 443,但模板渲染时没有把证书指令带进去,nginx -t 报 no "ssl_certificate" is defined。生成器不区分「哪一段错了」,直接整份回退。

这就形成一个死循环:想改配置 → 生成失败 → 整份回滚 → 再改,还是失败。破局的办法是先手工把 443 段补全(哪怕用一张临时自签证书),让 nginx -t 能过,再让生成器跑。顺序很重要:先让它能通过语法检查,再谈定制。

四、收尾脚本:用标记做幂等注入

既然生成器每次都会重写,那就在它之后补一刀。写一个收尾脚本,在生成完成后执行,把四项定制按「标记 + 幂等」的方式插回去。

关键是幂等。用 sed -i 直接追加的写法,跑两次就会插两份,第三次可能插到注释里。我们用一对注释当锚点:

#!/usr/bin/env bash
set -euo pipefail

CONF="/etc/waf/sites-enabled/site-5.conf"
BEGIN="# >>> ops-custom begin"
END="# <<< ops-custom end"

inject() {
  # 已有标记则整段替换,没有则插到 server 块第一行之后
  if grep -qF "$BEGIN" "$CONF"; then
    awk -v b="$BEGIN" -v e="$END" -v f="$1" '
      $0==b {print; while ((getline line < f) > 0) print line; skip=1; next}
      $0==e {skip=0}
      !skip {print}
    ' "$CONF" > "$CONF.tmp" && mv "$CONF.tmp" "$CONF"
  else
    awk -v f="$1" -v b="$BEGIN" -v e="$END" '
      {print} /^[[:space:]]*server[[:space:]]*\{/ && !done {
        print b; while ((getline line < f) > 0) print line; print e; done=1
      }
    ' "$CONF" > "$CONF.tmp" && mv "$CONF.tmp" "$CONF"
  fi
}

cp -a "$CONF" "$CONF.bak.$(date +%s)"
inject /opt/ops/custom/ssl-and-headers.conf
nginx -t && nginx -s reload

几个细节值得说:

先备份再改,备份名带时间戳。回滚只需要 cp 回来,不用回忆改了什么。

awk 比 sed 适合做「整段替换」,因为它能按行状态机处理,不会像 sed 那样被特殊字符噎住。

改完必须 nginx -t 再 reload,而且用 && 串起来——语法不过就不 reload,保持旧配置继续服务。

五、光有收尾脚本还不够

收尾脚本解决「我知道要跑」,但解决不了「别人在控制台点了保存,我不知道」。所以还要一个巡检。

我们的做法是一个五分钟跑一次的守护脚本,检查六项:证书是否在、请求体大小是否对、80 是否跳 443、XFF 是否透传、四个安全响应头是否齐全、配置语法是否有效。任意一项不符就重新跑一次收尾脚本,并记一行日志。

# 巡检片段:检查响应头是否齐全
for h in "strict-transport-security" "x-frame-options" "x-content-type-options" "referrer-policy"; do
  got=$(curl -sSI "https://app.example.com/" | grep -ci "^$h:")
  [ "$got" -eq 1 ] || { echo "$(date -Is) missing $h"; needs_fix=1; }
done
[ "${needs_fix:-0}" -eq 1 ] && /opt/ops/finalize.sh 5

这个守护脚本上线后,同类故障从「每次保存都要人盯」变成「最迟五分钟自愈」。但它只是兜底,不能替代收尾脚本——因为五分钟的窗口里,用户已经在报错了。

守护脚本还有一个必须有的功能:它自己要能被告警。如果守护脚本因为权限、路径、依赖变化而静默退出,你会以为一切正常。所以我们在里面加了一行心跳:

# 每次运行都写一行心跳,超过 15 分钟没有新行就告警
echo "$(date +%s) check=ok" >> /var/log/ops/finalize-guard.heartbeat
tail -1 /var/log/ops/finalize-guard.heartbeat

监控端只要判断「最后一行的时间戳距今超过 15 分钟」,就能发现守护自己挂了。这个思路适用于所有「自愈型」脚本:保护者也需要被保护,否则它坏掉的那天,正好是你最需要它的时候。

六、能改模板就别改产物

收尾脚本本质上是在跟生成器打补丁,属于「知道会坏所以每次补」。更彻底的做法是把四项定制写进生成器的模板,让它渲染出来就带着。

我们试过改模板,结论是:安全头这类「纯追加」的适合进模板;证书路径、真实 IP 这类和部署环境强相关的,留在收尾脚本里更稳,因为模板是产品的一部分,升级时会被覆盖。

所以最终是分层:模板管「所有站点都一样的」,收尾脚本管「这套环境特有的」,巡检管「有没有被意外抹掉」。三层各管一件事,比把全部希望寄托在任意一层都可靠。

小结

  • 配置生成器是「整份重写」,不是「改一行」。排查时找「模板缺什么」,不要找「哪一行变了」。
  • 生成失败往往整份回滚,表现是「保存成功但没变化」。先补全让它能过 nginx -t,再谈定制。
  • 定制项按「标记 + 整段替换」注入,保证幂等;直接 sed 追加跑两次就重复。
  • 每次改配置前备份、改完 nginx -t && nginx -s reload,语法不过就不 reload。
  • 收尾脚本解决「记得跑」,巡检解决「别人点了保存」。两者都要有,且巡检窗口越短越好。
  • 能进模板的进模板,环境相关的留脚本,剩下交给巡检——三层分工,别指望单点可靠。
查看「中间件」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。

控制台点一次保存,四项定制全被抹掉:配置生成器的收尾 - 起航社区