Files
micro_servers/waf/README.md
T
qing dcfdbcb9f2 docs(waf): 更新 README 文档与代码实现对齐
- 新增「安全响应头」章节,列出 7 个 WAF 注入的响应头及说明
- 修正「已知限制」中响应体检测描述,明确响应头检测已启用
- 澄清拦截日志等级区分(WARNING 命中拦截 / ERROR 加载错误)及 extra 字段类型
- 补充规则 ID 分段表的 CRS 可选说明
- 更新目录结构列表补充 logclient 子包
- 请求处理流程新增 ModifyResponse 步骤
2026-07-30 11:49:10 +08:00

12 KiB
Raw Blame History

waf — Web 应用防火墙网关

基于 Go + coraza 的 WAF 网关,部署在 gateway 之前,终止 TLS 并按 SecLang 规则库匹配请求,未命中则转发到 gateway。

客户端 ──HTTPS──▶ waf:443 (终止 TLS + coraza 规则匹配) ──明文 HTTP──▶ gateway:8443 (鉴权 + 反代)

为什么是 Go

  • coraza-waf/coraza 是 ModSecurity SecLang 的 Go 实现,100% 兼容 OWASP CRS v4
  • net/http/httputil.ReverseProxy 标准库反向代理,零第三方依赖
  • goroutine + net/http 吞吐量比 Python 高一个数量级
  • go build 出单二进制,core/init.txt 直接拉起

极客友好的规则系统

操作 命令
编辑规则 vim rules/local/custom.conf
校验语法 ./waf.bin -test rules/local/custom.conf
列出所有规则 ./waf.bin -list
测试输入 ./waf.bin -check "/?id=1' OR '1'='1"
热加载(不重启) kill -HUP $(pgrep waf)
临时禁用某文件 mv rules/local/x.conf x.conf.bak && kill -HUP $(pgrep waf) (引擎仅加载 .conf 后缀)

规则文件组织

