AngieGuardian/angie-guardian

GitHub: AngieGuardian/angie-guardian

为 Angie Web 服务器设计的 WAF 与工作量证明 bot 防火墙 sidecar,通过自适应挑战和行为分析保护网站与 API 免受恶意流量和自动化攻击。

Stars: 2 | Forks: 1

# Angie Guardian 一个 Web 应用程序防火墙 (WAF) 和工作量证明 bot 防火墙,专为 [Angie](https://angie.software/) 编写,使用 Go 语言。 Guardian 保护托管在 Angie 上的网站和 API 免受恶意流量、 滥用 bot 和自动化攻击,同时允许合法访问者正常访问 网站。它允许受信任的请求,通过工作量证明来挑战可疑的客户端, 并明确阻断真实的威胁。 ## 功能特性 Guardian 将请求时的 WAF 与工作量证明挑战相结合。策略 可按域名进行配置,因此一个实例可以保护多个 vhost。 - **热重载 WAF:** 支持针对路径、查询参数、 User-Agent、HTTP headers 和 HTTP 方法的 RE2 关键词和正则表达式规则。 - **自适应 bot 防御:** 行为评分、蜜罐、IP 封禁、 GeoIP/声誉检查以及已验证 bot 的 DNS 验证。 - **工作量证明通行证:** 自适应的 SHA-256 浏览器挑战,随后是经过 低成本验证的 Ed25519 签名 cookie;并提供可选的无 JS 降级方案。 - **抗重放攻击:** 挑战为一次性使用,且与域名和 客户端 IP 绑定。 - **异常检测:** 可选的离线训练,基于 Angie 访问日志,支持在实时请求路径中进行 快速评分。 - **高可用部署:** 持久化密钥、用于多副本的共享存储、安全 重载,以及故障开放或故障关闭集成。 ## 快速开始 从 [发布页面](https://gitlab.melroy.org/melroy/angie-guardian/-/releases) 安装指定版本的 `linux-amd64` 或 `linux-arm64` 包, 然后按照发布优先的 [入门指南](https://angieguardian.org/guide/getting-started) 操作。 归档文件包含二进制文件、示例配置、WAF 启动规则、 Angie 片段以及 systemd unit;运维人员无需安装 Go 或检出代码仓库。 ``` # After downloading and extracting a pinned release: sudo install -Dm755 guardiand /usr/local/bin/guardiand ``` 如需使用容器和持久化状态,请参阅 [生产环境 Docker 指南](https://angieguardian.org/guide/production#docker)。 ## 文档 指南、示例以及完整的配置和 API 参考文档发布在 ****。 ## 工作原理 Angie 保持在请求路径中,并继续为站点现有的静态文件、 `try_files`、FastCGI 或反向代理 handler 提供服务。Guardian 作为 sidecar 决策 服务:Angie 向其发送无请求体的内部授权子请求,并根据 返回的允许、挑战或拒绝响应执行操作。第一张图展示了该 实时请求生命周期;第二张图则将支持性的策略、状态、 训练和运维平面与核心请求热路径区分开来。 ``` @startuml skinparam backgroundColor #F8FAFC skinparam shadowing false skinparam roundCorner 12 skinparam defaultFontName Sans-Serif skinparam sequence { ArrowColor #2563EB LifeLineBorderColor #475569 LifeLineBackgroundColor #F8FAFC ParticipantBorderColor #475569 ParticipantBackgroundColor #F8FAFC ParticipantFontColor #0F172A ActorBorderColor #475569 ActorFontColor #0F172A GroupBorderColor #64748B GroupFontColor #0F172A } hide footbox title Angie Guardian - Live Request Path actor "Visitor/Bot" as Client participant "Angie vhost" as Angie participant "guardiand" as Guardian participant "Original site handler\n(backend)" as Backend Client -[#2563EB]> Angie: Request Angie -[#2563EB]> Guardian: Can this request continue? alt ALLOW Guardian -[#15803D]> Angie: Allow Angie -[#15803D]> Backend: Continue to the site Backend -[#15803D]> Client: Site response else CHALLENGE Guardian -[#D97706]> Angie: Challenge Angie -[#D97706]> Client: Proof-of-work page Client -[#D97706]> Angie: Solve challenge Angie -[#D97706]> Guardian: Verify solution Guardian -[#D97706]> Angie: Grant signed pass Angie -[#D97706]> Client: Signed pass Client -[#15803D]> Angie: Retry original request Angie -[#15803D]> Guardian: Check signed pass Guardian -[#15803D]> Angie: Allow Angie -[#15803D]> Backend: Continue to the site Backend -[#15803D]> Client: Site response else DENY Guardian -[#B91C1C]> Angie: Deny Angie -[#B91C1C]> Client: Block request else GUARDIAN UNAVAILABLE - FAIL OPEN Angie -[#15803D]> Backend: Continue to the site Backend -[#15803D]> Client: Site response end note over Angie, Backend #E8F5E9 Remove the fail-open error_page mapping to choose fail-closed. end note @enduml ``` 同一个 sidecar 加载策略和密钥材料,拥有有状态保护,并 暴露独立的运维监听器。离线训练和可选的 集成为此支持平面提供数据,而无需介入实时请求 路径。 ``` @startuml skinparam backgroundColor transparent skinparam shadowing false skinparam roundCorner 12 skinparam defaultFontName Sans-Serif skinparam ArrowFontSize 11 skinparam activity { BorderColor #475569 FontColor #0F172A BackgroundColor #F8FAFC } skinparam partition { BorderColor #475569 FontColor #0F172A } partition #F5F3FF "Angie Guardian - Policy, State, Training, and Operations" { start if (Integration path?) then (full sidecar) fork -[#64748B,dashed]-> :Load policy\nConfig, WAF rules, keys, and threat data; <<#F8FAFC>> fork again -[#2563EB]-> :Optional offline training\nAccess logs -> anomaly model; <<#DBEAFE>> end fork -[#2563EB]-> :Activate a validated snapshot\nKeep the last good version if reload fails; <<#FFF7E6>> fork -[#64748B,dashed]-> :Stateful protection\nBlocks, challenges, and counters; <<#F1F5F9>> fork again -[#7C3AED]-> :Operations\nDashboard, API, health, and metrics; <<#F8FAFC>> fork again -[#B91C1C,dashed]-> :Optional nftables\nDrop known blocks before Angie; <<#FDECEC>> end fork stop else (optional WASM alternative) -[#9333EA,dashed]-> :Run stateless policy inside Angie\nStore-free WAF only; no PoW or shared state; <<#F3E8FF>> stop endif } @enduml ``` ## 性能表现 Guardian 专为 Angie 的授权热路径而构建。在 AMD Ryzen Threadripper 7960X 上使用 64 个连接进行的本地回环测试得出了以下吞吐量 结果(每次为 3 次固定负载运行的中位数,每次运行均启动全新 daemon 和存储; 挑战发布在其**加载后的稳态**下测量,此时进程内的两个 计数器缓存均已填满并超过容量,而不是针对空 存储进行测量): | 存储后端 | 允许请求/秒 | 回访客户端请求/秒 | 每秒发布挑战 | |---|---:|---:|---:| | 内存存储 (ephemeral) | 180,000 | 173,000 | 160,000 | | Pebble (async, 默认) | 182,000 | 171,000 | 61,000 | | Pebble (sync, 完全持久化) | 179,000 | 170,000 | 34,000 | | BuntDB (async) | 182,000 | 170,000 | 56,000 | | Redis/Valkey | 94,000 | 93,000 | 49,000 | 在每个后端中,读取路径始终保持在 93,000 请求/秒以上。挑战 发布属于写密集型,这解释了内存 与持久化存储之间的差异。请参阅 [负载测试指南](https://angieguardian.org/guide/load-testing) 了解场景、延迟百分位、方法论和复现命令。 ## 集成路径 Guardian 提供两种运行方式,它们共享同一个决策核心: - **Sidecar(默认,全功能)。** 一个通过 原生 `auth_request` 指令接入 Angie 的 Go daemon。这是完整的实现: 工作量证明和行为 IP 封禁使用其拥有的共享存储; 异常评分和已验证 bot 的 DNS 验证也仅限 sidecar 模式。建议从这里开始。 - **WASM 模块(可选,无状态 WAF)。** 将无存储检查(allowlist、 denylist、蜜罐、关键词/正则签名)编译为 WebAssembly 并 通过 Angie 的 WASM 支持在其进程内运行,适用于喜欢该 集成方式的运维人员。它仅限无状态的 WAF:工作量证明、行为封禁、 异常评分和已验证 bot 的 DNS 检查需要 sidecar。使用 `make wasm` 进行构建;请参阅 [USAGE.md](USAGE.md) 中的 "WASM module" 章节。 这两种路径共享相同的无存储匹配逻辑。它们的有状态结果 有所不同:在 sidecar 中,`challenge` 可以通过绑定的 PoW token 获得满足,并且 `block`/蜜罐命中会保持 IP 封禁状态;无状态的 WASM guest 对 其中任何匹配项均返回普通的拒绝。经过验证的 PoW token 永远不会使 sidecar 客户端免受 `deny` 或 `block` 签名检查,因此被盗用的 token 无法 绕过 WAF。 ## 运维 - **可观测性:** Prometheus 指标、健康/就绪检查、内置 [Grafana dashboard](deploy/grafana-dashboard.json) 以及 [告警规则](deploy/alerts.yaml)。 - **管理 API 和控制面板:** 受 token 保护的封禁管理、近期 决策、异常/配置状态,以及可选的隔离网络 Web 控制面板。 - **安全维护:** 热重载保持最后已知良好的配置处于激活状态; 签名密钥可以轮换,而不会立即使现有通行证失效。 - **灵活的状态管理:** 单实例可使用 `memory`、`pebble` 或 `buntdb`; 共享多实例部署可使用 `redis`/`valkey`。 请参阅 [USAGE.md](USAGE.md) 了解端点、身份验证、控制面板设置、重载 和密钥轮换的详细信息。 ## 安全性 Guardian 的 [安全模型和局限性](https://angieguardian.org/guide/threat-model) 详细说明了它能防御什么以及故意不防御什么。如需报告 漏洞,请参阅 [SECURITY.md](SECURITY.md);请勿公开提出 issue。 ## 许可证 [AGPL-3.0](LICENSE),© Melroy van den Berg ## 开发 以下内容适用于构建、测试或修改 Angie Guardian 的贡献者。运维人员可以在此停止,并使用上面链接的文档。 ### 测试 默认的测试套件是自包含的;仅在执行端到端测试时需要 Docker。 | 检查 | 命令 | 目的 | |---|---|---| | 单元测试 | `go test ./...` | 核心、存储和 HTTP 传输 | | 竞态检测 | `go test -race ./...` | 并发回归 | | 端到端 | `make e2e` | 真实的 Angie → guardiand → 后端技术栈 | | 模糊测试 | `make fuzz` | 不受信任的及热重载的解析器 | | 基准测试 | `go test -bench=. -benchmem ./core/... ./transport/http/` | 请求热路径 | | 内存分配门控 | `make bench-regress` | 对照 `allocs-baseline.txt` 检查热路径 `allocs/op`;这也是一项 CI 作业 | [端到端套件](test/e2e/) 涵盖了工作量证明、WAF 结果、 行为封禁、故障开放、指标和管理 API。CI 会在 `main` 和发布标签上运行它;在合并技术栈级别的更改之前,请在本地运行测试。 将 `testdata/fuzz/` 中有用的模糊测试崩溃提交为回归种子。 #### 填充控制面板 运行一个临时实例并从两个 shell 生成具有代表性的控制面板流量: ``` go run ./cmd/guardiand -config test/seed/guardian.seed.yaml make seed # two minutes make seed SEEDTIME=5m # optional longer run ``` 打开 。所链接的 [种子配置](test/seed/guardian.seed.yaml) 仅限内存使用,旨在用于 本地开发。`make seed` 会创建真实的流量混合;在进行 吞吐量测量时,请使用 `guardian-loadtest`。 ### 从源码构建 所需的 Go 工具链版本在 [go.mod](go.mod) 中指定。将三个 sidecar 二进制文件构建到 `dist/` 中,或者额外构建可选的 WASM 模块: ``` make build make wasm ``` 文档站点可以通过 `make docs-dev` 进行预览,或通过 `make docs` 进行构建。 ### 性能测试 `guardian-loadtest` 像 Angie 那样通过 keepalive HTTP 驱动 热路径,并报告吞吐量和 延迟百分位。 **场景**(前三个以读取为主;`challenge` 是唯一的 写密集型路径): | 场景 | 作用 | 每个请求的存储 I/O | |---|---|---| | `allow` | 普通请求,完整流水线,以“默认允许”结束 | 一旦封禁镜像完成初始化填充,嵌入式后端上无操作;`redis` 上有 1 次读取(直读模式,因此其他副本的封禁会立即生效) | | `token` | 解决一个 PoW 挑战,然后使用 cookie 大量请求 `/auth`(生产环境中的常见路径) | 与 `allow` 相同 | | `deny` | 位于黑名单的客户端 IP(拒绝 + 决策日志记录路径) | 无存储 I/O | | `challenge` | 为每个请求发布一个新的 PoW 挑战 | 通常是 1 次**写入**(挑战 CAS);在 [攻击模式](https://angieguardian.org/guide/attack-mode) 下,或者当该有状态写入失败时,发布是无状态的:发布时不写入,单次消费的写入转移到兑换阶段。限流和升级计数器在进程内计算,并在后台刷入 | **结果**(单节点,回环,64 连接,负载生成器共享相同的 CPU:AMD Ryzen Threadripper 7960X,24C/48T;Linux 6.17;Go 1.26.5; Redis 后端使用 Valkey 9;每次运行均使用全新的 daemon 和擦除的存储;每个单元格 运行 3 次的中位数)。读取使用 `-warmup 50000 -n 500000`;挑战使用 `-warmup 150000 -n 150000`,因此其预热会将两个 131k 条目的计数器缓存 推过容量上限,并且测量窗口是加载后的稳态,而不是空 存储所提供的快速冷启动。每个单元格是**吞吐量 / p50 / p99** (req/s 和每次请求的延迟): | 后端 | allow | token | deny | challenge (写入) | |---|---|---|---|---| | `memory` (ephemeral) | 180k / 0.13ms / 1.8ms | 173k / 0.16ms / 1.7ms | 157k / 0.14ms / 2.3ms | **160k / 0.29ms / 1.6ms** | | `pebble` (async, 默认持久化) | 182k / 0.13ms / 1.8ms | 171k / 0.15ms / 1.8ms | 154k / 0.14ms / 2.4ms | **61k / 0.90ms / 3.6ms** | | `pebble` (sync, 完全持久化) | 179k / 0.13ms / 1.8ms | 170k / 0.15ms / 1.8ms | 154k / 0.14ms / 2.4ms | **34k / 1.5ms / 5.3ms** | | `buntdb` (async, 单文件) | 182k / 0.13ms / 1.8ms | 170k / 0.14ms / 1.8ms | 155k / 0.13ms / 2.4ms **56k / 1.2ms / 4.8ms** | | `redis`·`valkey` (集群) | 94k / 0.64ms / 1.3ms | 93k / 0.64ms / 1.4ms | 162k / 0.13ms / 2.3ms | **49k / 1.2ms / 2.5ms** | (`buntdb` + `sync: true` 仅测得约 0.6k 次挑战写入/秒,因为 每次提交都进行 fsync 的单写入者存储速度就是那么慢,因此该组合在 启动时即被**拒绝**;如需同步持久性,请使用 `pebble` 配合 `sync: true`。) 每个后端在读取路径上都以极大的优势超过了 ≥50k req/s 的预算。 在**嵌入式**后端上,进程内的封禁镜像使存储成为 权威数据源,因此在初始扫描后,`allow`/`token` 根本不进行存储 I/O, 这就是为什么它们聚集在 ~154–182k 的原因。`redis`/`valkey` 是个例外:它 保持直读模式(为了跨副本的正确性,每个请求进行一次网络读取), 因此其 `allow`/`token` 结果较低(~93–94k),而 `deny`(无存储读取)保持快速。 **写入**路径是后端至关重要的部分。发布挑战会写入一条 CAS 记录,并且持久化后端吸收该操作的能力远优于同步的 单写入者存储: - **`pebble`**(默认持久化)在异步模式下可维持约 61k 次挑战写入/秒, 即使在完全持久化的 `sync: true` 模式下,依然能达到约 34k/秒。两者都远高于 同步 fsync 的单写入者存储。它是一个 LSM 引擎,因此写入会命中 WAL 和 memtable 并在后台被刷入。 - **`buntdb`** 在异步模式下接近 Pebble(~56k/秒),并将所有内容 存储在一个文件中,这使得备份更加简单。它是单写入者的,因此 `sync: true` 会在每次提交时进行 fsync 并导致性能骤降至约 600/秒,因此 guardiand 在该配置下 **拒绝启动**,并建议您使用 Pebble 以获得同步 持久性。 - **`memory`** 完全没有写入上限(~160k/秒),但在 重启时会丢失所有状态。 - **`redis`/`valkey`** 维持约 49k 次挑战写入/秒(与 嵌入式持久化后端相当),是**多实例**选项:它是 共享存储,允许负载均衡器后面的副本看到彼此的封禁 和单次消费标记。它为此牺牲了一些读取吞吐量(每个 请求进行一次网络读取)。上述嵌入式后端仅限单节点。请参阅 [存储指南](https://angieguardian.org/guide/production#choosing-a-store-backend)。 当大量*新*客户端可能 超过持久化上限时,可通过以下两种方式进一步提升写入路径: - 设置 `pow.mode: suspicion`,这样就没有全量捕获的挑战(只有明确的 异常/WAF/GeoIP/声誉策略会发布挑战),或者依靠 [攻击模式](https://angieguardian.org/guide/attack-mode) 的 **无状态**发布,该模式在发布时不写入任何内容。剩下的唯一 写入是兑换时的单次消费标记,除非攻击者实际解决了工作量证明,否则 无法触发该写入。 - 已验证的 token 会在进程内缓存(约 35 ns,无内存分配,而完整的 Ed25519 验证需要约 40 µs),因此回访客户端的请求会始终保持在 快速读取路径上,而不受后端影响。 - 在这些速率下,读取路径主要受 Go 的垃圾回收器限制,而不是受 存储限制:设置 `GOGC=800` 可将允许吞吐量提高约 20%。请参阅 生产环境指南中的 "GC 调优"。 `redis` 后端可与 Redis 和 [Valkey](https://valkey.io/) 协同工作(这是一种直接替代方案,使用相同的通信协议和相同的 `backend: redis` 值)。 #### 复现 ``` go build ./cmd/guardiand ./cmd/guardian-loadtest mkdir -p .guardian sed -e 's#/etc/guardian/rules.d/common.yaml#deploy/rules-common.yaml#' \ -e 's#/etc/guardian/#.guardian/#g' \ -e 's#/var/lib/guardian/#.guardian/#g' \ guardian.example.yaml > guardian.local.yaml # 1. Start guardiand with your store backend (pebble shown; for redis set # store.backend: redis and store.addr in the config). PoW must be enabled # for the token/challenge scenarios. ./guardiand -config guardian.local.yaml & # 2. Run each scenario with FIXED WORK (-warmup/-n), 64 connections, and a # fresh daemon + wiped store per run so results are comparable. Use a # distinct -ip per read run so a behavioural block from one run doesn't # bleed into the next; the challenge scenario rotates the client IP itself # to dodge the issuance limit, and its warmup pushes the counter caches # past capacity so the measured window is the loaded steady state. allow # uses the PoW-off host: the scenario expects 200s, and a PoW-on host # answers 401 (challenge) for unvouched clients. ./guardian-loadtest -scenario allow -host api.example.com -ip 198.51.100.10 -c 64 -warmup 50000 -n 500000 ./guardian-loadtest -scenario token -host example.com -ip 198.51.100.11 -c 64 -warmup 50000 -n 500000 ./guardian-loadtest -scenario deny -host example.com -ip 203.0.113.9 -c 64 -warmup 50000 -n 500000 # IP must be denylisted ./guardian-loadtest -scenario challenge -host example.com -c 64 -warmup 150000 -n 150000 ``` 输出中的 `per-second:` 行显示了运行是否达到了稳态; 下降的线条表示总体混合了多种状态,只有带有 相同标志的固定负载运行才具有可比性。`make bench-regress` 是配套的 CI 门控:对照 `allocs-baseline.txt` 中提交的基准检查热路径的 allocs/op, 这是确定性的且与机器无关。 微基准测试位于代码旁边,涵盖热路径的每一层: `/auth` handler 及其构建的请求值、针对每个裁决的 `Evaluate`、 `ShedDecision`、PoW 验证和异常评分。 ``` go test -bench=. -benchmem ./core/... ./transport/http/ ``` 请在阅读负载测试的同时阅读它们,而不是用它们替代负载测试。在 ~180k req/s 的回环速率下,daemon 每个请求大约花费 70–80 µs 的 CPU,而 决策本身远低于一微秒:几乎所有的时间都花在了内核 和 `net/http` 上。因此,如果一项更改使决策路径的成本降低数倍, 最终对端到端请求速率的提升也仅有几个百分点,并且 最适合通过 CPU 消耗/请求来衡量。请参阅 [负载测试指南](https://angieguardian.org/guide/load-testing#micro-benchmarks)。 ### 架构 所有决策逻辑都位于与传输无关的单一接缝之后: ``` core.Engine.Evaluate(ctx, RequestContext) Decision ``` HTTP `auth_request` 传输是围绕它的轻量级封装。无存储的 WAF 检查位于叶子包 `core/stateless` 中,WASM guest 直接导入该包,因此进程内模块可以重用完全相同的逻辑,而 无需引入存储、PoW 或异常依赖项。 ``` core/ decision engine, pipeline, config, scoreboard, recent-decision ring core/stateless/ store-free WAF checks + value types (shared by sidecar & WASM) core/pow/ challenges, Ed25519 JWTs, token cache, key persistence + rotation core/waf/ signature rules, signed IDs core/anomaly/ statistical baseline model, online scorer, hot-swap cache core/store/ TTL'd shared state: memory | buntdb | pebble | redis core/intel/ GeoIP/ASN databases and IP reputation feeds core/botverify/ rDNS + forward-confirm crawler verification core/attackmode/ fleet attack posture: signal aggregation and state machine core/enforce/ block mirror and the optional nftables kernel sink core/health/ background store probe behind /readyz core/metrics/ Prometheus instrumentation (private registry) transport/http/ auth_request sidecar + admin/metrics/dashboard transport/wasm/ optional http-wasm guest (stateless WAF, runs inside Angie) internal/ small shared helpers (bounded file reads, background-work jitter) cmd/ guardiand (sidecar), guardian-train (offline anomaly training), guardian-loadtest (stress tool) deploy/ Angie snippets, systemd unit, rules, Grafana dashboard, alert rules web/ challenge/denied pages and the admin dashboard, with its vendored chart libraries (no CDN, works air-gapped) ```
标签:AppImage, EVTX分析, Go, PoW工作量证明, Ruby工具, Web应用防火墙, 反爬虫与Bot防御, 抗DDoS, 搜索引擎查询, 日志审计, 网络安全, 自定义请求头, 请求拦截, 隐私保护