monch1962/gql-firewall

GitHub: monch1962/gql-firewall

一个用 Go 构建的 GraphQL 防火墙 sidecar,通过集成 OPA/Rego 策略引擎在请求到达上游服务器前拦截并防护各类 GraphQL 攻击向量。

Stars: 0 | Forks: 0

# gql-firewall 一个 **GraphQL 防火墙 sidecar**,可在 GraphQL 请求到达上游服务器之前对其进行拦截、检查和保护。完全使用 Go 语言构建,并集成了 OPA/Rego 以实现策略即代码——涵盖了 [OWASP GraphQL Top 10](https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html) 以及常见的攻击向量。 ``` ┌───────────┐ GraphQL ┌──────────────┐ rules ┌─────────┐ Client ─▶ gql- │ query/block │ Upstream │◀───────▶│ OPA │ │ firewall │───────────────▶│ GraphQL │ eval │ Sidecar │ │ sidecar │ response │ Server │ │(optional)│ └───────────┘ └──────────────┘ └─────────┘ ``` ## 攻击覆盖范围 gql-firewall 的 OPA Rego 策略(33 个测试)覆盖了 12 类攻击,并在 HTTP 传输层进行了 46 项额外的红队攻击模拟测试: ### OPA 策略覆盖范围(12 类) | # | 攻击向量 | 检测方式 | 状态 | |---|---|---|---| | 1 | **Introspection 滥用** — `__schema` / `__type` / `__typename` 发现 | 拦截精确路径和嵌套路径 | ✅ | | 2 | **基于深度的 DoS** — 嵌套查询炸弹 `{a{b{c{...}}}}` | `depth_limit: 10` | ✅ | | 3 | **基于别名的 DoS** — 同一字段 × 500 个别名 | `field_count > 100` 阈值 | ✅ | | 4 | **基于指令的 DoS** — 消耗巨大的自定义指令 | `max_directives: 5` | ✅ | | 5 | **批处理攻击** — 单次请求包含多个操作 | `max_batch_size: 1` | ✅ | | 6 | **未授权的字段访问** — PII、敏感信息、管理员字段 | 12 字段黑名单 + 白名单 | ✅ | | 7 | **Mutation 滥用** — 生产环境使用 Subscription、意外的状态更改 | 操作类型限制 | ✅ | | 8 | **参数注入** — 深度嵌套的参数对象 | `max_argument_depth: 5` | ✅ | | 9 | **N+1 滥用** — 过多的列表字段请求 | `max_lists_requested: 5` | ✅ | | 10 | **Fragment 爆炸** — 跨多个 union 使用 `... on Type` | `max_fragment_spreads: 15` | ✅ | | 11 | **查询成本** — 易于解析但执行成本极高的查询 | `depth × field_count ≤ 50` 预算 | ✅ | | 12 | **Persisted Query 绕过** — 在仅限 PQ 模式下进行动态查询 | `require_persisted_queries` + hash 校验 | ✅ | ### 红队验证的传输层防护(46 项攻击模拟测试) 全部通过 TDD 验证 — 先编写测试,再实现防御。 | # | 攻击向量 | 防御手段 | 状态 | |---|---|---|---| | R1 | 缺少 `Content-Type` header | 除非存在 JSON Content-Type,否则返回 415 拒绝 | ✅ | | R2 | 错误的 `Content-Type` (`text/plain`) | 同 R1 修复 | ✅ | | R3 | 大小写敏感的路径绕过 (`/GRAPHQL`) | 在检查后缀前执行 `strings.ToLower` | ✅ | | R4 | 路径穿越 (`/graphql/../admin`) | 🟡 Go 在 handler 接收前已进行标准化处理 | ✅ | | R5 | 查询字符串注入 | 校验 Body 中的 `query` 字段 | ✅ | | R6 | OPA reason 注入 | `sanitizeReason()` 过滤不可打印的 ASCII 字符 | ✅ | | R7 | 双重 `Content-Type` header | Go 的 `Header.Get()` 返回第一个值 | ✅ | | R8a | 不带 `?query=` 参数的 GET 请求 | 返回 400 | ✅ | | R8b | 带有被拦截查询的 GET 请求 | 被拦截并返回 403 | ✅ | | R8c | 带有允许查询的 GET 请求 | 被拦截并转发 | ✅ | | R9 | Body 为空 | 返回 400 | ✅ | | R10 | 仅包含空白字符的 Body | 返回 400 | ✅ | | R11 | 有效的 JSON,但没有 `query` 字段 | 返回 400 | ✅ | | R12 | OPA 缓存内存耗尽 | 通过 hash 键控的缓存缓解 | ✅ | | R13 | 通过租户 ID 进行日志注入 | 使用 `%q` 进行转义 | ✅ | | R14 | 重复的 Content-Type(首个有效) | Go header 处理行为 | ✅ | | R15 | 租户 ID 边界情况 (`__double`) | `hasLeadingContent()` 守卫 | ✅ | | R17 | 缓存键冲突 | 基于 FNV hash 的字段路径 | ✅ | | R18 | 拦截响应中的无效 JSON | `sanitizeReason()` 确保生成有效 JSON | ✅ | | R24 | 上游 URL 校验 | `proxy.New()` 在 URL 无效时返回错误 | ✅ | | **R27** | **HTTP 方法覆盖** (X-HTTP-Method-Override) | 仍被作为 POST 检查(相同路径) | ✅ | | **R28** | **Content-Type: application/graphql** | 被拒绝 — 仅接受 JSON | ✅ | | **R29** | **带有 UTF-8 BOM 的 JSON** | Go 的 JSON decoder 会自动去除 BOM | ✅ | | **R30** | **URL 编码的表单 Body** | 被拒绝 — 415 Unsupported Media Type | ✅ | | **R31** | **匿名内联 fragment** `{ ... { } }` | 解析器正确处理 | ✅ | | **R32** | **变量作为指令参数** `@skip(if: $s)` | 解析器提取变量和操作指令 | ✅ | | **R33** | **负数的 Body 限制** | 被视为无限,启动时校验 flag | ✅ | | **R34** | **为零的 Body 限制** | 被视为无限(跳过 MaxBytesReader) | ✅ | | **R35** | **深层 fragment 链 (300+)** | 无堆栈溢出 — visited set 防止重入 | ✅ | | **R36** | **带有空查询的 Batch 项** | 返回 400 | ✅ | | **R37** | **Body 中重复的 JSON 键** | Go 使用最后一个值,不会崩溃 | ✅ | | **R38** | **JSON Body 中的 Unicode 转义** | 由 JSON 标准库解码,并正确解析 | ✅ | | **R39** | **不带 host 的上游 URL** | `New()` 返回错误 | ✅ | | **R40** | **上游使用 file:// scheme** | `New()` 返回错误 | ✅ | | **R41** | **超长的操作名称** (1万字符) | 解析时没有内存问题 | ✅ | | **R42** | **带有特殊字符的注释** (emoji, unicode) | 解析器正确处理 | ✅ | | **R43** | **混合 query/mutation 的 Batch** | 每一项都被独立检查 | ✅ | | **R44** | **冲突的查询 hash**(深度相同,路径不同) | Hash 包含完整的查询文本 | ✅ | | **R45** | **深层参数嵌套** (7 层) | `argument_depth` 计算正确 | ✅ | | **R46** | **复杂的变量类型** `[[[String!]!]!]!` | 解析器提取变量数量 | ✅ | | **R47** | **超长的字段参数字符串** (5万字符) | 解析时未发生 OOM | ✅ | | **R48** | **深度嵌套的 JSON Body** (1万层) | 被拒绝 — `json.Decoder` 具有深度限制 | ✅ | | **R49** | **JSON 后面的垃圾数据** `{...}extra` | `dec.More()` 检查会拒绝尾随数据 | ✅ | | **R50** | **限流器内存耗尽** (1万键) | Bucket 清理每分钟移除过期键 | ✅ | | **R51** | **Admin API Body 限制** | 在 PUSH handler 上应用 `10MB MaxBytesReader` | ✅ | | **R52** | **仅在 URL 参数中带查询的 POST** | Body 优先;Body 为空 → 400 | ✅ | | **R53** | **字段上有非常多的参数** (500个) | 解析器正确处理 | ✅ | | **R54** | **非标准方法** (OPTIONS/PUT/DELETE) | 直接通过不进行检查(正确) | ✅ | | **R55** | **JSON 字符串中的 Null 字节** | Go 的 JSON 解码器拒绝 null 字节 | ✅ | | **R56** | **限流器并发竞争** (50 个 goroutine) | 受 Mutex 保护的 bucket map,无竞争 | ✅ | | **R57** | **查询中超多字段** (1万个) | 解析器正确提取所有字段路径 | ✅ | | **R58** | **包含 1000+ 项的 Batch** | 每一项都被独立检查 | ✅ | | **R59** | **variables JSON 中极大的数字** | 存储为 `json.RawMessage`,无溢出 | ✅ | | **R60** | **多个 Content-Type 值** | Go 使用第一个值(接受 `application/json`) | ✅ | | **R61** | **带有无效 variables JSON 的 GET 请求** | Variables 错误不是致命的,查询仍被转发 | ✅ | | **R62** | **与指令名称匹配的字段名** | 将 `skip`, `include`, `deprecated` 用作字段名 | ✅ | | **R63** | **Admin API 巨大 payload** (10MB 配置) | `MaxBytesReader` 限制 Body,返回错误 | ✅ | | **R64** | **一个 Body 字段中包含多个 GraphQL 查询** | 解析器在组合语法上失败 → 400 | ✅ | | **R65** | **OPA reason 中的 CRLF 注入** (响应拆分) | `sanitizeReason()` 去除 < 32 和 > 126 的字符 | ✅ | | **R66** | **Admin API 空 token** (无身份验证) | 为向后兼容而设计;已记录在文档中 | ✅ | | **R67** | **限流器 X-Forwarded-For** | 密钥派生现在会使用第一个 XFF 值(如果存在) | ✅ | | **R68** | **Mutation 字段绕过 schema 校验** | `Validate()` 现在会检查 Mutation/Subscription 根类型 | ✅ | | **R69** | **联合查询(多字段 + 参数 + 深度)** | 解析器正确处理复杂的组合情况 | ✅ | | **R70** | **Admin API GET 访问 /admin/rules/update** | 返回 405 Method Not Allowed | ✅ | | **R71** | **参数中极大的整数** (`999...`) | 在 AST 中作为字符串存储,无溢出 | ✅ | | **R72** | **带有空查询字符串的 Batch 项** | 返回 400 Bad Request | ✅ | | **R73** | **Content-Type charset 变体** | 被接受(charset/编码变体不能绕过) | ✅ | | **R74** | **多值 X-Forwarded-For header** | 无崩溃;限流器使用第一个值 | ✅ | | **R75** | **Admin API 缺少限流** | 未被封装(独立端口;已记录在文档中) | ✅ | | **R76** | **OPA sidecar HTTP 连接耗尽** | 配置的 Transport:MaxIdleConns=10, IdleConnTimeout=30s | ✅ | | **R77** | **路径编码:尾随斜杠 / 大小写** | `isGraphQLPath` 针对标准化路径进行了验证 | ✅ | | **R78** | **Host header 注入** | 上游接收攻击者的 Host;已记录在文档中 | ✅ | | **R79** | **限流器 burst 为零会首先拦截** | Burst=0 意味着第一个请求被拦截(设计使然) | ✅ | | **R80** | **空的 OPA endpoint 允许全部** | 启动时由 CLI 校验守卫 | ✅ | | **R81** | **超长的有效字段路径** | Schema 验证且无溢出 | ✅ | | **R82** | **自引用的 schema 类型** | 在 Validate() 中无无限循环 | ✅ | ## CLI Flags | Flag | 默认值 | 描述 | |---||---| | `--listen` | `:8081` | 防火墙代理监听地址 | | `--upstream` | `http://localhost:8080` | 上游 GraphQL 服务器 | | `--schema` | `""` | GraphQL SDL schema 文件路径(可选) | | `--opa` | `""` | OPA sidecar endpoint(例如 http://localhost:8181/v1/data/graphql) | | `--opa-embed` | `""` | 用于嵌入式 OPA 评估的 Rego 策略文件路径 | | `--opa-params` | `""` | 用于嵌入式 OPA 的参数 JSON 文件路径 | | `--opa-cache-ttl` | `60s` | 缓存的 OPA 决策的 TTL | | `--opa-fail-closed` | `false` | 当 OPA 无法连接时进行拦截 | | `--opa-audit-only` | `false` | 记录 OPA 本应进行的拦截,但不强制执行 | | `--log-format` | `text` | 日志格式:text 或 json | | `--rate-limit` | `0` | 每个租户/IP 的速率限制(req/sec,0 = 禁用) | | `--rate-burst` | `0` | 速率限制突发大小(0 = 2x rate-limit) | | `--admin` | `:8082` | Admin API 监听地址(空 = 禁用) | | `--admin-token` | `""` | 用于 admin API 身份验证的 Bearer token | | `--metrics-listen` | `""` | 独立的指标端口(空 = 在主端口上提供服务) | | `--tls-cert` | `""` | TLS 证书文件路径 | | `--tls-key` | `""` | TLS 私钥文件路径 | | `--max-body-mb` | `1` | 最大请求 Body 大小(MB)(0 = 无限制,必须 ≥ 0) | | `--allowed-operation-names` | `""` | 允许的操作名称的逗号分隔列表
(非空时拦截未命名和未列出的操作) | ### 安全功能 | 功能 | Flag / 配置 | 可防范的问题 | |---|---|---| | **Admin API 身份验证** | `--admin-token` | 未经授权的 admin API 访问 (C-1) | | **OPA fail-closed** | `--opa-fail-closed` | 通过 OPA DoS 绕过 (C-2) | | **配置验证** | 内置 | 通过空配置禁用保护 (C-3) | | **TLS 加密** | `--tls-cert` + `--tls-key` | 明文拦截 (H-5) | | **Body 大小限制** | `--max-body-mb` | 通过超大请求导致 OOM (H-6) | | **服务器超时** | 内置 | Slow loris / 连接耗尽 (H-7) | | **租户密钥验证** | 通过 API | 租户冒充 (H-2) | | **净化错误信息** | 内置 | 信息泄露 (H-4) | | **改进的缓存键** | 内置 | OPA 缓存投毒 (H-3) | | **指标隔离** | `--metrics-listen` | 流量模式泄露 (M-4) | | **优雅关闭** | 内置 | 部署时丢弃请求 (M-3) | | **Panic 恢复** | 内置 | handler panic 导致的进程崩溃 (H1) | || **安全 header** | 内置 | MIME 嗅探 / 点击劫持 (H2) | || **查询解析超时** | 内置 | 通过构造的查询耗尽 CPU (H3) | || **上游 URL 验证** | 内置 | Scheme 注入 / SSRF (H4) | || **操作名称白名单** | `--allowed-operation-names` | 匿名/未列出的操作探测 | ### 核心 - **GraphQL 查询解析** — 使用 `gqlparser/v2` 解析查询、mutation 和 subscription。提取操作类型、名称、深度、字段计数和完整的字段路径。 - **OPA/Rego 策略引擎** — 所有防火墙规则均表示为 Rego 策略。支持两种部署模式: - **Sidecar 模式** (`--opa`):通过 OPA sidecar HTTP endpoint 评估策略。最适合横向扩展部署。 - **嵌入模式** (`--opa-embed`):使用 OPA Go 库在进程内评估策略。零外部依赖,评估时间约 10µs。 - **覆盖 12 种攻击类别** — 见上表。涵盖所有 OWASP GraphQL Top 10 + 2 种额外的攻击向量。 - **通过 OPA 数据注入进行配置** — 参数(depth_limit, max_field_count, field_blocklist 等)作为 OPA 数据注入。可通过 admin API 在运行时更新。 - **感知 SDL schema 的验证** — 接受 GraphQL schema 文件 (`--schema`)。在转发前验证请求的字段是否存在于 Query 类型中。 - **实时 admin API** — 通过 `:8082` 上的 REST API 在运行时查看和更新规则。 - **Prometheus 指标** — `/metrics` endpoint,包含请求、拦截、延迟、规则评估和 OPA 调用的计数器。 - **基于租户的策略隔离** — 每个租户(通过 `X-API-Key` header 标识)可以拥有自己的规则配置。通过 admin API 管理。 ### 性能 - **纯 Go 二进制文件** — 单一静态二进制文件,无运行时依赖。在使用嵌入式 OPA 时,P99 延迟小于 5ms。 - **OPA 决策缓存** — 避免对重复的查询模式进行冗余的 OPA 调用。缓存命中时约为 ~200µs,而 RPC 需 ~2ms。 ### 安全性 - **覆盖 84 种攻击向量**(12 种 OPA Rego + 72 种红队 HTTP 传输层攻击) - **红队验证** — 跨 Go 代理的 72 项攻击模拟测试。通过 7 轮测试发现并修补了真实的漏洞。 - **拒绝覆盖模型** — 默认放行请求,仅通过匹配拒绝规则进行拦截(对于分阶段推出是安全的) - **敏感字段拦截** — SSN、密码、信用卡、API key、机密信息 - **Introspection 拦截** — 直接路径 + 嵌套路径 - **操作限制** — 基于环境的操作类型控制 - **查询成本预算** — 深度 × 字段计数的复杂度预算 - **Persisted Query 模式** — 在生产环境中拦截动态查询 - **速率限制** — 通过 OPA 策略结合外部数据集成 - **仅审计模式** — `--opa-audit-only` 以仅记录日志的模式运行 OPA,以便在强制执行前安全地收集数据 ### 可观测性 - **Prometheus `/metrics`** — 暴露请求计数(按结果 + 操作类型)、拦截请求计数器(按规则原因)、延迟直方图(按结果)、规则评估计数器、OPA 调用计数器、配置重载计数器以及活跃租户仪表(Gauge)。 - **结构化的拦截原因** — 每个被拦截的查询都会返回一个机器可读的 `"reason"` 字段。 - **Admin 健康检查 endpoint** — `GET /admin/health` 返回 `{"status": "ok"}`,适合用于存活探针。 - **Admin 统计 endpoint** — `GET /admin/stats` 返回缓存大小和租户计数。 ## 快速开始 ### 前置条件 - Go 1.23+(用于构建) - OPA 1.0+(可选,用于策略评估 — `opa test opa-policies/` 校验) ### 安装与运行 ``` # 克隆并构建 git clone ... && cd gql-firewall go build -o gql-firewall ./cmd/server/ # 使用 embedded OPA 启动 (零外部依赖) ./gql-firewall \ --upstream http://localhost:8080 \ --opa-embed ./opa-policies/graphql.rego \ --opa-params ./config/params.json \ --listen :8081 # 使用 OPA sidecar 启动 ./gql-firewall \ --upstream http://localhost:8080 \ --opa http://localhost:8181/v1/data/graphql \ --listen :8081 \ --admin :8082 # 使用所有可选功能启动 ./gql-firewall \ --upstream http://localhost:8080 \ --opa-embed ./opa-policies/graphql.rego \ --opa-params ./config/params.json \ --schema ./schema.graphql \ --listen :8081 \ --admin :8082 \ --opa-cache-ttl 60s ``` ### 测试 ``` # 有效查询 (应该通过) curl -X POST http://localhost:8081/graphql \ -H "Content-Type: application/json" \ -d '{"query": "{ hello }"}' # 深层查询 (被深度限制 = 10 阻止) curl -X POST http://localhost:8081/graphql \ -H "Content-Type: application/json" \ -d '{"query": "{ a { b { c { d { e { f } } } } } }"}' # 敏感字段 (被字段 blocklist 阻止) curl -X POST http://localhost:8081/graphql \ -H "Content-Type: application/json" \ -d '{"query": "{ user { name ssn } }"}' # Introspection (如果已部署,会被 OPA 策略阻止) curl -X POST http://localhost:8081/graphql \ -H "Content-Type: application/json" \ -d '{"query": "{ __schema { types { name } } }"}' # 被阻止的操作类型 curl -X POST http://localhost:8081/graphql \ -H "Content-Type: application/json" \ -d '{"query": "subscription OnMsg { messageAdded { id } }"}' ``` ## 配置 ### 参数 JSON 所有防火墙参数都作为 OPA 数据注入。创建一个与 Rego 策略参数相对应的 JSON 文件: ``` { "depth_limit": 10, "max_field_count": 100, "blocked_operations": ["subscription"], "field_blocklist": ["__schema", "__type", "user.ssn", "user.password"] } ``` | 字段 | 类型 | 默认值 | 描述 | |---|---|---|---| | `depth_limit` | number | 10 | 最大查询嵌套深度 | | `max_field_count` | number | 100 | 每次查询的最大字段数 | | `blocked_operations` | array | `["subscription"]` | 要拦截的操作类型 | | `allowed_operations` | array | `[]` | 仅允许这些操作类型 | | `field_allowlist` | array | `[]` | 仅允许这些字段路径 | | `field_blocklist` | array | `[]` | 拦截这些字段路径(覆盖白名单) | | `max_directives` | number | 5 | 每次查询的最大指令数 | | `max_batch_size` | number | 1 | 每次批处理的最大操作数 | | `max_argument_depth` | number | 5 | 最大参数嵌套深度 | | `max_lists_requested` | number | 5 | 每次查询的最大列表字段数 | | `max_fragment_spreads` | number | 15 | 每次查询的最大 fragment spread 数 | | `cost_budget` | number | 50 | 复杂度预算 (深度 × 字段数) | | `require_persisted_queries` | bool | false | 拦截动态(非 persisted)查询 | | `allowed_operation_names` | array | `[]` | 仅允许这些操作名称(拦截未命名和未列出的) | ### OPA 策略 `opa-policies/` 目录包含生产就绪的 Rego 策略模板: ``` # 部署到 OPA sidecar curl -X PUT --data-binary @opa-policies/graphql.rego \ http://localhost:8181/v1/policies/graphql # 调试:评估特定查询 echo '{"input": {"depth": 5, "field_count": 3, "operation_type": "query", "field_paths": ["user", "user.name"]}}' | \ opa eval --data opa-policies/graphql.rego --input-file - "data.graphql" # 运行所有策略测试 opa test opa-policies/ -v ``` OPA 输入 schema 与解析器的 `QueryInfo` 结构相匹配: ``` { "input": { "operation_type": "query", "operation_name": "GetUser", "depth": 3, "field_count": 5, "field_paths": ["user", "user.name", "user.email"], "tenant_id": "acmecorp", "params": { "depth_limit": 10, "max_field_count": 100 } } } ``` ## Admin API | Endpoint | 方法 | 描述 | |---|---|---| | `GET /admin/health` | GET | 健康检查 — 返回 `{"status": "ok"}` | | `GET /admin/rules` | GET | 返回当前的规则配置 | | `PUT /admin/rules/update` | POST/PUT | 在运行时更新规则(接受参数 JSON,并推送到 OPA 数据存储) | | `GET /admin/stats` | GET | 返回运行时统计信息(缓存大小,租户数量) | | `GET /admin/tenants` | GET | 列出所有已配置的租户 ID | | `GET /admin/tenants/{id}` | GET | 获取特定租户的规则配置 | | `POST /admin/tenants/{id}` | POST/PUT | 创建或更新租户的规则 | | `DELETE /admin/tenants/{id}` | DELETE | 删除租户的规则(回退到默认配置) | ``` # 查看当前规则 curl http://localhost:8082/admin/rules # 在运行时更新规则 curl -X POST http://localhost:8082/admin/rules/update \ -H "Content-Type: application/json" \ -d '{"depth_limit": 5, "max_field_count": 50}' # 创建特定于租户的规则 (从 "myapp_..." API key 中提取 tenant ID) curl -X PUT http://localhost:8082/admin/tenants/myapp \ -H "Content-Type: application/json" \ -d '{"depth_limit": 3}' # 列出租户 curl http://localhost:8082/admin/tenants # 删除租户 curl -X DELETE http://localhost:8082/admin/tenants/myapp # 健康检查 curl http://localhost:8082/admin/health # 统计信息 curl http://localhost:8082/admin/stats ``` ## 指标 Prometheus 指标会在主监听端口的 `/metrics` 处暴露: | 指标 | 类型 | 标签 | 描述 | |---|---|---|---| | `gql_firewall_requests_total` | Counter | `outcome`, `operation_type` | GraphQL 请求总数 | | `gql_firewall_requests_blocked_total` | Counter | `reason` | 按规则原因拦截的请求数 | | `gql_firewall_request_duration_seconds` | Histogram | `outcome` | Pipeline 延迟 | | `gql_firewall_active_tenants` | Gauge | — | 活跃租户数 | | `gql_firewall_rule_evaluations_total` | Counter | `rule` | 规则评估计数 | | `gql_firewall_config_reloads_total` | Counter | — | 配置热重载计数 | | `gql_firewall_opa_requests_total` | Counter | `outcome` | OPA sidecar 调用计数 | | `gql_firewall_opa_audit_blocks_total` | Counter | `reason` | 仅审计模式下本应进行的 OPA 拦截次数 | ``` # 抓取 metrics curl http://localhost:8081/metrics ``` ## 架构 ``` Request Flow ════════════ ┌────────────────────────────────────────────────────────────┐ │ Client │ │ POST /graphql {"query": "..."} │ └─────────┬──────────────────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────────────────────────┐ │ gql-firewall sidecar (Go) │ │ │ │ 1. Parse GraphQL body (JSON) │ │ 2. Parse GraphQL query (Go gqlparser/v2, in-process) │ │ 3. Evaluate OPA policies (embedded or sidecar) │ │ │ ┌───────────────┐ │ │ └───────────────────────────────────▶│ OPA │ │ │ │ (:8181) │ │ │ └───────────────┘ │ │ 4. Forward or block? │ └─────────┬──────────────────────────────────────────────────┘ │ ┌──── Blocked ────▶ 403 {error, reason} ▼ │ ┌────────────────────┐ │ │ Upstream GraphQL │ │ │ Server │ │ └────────────────────┘ │ │ │ ▼ ▼ 200 {data} 403 {"error": "request blocked", "reason": "query depth exceeded limit"} ``` ## 项目结构 ``` gql-firewall/ ├── cmd/server/main.go # Entry point — wires everything together (+ test, 25 tests) ├── config/params.json # Sample OPA parameters ├── internal/ │ ├── parser/ # GraphQL query analysis (45 tests, including Rust compat) │ ├── opa/ # OPA evaluator: sidecar, embedded, data store, input builder (63 tests) │ ├── metrics/ # Prometheus instrumentation (6 tests) │ ├── proxy/ # HTTP reverse proxy (88 tests, including 39 red-team + 8 hardening + 9 input handling + 10 round 6) │ ├── ratelimit/ # Token-bucket rate limiter (6 tests) │ ├── testutil/ # Shared test helpers │ └── integration/ # End-to-end pipeline tests (23 tests, including 19 e2e HTTP tests) ├── opa-policies/ # OPA Rego policy templates (33 tests) │ ├── graphql.rego # 12 attack categories, parameterized via input.params │ └── graphql_test.rego # 33 policy tests ├── README.md ├── go.mod / go.sum └── .gitignore ``` ## 测试套件 ``` Go: 269 tests — server(25), parser(123), proxy(96), integration(23), opa(63), metrics(14), ratelimit(7) OPA/Rego: 37 tests — 12 attack categories, edge cases, combined rules, operation-name allowlist Total: 306 tests — all passing ``` ``` # 运行所有内容 go test ./... -count=1 opa test opa-policies/ ``` ## 部署 ### Docker 构建镜像: ``` docker build -t gql-firewall:latest . ``` 使用嵌入式 OPA 运行(单一容器,零外部依赖): ``` docker run -p 8081:8081 \ -v $(pwd)/config/params.json:/app/config/params.json \ gql-firewall:latest \ --upstream http://host.docker.internal:8080 \ --opa-embed /app/opa-policies/graphql.rego \ --opa-params /app/config/params.json \ --listen :8081 ``` 使用 OPA sidecar 运行(需要单独的 OPA 容器): ``` # 启动 OPA docker run -d --name opa -p 8181:8181 \ -v $(pwd)/opa-policies:/policies \ openpolicyagent/opa:1.1.0 run --server /policies # 启动 firewall docker run -p 8081:8081 \ gql-firewall:latest \ --upstream http://host.docker.internal:8080 \ --opa http://opa:8181/v1/data/graphql \ --listen :8081 ``` ### Kubernetes Sidecar 将防火墙作为 sidecar 容器与你的 GraphQL 服务器部署在同一个 pod 中: ``` apiVersion: apps/v1 kind: Deployment metadata: name: graphql-server spec: replicas: 2 selector: matchLabels: app: graphql template: metadata: labels: app: graphql spec: containers: - name: graphql image: my-graphql-server:latest ports: - containerPort: 8080 readinessProbe: httpGet: path: /health port: 8080 - name: gql-firewall image: gql-firewall:latest args: - --upstream=http://localhost:8080 - --opa-embed=/app/opa-policies/graphql.rego - --opa-params=/app/config/params.json - --listen=:8081 - --admin=:8082 ports: - containerPort: 8081 name: http - containerPort: 8082 name: admin volumeMounts: - name: opa-policy mountPath: /app/opa-policies - name: config mountPath: /app/config readinessProbe: httpGet: path: /admin/health port: 8082 initialDelaySeconds: 3 periodSeconds: 10 resources: limits: memory: "64Mi" cpu: "100m" requests: memory: "32Mi" cpu: "50m" volumes: - name: opa-policy configMap: name: gql-firewall-policy - name: config configMap: name: gql-firewall-config ``` 该 Service 将传入的 GraphQL 流量路由到防火墙 sidecar 端口,后者对其进行检查并转发到本地上游: ``` apiVersion: v1 kind: Service metadata: name: graphql spec: selector: app: graphql ports: - port: 80 targetPort: 8081 name: http ``` 用于策略和参数的 ConfigMaps: ``` # 从你的策略和配置文件创建 ConfigMaps kubectl create configmap gql-firewall-policy \ --from-file=opa-policies/graphql.rego kubectl create configmap gql-firewall-config \ --from-file=config/params.json ``` 结合 OPA sidecar(同一个 pod 中的独立容器): ``` - name: opa image: openpolicyagent/opa:1.1.0 args: - run - --server - /policies/graphql.rego ports: - containerPort: 8181 volumeMounts: - name: opa-policy mountPath: /policies - name: gql-firewall image: gql-firewall:latest args: - --upstream=http://localhost:8080 - --opa=http://localhost:8181/v1/data/graphql - --listen=:8081 ``` ### Docker Compose ``` version: "3.8" services: graphql: image: my-graphql-server:latest expose: - "8080" gql-firewall: build: . ports: - "8081:8081" command: - --upstream=http://graphql:8080 - --opa-embed=/app/opa-policies/graphql.rego - --opa-params=/app/config/params.json - --listen=:8081 volumes: - ./opa-policies:/app/opa-policies - ./config:/app/config ``` ## 建议的新功能 以下功能符合该产品的商业模式 — 一款面向中端市场 API 团队的独立 GraphQL 防火,采用仅限 Go 的二进制文件和基于 OPA 的策略,并最终有望被 Datadog、Kong 或 Cloudflare 收购。 ### 学习资源 - **[编写自定义 OPA 策略](docs/opa-samples/tutorial.md)** — 包含实例操作、测试指南以及所有输入字段参考的实操教程 - **[25 个示例策略](docs/opa-samples/)** — 可运行的 Rego 文件,涵盖每种攻击类别、租户隔离、CIDR 过滤、基于时间的访问控制以及组合规则 - **[OPA 策略语言](https://www.openpolicyagent.org/docs/latest/policy-language/)** — 官方的 Rego 参考 ### 近期阶段(阶段 1 — 在现有架构上构建) ✅ 已完成 **1. Prometheus 指标 endpoint** — 将拦截计数器、延迟直方图和活跃租户计数暴露为 Prometheus `/metrics`。**✅ 已实现。** `gql_firewall_requests_total`、`gql_firewall_requests_blocked_total`、`gql_firewall_request_duration_seconds`、`gql_firewall_active_tenants`、`gql_firewall_rule_evaluations_total`、`gql_firewall_config_reloads_total`、`gql_firewall_opa_requests_total` 以及 `gql_firewall_opa_audit_blocks_total` 全部暴露在 `/metrics` 下。 **2. 感知 GraphQL schema 的验证** — 接受一个 SDL schema 文件 (`--schema schema.graphql`)。使用它来检测请求不存在的字段、参数类型错误或已弃用的字段的查询。**✅ 已实现。** 完整的 schema 验证,包括字段存在性检查、覆盖 Query/Mutation 字段的类型检查以及弃用指令检测。参见 `SchemaInfo.Validate()`。 **3. 基于操作名称的白名单** — 在生产环境中仅允许已知的操作名称(例如允许 `query GetUser { ... }` 但拦截任意的未命名查询)。**✅ 已实现。** 使用 `--allowed-operation-names GetUser,ListPosts` 来启用。未命名的查询和未列出的名称将被拦截。适用于嵌入式和 sidecar OPA 模式。 ### 中期阶段(阶段 2 — 高级版本启用功能) **4. 利用 OPA data bundle 实现的租户隔离** — 基于 API key 或 JWT claims 将请求路由到特定于租户的 OPA data bundle。每个租户都会获得自己的策略作用域,而无需部署单独的 sidecar。这是区别于通用 API 网关的护城河产品功能。 **5. Go sidecar 中的 Rego 策略缓存** — 带有 TTL 地在本地缓存 OPA 决策,以避免重复查询时的网络跳转。对于典型的中端市场 GraphQL API,大约 70% 的查询模式会在 60 秒内重复。这会将 P99 延迟从 ~2ms (OPA RPC) 降低到 ~200µs (本地缓存命中)。 **6. 用于实时规则管理的 REST API** — 添加 admin endpoint (`POST /admin/rules`, `GET /admin/stats`),以便平台团队无需访问文件或重启 sidecar 即可更新规则。这是一项基本的企业级功能,能够支持 Silver/Gold 级别的定价模型($15K-$40K/月)。 ### 长期阶段(阶段 3 — 收购价值) **7. Datadog 原生集成** — 发布一个带有开箱即用仪表板的 Datadog 集成磁贴(按规则分类的拦截查询、延迟热力图、租户级别的成本归因)。这是 Datadog 收购路径中杠杆率最高的单一功能 — 它消除了尽职调查中关于“集成成本”的异议。 **8. GraphQL 成本分析引擎** — 用感知 schema 的成本权重(每个 resolver 的成本、列表大小乘数、join 复杂度)替换简单的 `深度 × 字段数` 启发式算法。为每个 API key 暴露成本预算。这将产品从“WAF for GraphQL”转变为“成本管理平台” — 一个更高价值的品类。 **9. Kong/Konnect 插件打包** — 将 Go sidecar 打包为 Kong 插件 (Go PDK),以抢占运行 Kong 的 30% 的市场。这不会改变架构 — 这只是部署打包方式的改变 — 但它打通了被 Kong 收购的路径。 ## 开发 ### 添加新的攻击检测 1. 将拒绝规则添加到 `opa-policies/graphql.rego` 中,并附带一条独特的消息 2. 在 `opa-policies/graphql_test.rego` 中编写测试用例 3. 使用 `opa test opa-policies/` 进行验证 4. 在 `internal/opa/owasp_test.go` 中添加 Go 级别的集成测试 ## 路线图 | 阶段 | 时间表 | 功能 | 营收 | |---|---|---|---| | **阶段 1** | 第 0-6 个月 | Prometheus,感知 schema 的验证,操作名称白名单 | $99-499/月 | | **阶段 2** | 第 6-12 个月 | 租户隔离,策略缓存,实时 admin API | $1,999-9,999/月 | | **阶段 3** | 第 12-24 个月 | Datadog 集成,成本分析引擎,Kong 打包 | $40-100K/月 | ## 许可证 MIT
标签:CISA项目, EVTX分析, Go, GraphQL, Homebrew安装, OPA, Ruby工具, Sidecar, Web安全, 底层编程, 日志审计, 结构化提示词, 蓝队分析, 防火墙, 靶场