Solo Enterprise 将 OWASP CRS 规则引擎 Coraza 应用于 LLM prompt 检查和 MCP 工具调用,在不修改 agent 的前提下实现企业级安全治理。
传统的 Web 应用防火墙读取 URL、一些请求头,也许还有一个表单 body。但对于 Agent 流量而言,这搞错了层级——真正有意义的内容在请求体里:LLM 调用的 prompt,MCP 调用的方法名、工具名和参数。
Solo Enterprise for agentgateway 运行的是 Coraza,这是 OWASP 维护的规则引擎,作为共享扩展。开启 body 检查后,安全团队已经熟悉如何分析的 OWASP Core Rule Set 就能应用到 prompt 和工具调用上,SOC 可以在 SecLang 中添加自己的签名,而无需触碰任何 Agent、模型或 MCP server。
策略文件位于 msquared/agentic-demo 下的 manifests/governance/。下面的每一个状态码都来自 v2026.8.2 的真实集群。
让 body 检查真正生效的配置
两个设置缺一不可,漏掉任何一个都会导致 WAF 看起来健康但放行所有 payload。
apiVersion: waf.solo.io/v1alpha1
kind: WAFPolicy
metadata:
name: governed-llm-waf
namespace: agentgateway-system
spec:
processingConfig:
request:
mode: HeadersAndBody # 1. buffer the body at all
coreRuleSet:
settings:
inline: |
SecDefaultAction "phase:1,log,auditlog,deny,status:403"
SecDefaultAction "phase:2,log,auditlog,deny,status:403"
SecAction "id:900990,phase:1,pass,t:none,nolog,tag:'OWASP_CRS',ver:'OWASP_CRS/4.23.0',setvar:tx.crs_setup_version=4230"
ruleEngineSettings:
inline: |
SecRuleEngine On
SecAuditEngine RelevantOnly
SecAuditLog /dev/stdout
SecAuditLogFormat JSON
SecAuditLogParts AKHZ
# 2. parse the JSON body into ARGS so rules can see prompt fields
SecRule REQUEST_HEADERS:Content-Type "^application/json" \
"id:200001,phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
mode: HeadersAndBody 让 body 可用。规则 200001 让 body 结构化:没有 JSON body processor 的话,body 就是一个不透明的大 blob,基于 ARGS 匹配的规则根本无从下手。加上它之后,一个 chat completions 请求就变成了可寻址的字段,ARGS 能覆盖 json.model、json.messages.0.content 以及文档中的所有其他内容。
自定义签名与 CRS 并列放置。以下三条是 AI 专用的,规则 ID 段是为它们预留的:
customDirectives:
- inline: |
SecRule ARGS "@rx (?i)ignore\s+(all\s+)?(previous|prior|above)\s+instructions" \
"id:9001,phase:2,deny,status:403,log,msg:'LLM prompt injection: instruction override'"
SecRule ARGS "@rx (?i)\b(DAN|developer)\s+mode\b|\bjailbreak\b" \
"id:9002,phase:2,deny,status:403,log,msg:'LLM jailbreak framing'"
SecRule ARGS "@rx (?i)(print|reveal|show|repeat)\s+(your|the)\s+(system\s+prompt|hidden\s+instructions)" \
"id:9003,phase:2,deny,status:403,log,msg:'LLM system-prompt exfiltration attempt'"
将策略绑定到路由是另一个资源,这意味着一个 WAFPolicy 可以被多个路由复用:
apiVersion: enterpriseagentgateway.solo.io/v1alpha1
kind: EnterpriseAgentgatewayPolicy
metadata:
name: governed-llm-waf
namespace: agentgateway-system
spec:
targetRefs:
- group: gateway.networking.k8s.io
kind: HTTPRoute
name: governed-llm
traffic:
entWAF:
wafPolicyRef:
name: governed-llm-waf
在 LLM 路由上会被拦截什么
每一行都是同一个认证用户,所以唯一变化的只有 payload:
最后三条是标准 CRS,没有任何 AI 专属配置。承载 LLM 流量的网关仍然是一个 HTTP 端点,和其他端点一样被扫描。
调用方看到的内容由你配置,返回简洁的信息比详细描述更好:
{"error":{"type":"policy_violation","message":"Request blocked by the enterprise AI WAF policy."}}
详情应该放在审计日志里,而不是放在响应体中——那是攻击者用来调整下一次尝试的素材。
关于误报问题,在被问到之前就回答
任何有 WAF 运维经验的人首先会问:这会对正常流量造成什么影响?Prompt 文本往往很长、不可预测,且充满了在上下文中看起来像恶意内容的字符串。因此测试套件包含了一个刻意刁钻但完全真实的工程 prompt:SQL 关键字、带查询参数的 URL、诊断码和固件版本。
curl -s -o /dev/null -w '%{http_code}\n' localhost:8081/governed-llm/v1/chat/completions \
-H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
-d '{"model":"acme-standard","max_tokens":30,"messages":[
{"role":"system","content":"You are a helpful assistant for platform engineers."},
{"role":"user","content":"Summarize in two sentences: our device logs show intermittent bus errors (code 639, severity 9) on the v2 platform after firmware 2.4.1; the SELECT statement in our telemetry pipeline returns duplicates; and the admin portal at https://example.com/portal?id=42&view=full times out under load."}]}'
返回 200。这个单独的 case 值得放入 CI,因为一旦有人提高了 CRS paranoia 级别或添加了一条宽泛的自定义规则,这个请求就会告诉他们代价是什么。
同一道防火墙用在 MCP 前面
这是我之前没见过有人做的部分。MCP 是基于 HTTP 的 JSON-RPC,所以一旦 body 被解析,WAF 就能寻址协议自身的结构:json.method、json.params.name 以及 json.params.arguments 下的每一个条目。
两条自定义规则覆盖了 MCP 专属的场景:
customDirectives:
- inline: |
# 9101 - JSON-RPC method allowlist
SecRule ARGS:json.method "!@rx ^(initialize|notifications/initialized|ping|tools/list|tools/call)$" \
"id:9101,phase:2,deny,status:403,log,msg:'MCP method not allowlisted'"
# 9102 - prompt injection inside any tool argument
SecRule ARGS:/^json\.params\.arguments\./ "@rx (?i)ignore\s+(all\s+)?(previous|prior|above)\s+instructions" \
"id:9102,phase:2,deny,status:403,log,msg:'MCP tool argument carries prompt injection'"
在受治理的路由上打开一个真实的 MCP 会话,然后向它发送混合的诚实和恶意调用:
诚实的调用返回应有的结果:
{"jsonrpc":"2.0","id":8,"result":{"content":[{"type":"text",
"text":"Weather in Portland: Temperature: 77 F, Humidity: 87%, Wind: 14.2 mph"}],"isError":false}}
被拦截的调用得到的是 JSON-RPC 格式的错误而非原始 HTML,这对 MCP client 必须解析它而言很重要:
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Request blocked by the enterprise MCP WAF policy."}}
表格中的第三行和第四行是我认为最有说服力的两个例子。这两条都不是专门为 MCP 写的规则。city 参数里的 ../../etc/passwd 是路径遍历,script 标签是 XSS,不管谁发送的以及出现在哪个字段里。一旦参数可以被解析,二十年积累的 CRS 签名就立即生效了。
方法白名单是协议层面的最小权限
规则 9101 是我会优先放在每个 MCP 路由上的第一条规则,甚至在任何内容签名之前。
MCP server 暴露的不只是工具。协议中还有 resources、prompts、sampling 和 completion 方法,而给定的 client 通常只需要其中一小部分。一个只需要调用工具的 client 只需要五个方法。其他一切都是协议定义中存在但没有人真正需要的攻击面。
上表中 resources/list 返回 403 就是这条规则在起作用。网关背后的 MCP server 仍然实现了这个方法,只是 client 无法触及它,而且这个决定在联系 server 之前就做出了。
这和网络团队在允许四个端口而不是全量端口时的思路完全一致。它可以干净地迁移到工具协议上,而且不同于授权策略,它不需要身份模型就能发挥作用。
WAF 记录了什么
拦截的价值取决于审计跟踪的质量。配置了 SecAuditLogFormat JSON 和 SecAuditLog /dev/stdout 后,每一次干预都会落在 WAF server 的日志中,消息会指明触发的是哪条规则:
kubectl logs deploy/waf-server-enterprise-agentgateway -n agentgateway-system \
--since=3m | grep -oE '"msg":"[^"]*"' | sort | uniq -c | sort -rn
2 "msg":"MCP method not allowlisted"
1 "msg":"XSS Attack Detected via libinjection"
1 "msg":"SQL Injection Attack Detected via libinjection"
1 "msg":"Path Traversal Attack (/../) or (/.../)"
1 "msg":"MCP tool argument carries prompt injection"
1 "msg":"LLM prompt injection: instruction override"
1 "msg":"LLM jailbreak framing"
1 "msg":"Found User-Agent associated with security scanner"
自定义规则和 CRS 规则以相同的格式出现在同一个流中,这让已经熟悉 ModSecurity 或 CRS 仪表板的团队可以直接使用。同一个 server 上有 Prometheus 计数器:
waf_server_requests_total{action="allow",reason=""} 64
waf_server_requests_total{action="deny",reason="waf_blocked"} 77
waf_server_policy_status{name="governed-llm-waf",status="active"} 1
执行顺序,以及一个值得知道的失败模式
WAF 不是请求遇到的第一个环节。在这条路由上,执行顺序是:JWT 认证 → CEL 授权 → WAF。匿名调用者得到 401,策略拒绝的调用者得到 403,防火墙根本不会运行。因此 WAF 拦截的一切都是已认证、已授权用户发出的恶意 payload,这比来自互联网的原始拦截计数要有意义得多。
我在《agentgateway CEL 踩坑记》两篇中写过这道前置的授权层,其中一篇描述了一个策略看似应该拒绝但实际静默放行了请求的坑。在你依赖那层来过滤到达 WAF 的流量之前,值得一读。
需要知道的失败模式:无效的 WAFPolicy 会导致 Fail Closed。规则编译不通过时,路由会得到 HTTP 500 而不是让流量无过滤地通过。这是正确的默认行为,意味着 WAF 变更后出现 500 几乎一定是编译错误而不是运行时拦截:
kubectl describe wafpolicy governed-llm-waf -n agentgateway-system
# check status.conditions[type=Ready]
运行demo
./setup.sh # k3d cluster, mesh, gateway, agents (~15 min)
./port-forward.sh
./governance-demo.sh --act 3
Act 3 同时应用两条策略并执行上面两个表中的每一个请求,在状态码旁边打印预期结果。--check 以非交互方式运行整个治理流程并断言 24 个结果。
它需要 Solo Enterprise 许可证,因为 WAFPolicy 和 EnterpriseAgentgatewayPolicy 是企业级 CRD。规则本身是纯 SecLang 和 OWASP CRS,所以签名可以移植到任何 Coraza 或 ModSecurity 部署。网关提供的是位置优势:所有 LLM 和 MCP 请求已经必经的一个集中点,而且 body 已经解析好了。
下一步:当你停止一个已经做了你不喜欢的事情的 Agent 时,这条记录会变成什么样。
常见问题
Web 应用防火墙能检查 LLM prompt 吗?
可以,前提是开启了 request body 检查且 body 被解析为 JSON。将 processingConfig.request.mode 设为 HeadersAndBody,并添加一条带有 ctl:requestBodyProcessor=JSON 的 Coraza 规则,就能把 chat completions body 变成可检查的 ARGS,这样 OWASP Core Rule Set 规则和自定义 SecLang 签名都能针对 prompt 文本进行评估,而不只是 URL 和请求头。
如何将 OWASP CRS 应用到 MCP 工具调用上?
MCP 是基于 HTTP 的 JSON-RPC,所以同一个支持 body 的 WAF 能看到方法名、工具名以及每个工具参数作为 JSON 字段。CRS 规则然后针对参数值进行评估,这就是为什么 city 参数包含 ../../etc/passwd 会被识别为路径遍历,包含 script 标签会被识别为 XSS,而无需对 MCP server 做任何改动。
如何限制 MCP client 可以调用哪些方法?
写一条 Coraza 规则,用正则对 JSON-RPC 方法字段做否定匹配,例如 SecRule ARGS:json.method 配合 !@rx 只匹配 initialize、notifications/initialized、ping、tools/list 和 tools/call。列表之外的任何方法(如 resources/list 或 prompts/get)都会在网关处被 403 拒绝,在 MCP server 被联系之前就执行。这是协议层面的最小权限,在联系 server 之前就强制执行。
原文正式版本(含机器可读 markdown)见 https://webofmike.com/waf-for-llm-and-mcp-traffic/index.md