服务器配置管理实战:每天一次生效快照、etckeeper 纳管 /etc 与配置漂移周巡检全流程

做久了个人站,你会发现真正让人崩溃的不是硬盘坏了,也不是流量掉了,而是那种「明明什么都没改,两天后才发现少了东西」的失忆。某个 CSS 文件被删了,某个 301 规则被注释掉了,某个 Nginx 的 gzip 级别被人从 6 调回了 1,某段统计代码悄悄消失——没有告警,没有日志,只有几天后一条来自搜索引擎的抓取异常曲线,把你从被窝里叫起来。

我在自己的小站上吃过一次这类教训。当时为了压一个紧急的 PHP 报错,我临时改了 /etc/nginx/conf.d/ 下的站点配置,把一段 location ~* \.(js|css)$ 的缓存头整块换掉,改完页面正常,我就去睡了。三天后我才发现静态资源的 Cache-Control 变成了 no-cache,CDN 回源率翻了三倍,季度流量包提前用掉了六成。配置本身没错,错的是我根本不知道「原来那段是什么」。从那次之后,我开始认真做一件事:给服务器的配置目录建一条时间线。

为什么服务器配置天然容易「无痕丢失」

网站代码有 Git,这是大家默认的。但 /etc 下的东西几乎没人管。原因很朴素:

第一,服务器上的改动往往发生在最不适合记录的场合——凌晨两点、正在排障、SSH 窗口只开着一个、脑子里只有一个念头「先让它跑起来」。没人会在那个时候去 git commit。

第二,配置的「正确状态」不在文件里,而在人脑和运行时的进程里。nginx -T 输出的才是真正生效的配置,磁盘上的文件可能被 include 覆盖、被注释、被别人后来加的文件抢先。你改了文件,但生效的可能不是它。

第三,很多改动是「删除型」的。加东西容易发现,少了一行 add_header 或者少了一个 rewrite,除非你天天盯着,否则三个月都不会有人知道。而删除型改动往往是最致命的:安全头没了、强制跳转没了、防盗链没了。

所以配置管理的目标不是「备份文件」,而是回答三个问题:什么时候变的?谁变的?变之前长什么样?

第一层:每天一次「生效配置」快照

不要急着上复杂工具,先把最基础的一件事做对:每天固定时间,把真正生效的配置导出来存一份。这一步的性价比极高,因为它同时解决了「改错怎么回滚」和「当时是什么样」。

Nginx 的 -T 参数会把主配置和所有 include 的文件合并打印出来,这是唯一可信的「运行时真相」:

nginx -T > /var/backups/conf/nginx-$(date +%F).conf 2>/dev/null

但只有这一行不够,因为它丢了两样东西:注释和文件边界。-T 的输出里,每个文件前面有一行 # configuration file /etc/nginx/...,这行要保留,否则你没法知道某段 server 块属于哪个站点文件。另外,PHP-FPM 和 MySQL 的配置不在里面,得单独导出:

#!/bin/bash
# /usr/local/bin/snap-conf.sh
set -u
D=/var/backups/conf/$(date +%F)
mkdir -p "$D"

nginx -T > "$D/nginx-effective.conf" 2>/dev/null
cp -a /etc/nginx "$D/nginx-etc"

