04-WAF自己不能是短板
WAF 自己不能是短板
ShieldWAF 系列文章第四篇,2026-10。前三篇讲的是防别人,这篇讲防自己:管理 API 的防重放、密钥与审计、AI 智能体的边界,以及部署运维里用真金白银换来的教训。
有个事实说出来有点讽刺:WAF 管理平台掌握着它保护的所有站点的证书私钥,能改所有防护策略,能看到流量里的敏感数据。对一个攻击者来说,它比被保护的业务本身更诱人。
所以这个项目里有一条自我要求:数据面的每一分投入,管理面至少要配得上同等强度。
写接口的防重放签名
场景:管理员的某个请求从日志、代理或内网抓包里泄露了,攻击者原样重放——没有防护的话,改策略、加黑白名单、换证书,全都是一次 POST 的事。
所有状态变更接口(POST/PUT/PATCH/DELETE)过统一中间件,校验三个头:
X-Signature = hex( HMAC-SHA256(secret,
METHOD \n
PATH?QUERY \n
timestamp \n
nonce \n
sha256_hex(body) ))
时间窗 ±5 分钟,nonce 进 Redis 防重放。前端所有写请求的封装、所有直调 API 的脚本和测试都要实现这套签名——写接口接一个,签名就要跟一个。这是刻意制造的摩擦:新写接口的人必须先读懂协议。
两个附带约定:GET 不签名,读操作的风险由 TLS 和权限体系兜底,全签一遍的成本不值;multipart 的 body 哈希很难逐字节对齐,所以上传类接口一律 JSON + base64。
密钥、审计与登录
- 证书私钥走 KMS 式信封加密,主密钥
WAF_CERT_ENCRYPTION_KEY32 字节,明文永不落库。 - AI 智能体的 LLM API Key 用 AES-256-GCM 加密落库,AAD 绑定租户——别的租户拿到密文也解不开;页面上只显示打码后的内容。
- 审计日志 append-only + SHA-256 哈希链:每条记录对前一条的哈希做链式签名,事后没人能悄悄改历史。安全平台自己的操作记录,完整性优先于一切。日志保留期审计类不少于 365 天。
- 登录是 JWT 双 Token(access 15 分钟 + refresh 24 小时),5 次失败锁 30 分钟。企业场景走 SSO,OIDC/OAuth2/SAML 2.0 三协议,其中 SAML 做了 SP/IdP 双模式、断言强校验和重放防护。
AI 智能体的三条铁律
我们把 LLM 接进了安全运营:告警自动分诊、自然语言查安全事件、给处置建议。听起来危险,确实危险,所以立了三条铁律,原文在 docs/AI-SECURITY-AGENT-PLAN.md:
- 不自主写操作。智能体的写工具(封 IP、调策略)只产"提案",人工在审批队列里点了通过才执行,且禁止自审自批。
- 不进数据面。waf-engine 对智能体零感知,智能体再失控也碰不到流量路径。
- 不替代规则引擎。LLM 的输出永不参与拦截决策。
配套机制是一整套:写工具参数过 JSON Schema 白名单(IP 字段除了 pattern 还有 net.ParseIP 二次校验)、工具结果用 <untrusted-observation> 标签定界防提示注入、请求体和请求头默认不出网、每租户每天 20 万 token 配额、单轮最多 8 步工具调用、分诊限速每小时 30 条。
发布节奏也是三重开关默认全关:总开关 → 租户级开关 → shadow_mode(只写建议、不产提案),灰度期默认 shadow。LLM 走 OpenAI 兼容协议,接 DeepSeek/GLM/vLLM 都行,另有一个 mock provider 支撑离线演示和 CI——智能体的注入测试集和工具校验矩阵在 CI 里是阻断项,不依赖真模型。
降级无处不在,但降级必须可见
引擎对每个外部依赖都有降级路径:PG 挂了事件落本地 WAL 文件,Redis 挂了限速退化进程内,控制面挂了用最后同步的策略继续防。这套设计跑起来之后,我们花了一段时间才真正理解它的另一面。
2026 年 10 月的一起生产事故:某套外置数据库的环境里,compose 里引擎服务的 DB 密码指到了错误的变量。引擎连不上库,事件静默落了本地 jsonl 兜底——服务"健康",流量正常,防护正常。症状只有两个:前端日志页空白;兜底文件持续增长,但没有自动回放机制,等发现时只能手工补录。
复盘写进了部署规范,两句话值得原样贴出来:
日志页空但流量正常 → 先看引擎日志的
db_initialized与pool unavailable计数。
验证数据库密码必须走真实网卡地址——容器内 127.0.0.1 会命中 pg_hba 的 trust 规则,测不出密码错误。
这次事故把一条原则钉死了:每个降级路径要么有告警,要么有巡检,否则它就是下一个静默故障的温床。 同类教训压测时也踩过一次:引擎没配 DATABASE_URL 时合规事件退化 jsonl 兜底,高 RPS 压几个小时写了 80G,差点写满整盘。兜底逻辑是为了"别丢数据"设计的,但它自己也需要被观测。
部署:把"能跑"变成"可复现"
部署工具 deploy.py 零第三方依赖(只用标准库),TOML 写多环境配置,渲染出 .env(0600 权限)+ compose + PG/Redis 配置 + 备份脚本,本地或 SSH 远端一键 up。up 之前自动快照上一版配置,支持回滚;密钥留空自动生成、已存在则复用(幂等),轮换要走显式的 secrets --rotate。
构建侧有几个省时间的机制,细节不展开,讲两个有意思的。
两个 Go 服务镜像的"模块下载层"做到了逐字节一致——经典构建器按"父层 + 指令"全局匹配层缓存,链路一致则后构建的镜像直接命中先构建方拉好的依赖,go mod download 只在 go.mod 变动时重跑。为此 control-plane 的 Dockerfile 里留着几个自己并不使用的 ARG,注释写明"为对齐链路而存在"——这种代码看着莫名其妙,删了构建立刻慢十分钟。
另一个坑是 PowerShell 5.1:脚本在 $ErrorActionPreference='Stop' 下对原生命令做 stderr 重定向,会把 stderr 行包装成异常终止脚本。我们的 buildx 探测逻辑(不存在就降级 legacy builder)里,"探测失败"这个预期分支自己先炸掉了脚本,降级永远执行不到。解法是把所有探测类调用包进一个临时降 ErrorActionPreference 的函数。Windows 运维脚本的这些暗礁,只能靠踩。
尾声
安全产品有个特殊之处:它的第一客户是自己。策略能不能灰度、误报能不能快速逃逸、审计能不能自证清白、故障能不能自己先发现——这些能力做扎实了,产品对外给的承诺才立得住。
四篇写完了。仓库 docs/ 目录下有比文章详细得多的设计文档和规则文件,欢迎对着代码挑毛病。



浙公网安备 33010602011771号