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_KEY 32 字节,明文永不落库。
  • 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:

  1. 不自主写操作。智能体的写工具(封 IP、调策略)只产"提案",人工在审批队列里点了通过才执行,且禁止自审自批。
  2. 不进数据面。waf-engine 对智能体零感知,智能体再失控也碰不到流量路径。
  3. 不替代规则引擎。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/ 目录下有比文章详细得多的设计文档和规则文件,欢迎对着代码挑毛病。

Snipaste_2026-10-24_09-15-14

Snipaste_2026-10-24_09-13-52

posted @ 2026-10-24 09:19  cliffdev  阅读(3)  评论(0)    收藏  举报