for v in $(ls /etc/php/*/fpm/pool.d/ 2>/dev/null); do :; done
cp -a /etc/php "$D/php" 2>/dev/null

mysql --defaults-file=/etc/mysql/debian.cnf -N -e \
  "SELECT CONCAT('SET GLOBAL ',VARIABLE_NAME,'=',QUOTE(VARIABLE_VALUE),';')
   FROM performance_schema.global_variables
   WHERE VARIABLE_NAME NOT LIKE 'last_%';" \
  > "$D/mysql-globals.sql" 2>/dev/null

gzip -9 "$D/nginx-effective.conf"
find /var/backups/conf -maxdepth 1 -type d -mtime +30 -exec rm -rf {} +

关于 MySQL 那一句多说一句:performance_schema.global_variables 里既有静态变量也有动态变量,直接生成 SET GLOBAL 脚本在静态变量上会报错。真正要恢复时不能用整段回放,而是先 diff 出差异再挑着执行。这份快照的价值在于「比对」,不在于「照抄执行」,这一点心里要清楚,否则真出事的时候会踩第二次坑。

这段脚本挂到 cron 或者 systemd timer 上,每天凌晨跑一次。文件非常小,一个站一年也就几 MB。它不会告诉你「谁改的」,但它能告诉你「昨天和今天差了哪几行」,这已经能省掉 80% 的排障时间。

第二层:用 git 把 /etc 真正纳管起来

快照解决了「变化可见」,但还不够优雅:每天一个目录,想找「这段配置是什么时候引入的」要翻很多份。这时候就到了 etckeeper 的场合。它做的事情很单纯:在你的机器上初始化一个 /etc/.git,然后用 hook 挂到 apt/yum 的操作上,任何软件包安装、升级导致的配置变化都会自动提交一次,附上包名和版本。

apt-get install -y etckeeper
# /etc/etckeeper/etckeeper.conf 里设置
# VCS="git"
# AVOID_DAILY_AUTOCOMMITS=1
# AVOID_COMMIT_BEFORE_INSTALL=0

etckeeper init
etckeeper commit "init: baseline"

装完之后,每次 apt upgrade 都会在 /etc 里留一条提交记录,提交信息形如「committing changes in /etc after apt run」并附带包名。这解决了一大类事故:某个升级把 nginx 的默认配置覆盖了,或者是把 sshd_config 的 PermitRootLogin 悄悄改回去了,你在 git log 里一眼就能看到。

但对「我手动改文件」这种情况,etckeeper 是不知道的,除非你自己提交。所以真正好用的姿势是把它和自己的操作习惯绑在一起:改完配置、验证通过、nginx -t 通过之后,立刻提交。

cd /etc
git add -A
git commit -m "nginx: 给静态资源恢复 30 天缓存头,关闭 no-cache 回退" -q

这条命令最好写成一个别名或者小脚本 cfcommit,让它比「不提交」更省事,人才会真的用。我的做法是把它接在 nginx -t 之后,如果校验失败就不提交,同时打印出 git diff 方便回看:

#!/bin/bash
# /usr/local/bin/deploy-conf.sh
set -e
nginx -t 2>>/tmp/nginx-t.log || { echo "配置校验失败,未提交"; tail -5 /tmp/nginx-t.log; exit 1; }
systemctl reload nginx
cd /etc
git add -A
git commit -m "conf: $(date '+%F %T') $*" -q || echo "无变化"
git log --oneline -3

注意这里的顺序:先校验、再 reload、最后提交。提交在 reload 成功之后,这样 git 里的每一个提交都对应一个「曾经真的生效过」的状态,而不是一份可能把站打挂的半成品。这个顺序看起来是小事,但你回滚的时候会感谢自己——git checkout 一个从未生效过的中间态,是另一种灾难。

第三层:把「谁的改动」和「改了什么」分开记录

git 记录的是内容差异,但如果你不是一个人运维,光有内容不够。实际排障中最常见的问题是「这个改动是我做的还是别人做的,还是某个脚本自己做的」。这时候需要 auditd 补上行为轨迹。

# /etc/audit/rules.d/site-conf.rules
-w /etc/nginx/ -p wa -k nginx_conf
-w /etc/php/ -p wa -k php_conf
-w /etc/mysql/ -p wa -k mysql_conf
-w /etc/cron.d/ -p wa -k cron_jobs
-a always,exit -F arch=b64 -S execve -F path=/usr/sbin/nginx -k nginx_exec

加载规则:augenrules --load,然后用 ausearch -k nginx_conf -i 查。输出里会包含 auid(登录用户)、pid、comm 和 syscall,能区分「是人手改的」还是「是某个脚本改的」。配合 git 的 diff,一个完整的因果链就成立了:三点十分某人改了文件,三点十一分 reload,三点十二分缓存头变化。

这套组合还有一个隐性收益:当你的服务器被入侵时,auditd 的 -w /etc/nginx/ -p wa 规则往往能留下攻击者改配置的证据。很多挂马案例的最后一步就是在 nginx 配置里加一段 sub_filter 注入脚本,这条规则正好覆盖。

第四层:定期做「配置漂移」比对

前三层都是「事情发生时记录」,还有一类问题是「事情早就发生了,但你不知道什么时候」。比如你上个月为了测试临时注释掉了一段 add_header Strict-Transport-Security,忘了恢复。这种改动在 git 里也就是一行注释,翻 log 很难注意到。

我的做法是每周做一次「期望态 vs 实际态」的检查,不是比对文件,而是比对关键行为。写一个几十行的 shell,逐条断言那些「绝对不能变」的东西:

#!/bin/bash
# /usr/local/bin/conf-drift-check.sh
F=0
chk(){ if eval "$2"; then echo "OK   $1"; else echo "FAIL $1"; F=1; fi; }

chk "HTTPS 强制跳转" \
    "curl -sI http://127.0.0.1/ -H 'Host: www.example.com' | grep -q '301'"
chk "HSTS 头存在" \
    "curl -sI https://www.example.com/ -k | grep -qi 'strict-transport-security'"
chk "静态资源缓存 30 天" \
    "curl -sI https://www.example.com/style.css -k | grep -qi 'max-age=2592000'"
chk "gzip 开启" \
    "curl -s -o /dev/null -w '%{size_download}' -H 'Accept-Encoding: gzip' https://www.example.com/ | awk '{exit !($1>0)}'"
chk "默认站点不泄漏" \
    "! curl -s http://127.0.0.1/ -H 'Host: unknown.example' | grep -q 'Welcome to nginx'"
chk "PHP 版本未被暴露" \
    "! curl -sI https://www.example.com/ -k | grep -qi 'x-powered-by'"

[ $F -eq 0 ] && echo "全部通过" || echo "存在漂移,请检查"

这个脚本的妙处在于它检查的是结果而不是文件。哪怕某天你重构了 nginx 配置、把 HSTS 从头文件挪到了别的 server 块,只要最终行为对,检查就通过;反过来,哪怕文件看起来完好,只要实际响应变了,它就会报红。对于「只关心站能不能正常工作」的个人站长,这种黑盒断言比逐行 diff 更贴近需求。

把它挂到每周一的 timer 上,输出通过邮件或者 ntfy 推给自己,五分钟就能看完。

回滚:真正出事那天怎么用

前面所有的记录,最后都要落到一个动作上:出事了,把配置退回去。这一步要提前演练,别等真出事再想。

我的回滚顺序是这样的,从轻到重:

第一步,单文件回退。如果已经知道是哪个文件的问题,直接 git log --oneline -- /etc/nginx/conf.d/site.conf 找到上一个好版本,git checkout HEAD -- 文件路径,然后 nginx -t && nginx -s reload。这是最快的路径,一分钟内完成。

第二步,整目录回退。如果同时改了好几个文件又说不清是哪个,git stash 当前状态,git checkout HEAD -- .,校验后 reload。回退完记得 git commit 一次「revert to xxx」,否则下一次 diff 会很难读。

第三步,回退失败时的兜底。如果 reload 之后站直接 502,说明回退的版本和当前的 PHP-FPM 或者后端不兼容。这时候不要慌,nginx -t 通过但服务异常,通常是因为上游地址变了。用 ss -lntp | grep php 确认 FPM 的监听地址,再看 git diff 里有没有动过 fastcgi_pass。这一步的经验是:永远在改 fastcgi_pass 之前先确认新地址是通的,用 curl --unix-socket 或者 nc -z 测一下,别改完再 reload。

第四步,最坏情况。服务器整个连不上,配置也回不去。这时候每天的快照目录就是救命稻草,/var/backups/conf/YYYY-MM-DD/nginx-etc 整目录 scp 回去覆盖,nginx -t 之后重启。这就是为什么第一层要做得简单可靠、不依赖 git 不依赖任何服务——它必须是最原始的那一层。

几个我踩过的具体坑

坑一:git 仓库权限。/etc 默认是 root 拥有,etckeeper init 之后目录会变成 root:root 0700,如果有服务以非 root 用户读取 /etc 下的配置,可能会读不到。装完之后用 ls -ld /etc 确认权限没变。这个坑我在一台机器上踩过,表现是 php-fpm 启动失败但报错含糊。

坑二:.git 目录被备份工具反复扫描。你的备份脚本可能每天同步 /etc,/etc/.git 里的对象会越滚越大。给备份加个 exclude,或者定期 git gc --aggressive 压一下。小机器上一年不整理,这个仓库能长到几百 MB。

坑三:密码和密钥进了 git。这是最要命的。/etc/nginx/ 里可能有 htpasswd 文件,/etc/php/ 里可能有数据库密码,/etc/mysql/debian.cnf 本身就是明文的。既然整目录纳管,就必然会提交进去。做法有两个:一是 .gitignore 掉敏感文件但保留一个 .example 模板;二是把这个仓库放在本地、只推到自己的私有机器的裸仓库里,永远不要推到公开的 GitHub。我选后者,因为前者会让你在恢复时找不到那份缺失的配置。

坑四:reload 不是重启。很多人以为 systemctl reload nginx 就等于配置生效,其实 reload 只是重新加载配置并平滑切换 worker,某些指令(比如 listen 的变更、部分模块参数)必须 restart 才生效。改完 reload 之后行为没变,先想想是不是指令本身不支持热加载,别急着怀疑自己改错了。

一份可以直接抄的落地清单

如果你只想花一个下午把这件事做起来,按这个顺序:

一,写 snap-conf.sh,cron 每天 04:00 跑,保留 30 天。二,apt install etckeeper && etckeeper init && etckeeper commit "baseline",然后把手动改配置的习惯改成「改完跑 deploy-conf.sh」。三,加四条 auditd 规则,重点是 /etc/nginx/ 和 /etc/cron.d/。四,写 conf-drift-check.sh,先放五条最常见的断言,每周一跑。五,演练一次回滚,找一个不重要的配置项故意改坏,走一遍 git 回退流程,确认自己闭着眼睛也能做。

整套东西加起来不到一百行,没有任何外部依赖,不占什么资源。它不会让你的站变快,也不会让排名上升,但它会在某一天凌晨,替你挡掉一次「明明什么都没改」的沉默事故。对个人站长来说,这种确定性比任何花哨的功能都值钱。

配置管理的本质不是工具,是一种态度:承认自己的记忆不可靠,承认深夜的自己会犯错,然后把「可恢复」这件事设计进系统里。等你哪一天真的用上了那份额外的快照,你会觉得当初那个下午花得真值。

Last modification:October 17th, 2026 at 08:25 pm

Leave a Comment