rules/
├── waf.conf              # 主入口:基础配置(SecRuleEngine / SecRequestBodyLimit 等)
├── local/                # 本地自定义规则(ID 段 100000-199999)
│   ├── custom.conf       # 示例:SQL注入/XSS/路径遍历/命令注入/扫描器UA
│   └── exceptions.conf   # 误报白名单(SecRuleRemoveById / ctl:ruleRemoveById)
└── crs/                  # OWASP CRS(不入库,由 _fetch_crs.sh 拉取,可选)
    ├── coraza.conf       # CRS 入口(请求体处理器初始化 + tx.crs_setup_version)
    ├── LICENSE           # Apache-2.0
    └── rules/*.conf      # CRS 规则文件

注意:engine 不使用 coraza 的 Include 指令(其路径相对 cwd 解析易出错),改为在 engine.Reload 中自行扫描目录、逐个 WithDirectivesFromFile 加载。

规则加载顺序

由 engine.go:50-89 Reload 控制,固定顺序:

  1. waf.conf — 基础配置
  2. local/*.conf — 本地自定义规则(按文件名字母序)
  3. crs/coraza.conf — OWASP CRS 入口(仅当文件存在时加载)
  4. crs/rules/*.conf — OWASP CRS 规则(按文件名字母序)

规则 ID 分段

ID 段 用途
100000-199999 本地自定义规则
200000-200006 CRS 请求体处理器初始化(XML/JSON,由 crs/coraza.conf 定义,仅当启用 CRS 时存在)
900000-999999 OWASP CRS 规则(由 crs/rules/ 提供,不手改,仅当启用 CRS 时存在)

规则语法(SecLang)

SecRule REQUEST_URI|ARGS "@rx (?i)union[\s/*]+select" \
    "id:100001,phase:1,deny,status:403,log,msg:'SQL injection: UNION SELECT'"

字段说明:

  • REQUEST_URI|ARGS 匹配目标(URI / 参数 / Header / Cookie 等)
  • @rx ... 正则匹配(也支持 @pm、@contains、@beginsWith 等)
  • phase:1 请求头阶段 / phase:2 请求体阶段 / phase:3 响应头 / phase:4 响应体
  • deny,status:403 命中后动作
  • msg 命中日志描述

部署形态

WAF 必须终止 TLS 才能看到明文 body,因此证书需从 gateway 迁到 WAF:

  1. 把 gateway 的 full_chain.crt / server.key 复制到 waf/certs/
  2. gateway 改为监听 127.0.0.1:8443,关闭 TLS(仅本机)
  3. waf 监听 :443,转发到 http://127.0.0.1:8443

配置

config.yaml:

listen: ":443"
tls_cert: "./certs/full_chain.crt"
tls_key:  "./certs/server.key"

# gateway 内部 HTTP 端口(gateway 需改为监听 127.0.0.1:8443 不启用 TLS)
upstream: "http://127.0.0.1:8443"

# 规则根目录,主入口为 <rules_dir>/waf.conf
rules_dir: "./rules"
字段 默认值 说明
listen :443 监听地址
tls_cert / tls_key — TLS 证书与私钥路径
upstream — gateway 地址,必填(启动时硬性校验,缺失将导致进程退出)
rules_dir ./rules 规则根目录

目录结构

waf/
├── main.go                  # 入口:服务模式 + CLI 子命令
├── config.yaml              # 配置
├── go.mod
├── internal/
│   ├── engine/
│   │   └── engine.go        # coraza 封装:加载/热加载/列举/单测/输入检测
│   ├── logclient/
│   │   └── logclient.go     # Go 版 pymsg 日志客户端(异步批量推送)
│   └── proxy/
│       └── proxy.go         # ReverseProxy + WAF 事务接入 + 安全响应头注入
├── rules/
│   ├── waf.conf
│   └── local/
│       ├── custom.conf
│       └── exceptions.conf
├── _set.sh                  # 编译(含 go mod tidy + download + build)
├── _run.sh                  # 运行
├── _goproxy.sh              # 设置 GOPROXY 国内镜像(由 _set.sh 调用)
├── _fetch_crs.sh            # 拉取 OWASP CRS(可选)
├── .gitignore
└── README.md

安装

# 1. 安装 Go 1.21+(实际要求 1.22+,见 go.mod)
brew install go    # macOS
# 或 sudo apt install golang-go  # Debian/Ubuntu

# 2. 编译(_set.sh 会自动设置 GOPROXY 国内镜像 + GOTOOLCHAIN=local)
bash _set.sh

# 3. 准备证书
mkdir certs && cp /path/to/full_chain.crt /path/to/server.key certs/

# 4. 运行
bash _run.sh

国内环境:_set.sh 会调用 _goproxy.sh 自动设置 GOPROXY=https://goproxy.cn,direct 与 GOSUMDB=sum.golang.google.cn;并强制 GOTOOLCHAIN=local 避免 Go 1.21+ 自动从 proxy.golang.org 下载 toolchain 超时。用户已显式配置非默认代理时尊重不覆盖。

接入 core 自启动

在 core/init.txt 增加:

bash /opt/micro_servers/waf/_run.sh

并把 gateway 改为内部 HTTP 端口(不再监听 443)。

安全响应头

WAF 在边缘层统一注入安全响应头(proxy.go:54-77 setSecurityHeaders),覆盖所有出站响应(包括正常转发、命中拦截、上游 502)。下游服务(static/dnfs 等)的同名头会被 WAF 覆盖,避免 CSP 绕过。

Header 值 说明
X-Frame-Options SAMEORIGIN 允许本站 iframe 嵌套(登录页条例弹窗),防跨站点击劫持
X-Content-Type-Options nosniff —
X-XSS-Protection 1; mode=block 现代浏览器已废弃,仅兼容旧浏览器
Referrer-Policy strict-origin-when-cross-origin —
Permissions-Policy camera=(), microphone=(), geolocation=() —
Strict-Transport-Security max-age=15552000 HSTS 半年期,不含子域
Content-Security-Policy default-src 'self'; script-src 'self' 'unsafe-inline'; ... 暂保留 'unsafe-inline' 以兼容 forgot.html 与 games/*.html 的内联脚本

拦截日志

命中规则时 block() 经 logclient(Go 版 pymsg 客户端,见 internal/logclient/)异步批量推送到 db:8005 的 Logs 表(与其他服务的集中日志同链路;标准日志已静默,stdout/journalctl 中看不到拦截记录):

  • 规则命中拦截(block 触发):service=waf、level=WARNING
  • 规则加载/解析错误(LogError 回调触发,引擎初始化/热加载时的语义错误):service=waf、level=ERROR

block() 推送的日志 extra 字段(JSON 字符串):

字段 类型 说明
rule_id int 命中规则 ID
ip string RemoteAddr 原始值,含端口号(如 1.2.3.4:54321)
method string 请求方法
path string 请求路径
query string 查询字符串
ua string User-Agent
cost_ms int64 处理耗时(毫秒)

fail-safe:db 不可用时日志静默丢弃,不影响拦截动作本身。

查询最近的拦截记录:

sqlite3 /opt/micro_servers/db/database.db \
  "SELECT ts, message, extra FROM Logs WHERE service='waf' AND level='WARNING' ORDER BY id DESC LIMIT 20;"

聚合统计命中规则:

sqlite3 /opt/micro_servers/db/database.db \
  "SELECT json_extract(extra,'\$.rule_id') AS rule, COUNT(*) FROM Logs \
   WHERE service='waf' AND level='WARNING' GROUP BY rule ORDER BY 2 DESC;"

查看规则加载/解析错误:

sqlite3 /opt/micro_servers/db/database.db \
  "SELECT ts, message, extra FROM Logs WHERE service='waf' AND level='ERROR' ORDER BY id DESC LIMIT 20;"

启用 OWASP CRS(可选)

通过 Go module proxy 拉取(不依赖 GitHub 直连,国内可用):

bash _fetch_crs.sh            # 拉取最新版
# 或
bash _fetch_crs.sh v4.25.0    # 拉取指定版本

脚本会:

  1. 通过 GOPROXY=https://goproxy.cn,direct 下载 github.com/corazawaf/coraza-coreruleset/v4 到模块缓存
  2. 复制规则文件(*.conf + *.data)到 rules/crs/rules/
  3. 自动生成 rules/crs/coraza.conf(含请求体处理器初始化规则 200000-200006 与 tx.crs_setup_version)
  4. 复制 Apache-2.0 LICENSE

完成后 rules/crs/coraza.conf 存在,engine.Reload 会自动检测并加载,无需改 waf.conf。直接热加载即可生效:

kill -HUP $(pgrep waf)

校验加载结果:

./waf.bin -list | head

rules/crs/ 整个目录已在 .gitignore 中,不入库。如需升级 CRS,重跑 _fetch_crs.sh 即可。

请求处理流程

proxy.go:81-138 ServeHTTP:

  1. ProcessConnection + ProcessURI + 添加请求头(含 Host)
  2. ProcessRequestHeaders → 命中即 block 返回 403
  3. 读全量请求体 → ReadRequestBodyFrom → ProcessRequestBody → 命中即 block
  4. httputil.ReverseProxy.ServeHTTP 转发到 gateway
  5. 捕获响应头 → ProcessResponseHeaders
  6. ModifyResponse 统一注入安全响应头(覆盖下游同名头)

Director 改写(proxy.go:36-46):剥离 RemoteAddr 端口,设置 X-Forwarded-For 与 X-Forwarded-Proto: https,供 gateway 的 RateLimiter 取真实客户端 IP。

ErrorHandler(proxy.go:48-53):upstream 不可达时返回 502,同时注入安全响应头,不暴露内部堆栈,仅服务端 log。

已知限制

  • 请求体全量读入内存检测,10MB 上限(SecRequestBodyLimit)。大文件上传场景需调大或加白名单。
  • 响应头检测已启用(ProcessResponseHeaders),响应体检测未启用(未 ProcessResponseBody,缓冲整个响应延迟成本高)。SQL 报错回显等响应体泄漏检测缺失。
  • 限流/黑白名单未实现,可后续作为 coraza 规则或独立中间件加。
  • http.Server 未设 ReadTimeout / WriteTimeout / IdleTimeout,存在慢连接攻击风险。
  • TLS 仅设 MinVersion: TLS1.2,未限定 cipher suites,允许弱加密套件。
  • X-Forwarded-For 直接覆盖原值,未按 RFC 7239 追加;当前 gateway 也只取第一个 IP,无实际影响,但语义不严格。
  • CSP 仍含 'unsafe-inline'(为兼容 forgot.html 与 games/*.html 的内联脚本),待前端清理后可收紧。
  • HSTS 已启用(半年期,不含子域),站点全量 HTTPS 覆盖后可按需收紧。

依赖

  • Go 1.22+(见 go.mod,锁 go 1.22.0)
  • github.com/corazawaf/coraza/v3 v3.3.0(锁版本,避免 v3.7.0 要求 Go 1.25+)
  • gopkg.in/yaml.v3 v3.0.1

许可证

MIT License