Egress Policy(出网策略)
Egress Policy 插件在请求到达上游之前,同步调用 CyberGuard 对请求内容进行检测,并依据配置的策略链决定放行(PASS)或拦截(BLOCK)。
启用条件
Egress Policy 插件只有在同时满足以下条件时,才会被挂进请求链路:
- 静态配置
plugins.egress_policy.enable = true - 当前 route 配置了
plugin_config.egress_policy
两个条件缺一不可。若只满足其中之一,插件不会进入该 route 的请求链路。
Egress Policy 只挂在 OnForwardChat hook 上,仅对 /v1/chat/completions 等 chat 请求生效;embeddings、models 等请求不受影响。
执行位置
在插件执行顺序中,Egress Policy 位于:
- 之后:
upstream-ratelimit(先检查配额) - 之前:
guard、requestcache
静态配置
在 config.toml 中通过 [plugins.egress_policy] 配置全局参数:
[plugins.egress_policy]
enable = true
check_service_url = "http://cyberguard:9019"
timeout_ms = 5000
| 字段 | 类型 | 说明 |
|---|---|---|
enable | bool | 是否全局启用插件,必须为 true 才能激活 |
check_service_url | string | CyberGuard gRPC 服务地址,静态配置优先于路由配置 |
timeout_ms | int64 | 单次 gRPC 调用超时(毫秒),静态配置优先于路由配置;0 时使用默认值 5000ms |
路由配置
路由级配置通过 RoutePluginConfig 管理接口写入,plugin_key 固定为 egress_policy,config 为以下结构的 JSON:
{
"check_service_url": "http://cyberguard:9019",
"timeout_ms": 3000,
"policy_chain_json": "{ ... }",
"policies": [
{
"id": "block-unsafe",
"name": "拦截 unsafe 内容",
"priority": 10,
"enabled": true,
"matches": [
{ "source": "GUARD", "key": "status", "operator": "EQUALS", "value": "unsafe" }
],
"action": {
"type": "BLOCK",
"status_code": 403,
"message": "请求内容不合规,已被拦截"
}
}
]
}
| 字段 | 类型 | 说明 |
|---|---|---|
check_service_url | string | 覆盖全局 check_service_url(仅当静态配置为空时生效) |
timeout_ms | int64 | 覆盖全局 timeout_ms(仅当静态配置为 0 时生效) |
policy_chain_json | string | 传递给 CyberGuard 的 PolicyChain JSON,当有 source=GUARD 策略时必填 |
policies | array | 策略列表,按 priority 升序评估(First Match Wins) |
policy 字段
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 唯一标识,会记录在请求日志中 |
name | string | 人类可读名称(可选) |
priority | int | 优先级,值越小越先匹配 |
enabled | bool | 是否参与匹配,false 时直接跳过 |
matches | array | 匹配条件列表,AND 语义(全部满足才触发) |
action | object | 匹配成功后的动作 |
match 字段
| 字段 | 类型 | 说明 |
|---|---|---|
source | string | 取值来源:GUARD 或 HEADER |
key | string | 字段名。GUARD 时为 status 或 action;HEADER 时为 HTTP 请求头名称(大小写不敏感) |
operator | string | 比较运算符,见下表 |
value | string | 期望值(EQUALS / NOT_EQUALS 使用) |
values | string[] | 期望值列表(IN 使用) |
支持的 operator:
| 值 | 语义 |
|---|---|
EQUALS | 等于(大小写不敏感) |
NOT_EQUALS | 不等于(大小写不敏感) |
IN | 在 values 列表中(大小写不敏感) |
EXISTS | 字段存在且非空 |
NOT_EXISTS | 字段不存在或为空 |
GUARD source 的 key 取值:
| key | 说明 |
|---|---|
status | CyberGuard 检测结果:safe 或 unsafe |
action | CyberGuard 建议动作:pass、alert、block、override |
action 字段
| 字段 | 类型 | 说明 |
|---|---|---|
type | string | BLOCK 或 PASS |
status_code | int | BLOCK 时返回的 HTTP 状态码,默认 403 |
message | string | BLOCK 时返回的错误消息,默认 请求内容不合规,已被拦截 |
工作流程
请求 → reqIR 为 nil?
├─ Yes → bypass(直接放行)
└─ No → 调用 CyberGuard gRPC
├─ 超时 / 错误 / 无消息 → bypass(fail-open,直接放行)
└─ 成功 → 按 priority 升序评估策略
├─ 匹配到 BLOCK 策略 → 返回错误响应(拦截)
├─ 匹配到 PASS 策略 → 放行
└─ 没有策略匹配 → 默认放行
决策结果(effect_action)
| 值 | 含义 |
|---|---|
pass | 请求放行,到达上游 |
block | 请求被拦截,不到达上游 |
bypass | CyberGuard 不可用(超时/错误),fail-open 放行 |
拦截响应
当策略命中 BLOCK 时,插件直接设置 TerminalResponse,不调用 next:
HTTP/<status_code>
Content-Type: application/json
{
"error": {
"message": "<policy.action.message>",
"type": "egress_policy_blocked",
"param": null,
"code": null
}
}
默认 status_code 为 403,默认 message 为 请求内容不合规,已被拦截。
请求日志
每次请求都会向 ext_fields["egress_policy"] 写入决策记录:
{
"egress_policy": {
"effect_action": "block",
"matched_policy_id": "block-unsafe",
"guard_status": "unsafe",
"guard_action": "block"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
effect_action | string | pass、block 或 bypass |
matched_policy_id | string | 命中策略的 id,无匹配或 bypass 时为空 |
guard_status | string | CyberGuard 检测状态(safe / unsafe),bypass 时为空 |
guard_action | string | CyberGuard 建议动作,bypass 时为空 |
参见 日志扩展字段 中关于 ext_fields 的说明。
Prometheus 指标
插件暴露以下 Prometheus 指标(通过管理服务器 GET /metrics 获取):
aidy_egress_policy_decisions_total(Counter)
每次策略决策都会递增,包含 bypass。
| 标签 | 说明 |
|---|---|
route_id | 触发决策的 route ID |
effect_action | pass、block 或 bypass |
guard_status | safe、unsafe 或空字符串(bypass 时) |
aidy_egress_policy_bypass_total(Counter)
仅在 effect_action=bypass 时递增,用于独立监控 CyberGuard 服务可用性。
| 标签 | 说明 |
|---|---|
route_id | 触发 bypass 的 route ID |
aidy_egress_policy_cyberguard_duration_seconds(Histogram)
记录每次同步 CyberGuard gRPC 调用的耗时。
Buckets:.05, .1, .25, .5, 1, 2, 5, 10(秒)
| 标签 | 说明 |
|---|---|
route_id | 触发调用的 route ID |
result | ok(成功)或 error(超时/连接失败等) |
配置示例
示例 1:拦截 unsafe 请求
# config.toml
[plugins.egress_policy]
enable = true
check_service_url = "http://cyberguard:9019"
timeout_ms = 5000
{
"policies": [
{
"id": "block-unsafe",
"priority": 10,
"enabled": true,
"matches": [
{ "source": "GUARD", "key": "status", "operator": "EQUALS", "value": "unsafe" }
],
"action": { "type": "BLOCK", "status_code": 403 }
}
]
}
示例 2:根据请求头拦截特定租户
{
"policies": [
{
"id": "block-risky-tenant",
"priority": 5,
"enabled": true,
"matches": [
{ "source": "HEADER", "key": "X-Risk-Level", "operator": "IN", "values": ["high", "critical"] },
{ "source": "GUARD", "key": "status", "operator": "EQUALS", "value": "unsafe" }
],
"action": { "type": "BLOCK", "status_code": 403, "message": "高风险租户请求已拦截" }
},
{
"id": "pass-all",
"priority": 100,
"enabled": true,
"matches": [],
"action": { "type": "PASS" }
}
]
}
示例 3:仅观察(不拦截)
若只需采集指标、不做任何拦截,可不配置任何策略或仅配置 PASS 策略;插件会正常调用 CyberGuard 并记录日志,最终默认放行。