跳到主要内容

安全护栏行为

安全护栏会在 AISIX 调用上游模型之前、模型返回响应之后,或在两处添加内容策略检查。其共用运行时控制项决定哪些流量会被检查、匹配是否会阻止流量,以及调用方和操作员能够观测到什么。

内置关键词PII 安全护栏在网关内部评估内容。远程安全护栏将提取出的内容发送到审核服务,并应用其决策。选择安全护栏服务提供方比较了可用的内置和远程选项。

两类安全护栏都使用本页所述的行为。远程安全护栏还需要为审核服务故障配置明确策略。

检查位置

安全护栏的检查位置决定 AISIX 在哪里执行检查,以及阻断内容时会发生什么:

检查位置AISIX 检查内容被阻断时的效果
inputAISIX 发送到服务提供方之前的调用方请求。不会调用服务提供方。
outputAISIX 返回给调用方之前的响应内容。调用方不会收到被阻断的响应。
both请求和响应内容。在路由支持的两侧都应用同一个安全护栏。

输入安全护栏覆盖标准推理路由中包含文本的部分,包括 Chat Completions、Completions、Responses、Messages、Embeddings、图像和视频生成、音频语音及重排请求。AISIX 还会扫描可选的音频转录和翻译提示词、Realtime 文本帧及 MCP tools/call 参数。相同的输入检查也覆盖 Batch 和 Fine-tuning 请求载荷,以及透传路由请求体。

输出安全护栏会扫描 Chat Completions、Completions、Responses、Messages、音频转录和翻译返回的文本。它们还会扫描 Realtime 文本帧、MCP tools/call 结果、Batch 和 Fine-tuning 响应载荷,以及透传路由响应体。Realtime 二进制帧会直接中继,不执行安全护栏内容检查。

在透传路由上,安全护栏通过 passthrough_route 挂载作用域生效,并与调用方 API Key、团队和环境作用域并列。扫描文本遵循识别出的正文信封。携带可识别 LLM 信封(Chat、Completions、Responses)的请求会扫描提取出的消息或提示词文本;信封提取不出文本时回退为整个正文。不透明流量则把完整请求体和响应体作为文本扫描。

请求在转发前扫描。阻断判定会返回 422,并且不会接触上游。内置 pii 安全护栏无法把掩码结果写回服务提供方原生正文,因此仅配置 mask 动作时,匹配内容会不经掩码原样转发。如果匹配内容绝不能到达上游,请使用 block 动作。Presidio、Lakera 等安全护栏在透传流量没有回写通道时,则会把可掩码结果判定为阻断。有一种情况会覆盖上述全部规则。Anthropic thinking 块内部的掩码命中,对所有安全护栏类型都会原样转发,因为该块是带签名的。参见推理与思考内容

非流式响应先整体扫描再中继;阻断判定会返回 422,并扣下上游响应体。流式响应按护栏链的流式输出策略增量中继。hold-back 护栏会按窗口或全量缓冲帧,只有扫描文本通过后才释放;阻断判定会以一个终止性 SSE error 帧结束流,并丢弃被扣住的帧。各接口面使用各自协议的错误形态,并没有一个统一的帧。Chat Completions 和透传路由携带 content_filter/v1/messages 使用嵌套的 Anthropic 形态,错误类型取 Anthropic 合法值,且没有 code 字段。/v1/responses 则按上游区分:上游原生提供 Responses API 时,被保留的流会以 422 而不是一个帧被拒绝;AISIX 为不提供该 API 的服务提供方做桥接时,拒绝才是那个扁平的 Responses 错误事件。参见安全护栏拒绝

Files 路由不做筛查

安全护栏完全不作用于 Files APIPOST /v1/filesGET /v1/filesGET /v1/files/{id}DELETE /v1/files/{id}GET /v1/files/{id}/content 既不运行输入侧检查也不运行输出侧检查,无论请求作用域内挂了什么安全护栏、fail_open 取什么值。上传的文件会按调用方书写的样子原样离开网关,下载的文件也会按服务提供方存储的样子原样到达调用方。同一段文本放在对话消息里会被关键词、PII 或远程安全护栏拒绝,放在这里则根本不会被看到。

遥测不会反映这个缺口。 Files 的用量事件与经过筛查的请求逐字节一致:没有执行命中,guardrail_bypassed_reason 也是空的。因此即使在环境上挂了一条关闭式失败的安全护栏,也不会有任何信号提示这些路由没有被覆盖——从用量记录上看,没有任何安全护栏读过的 Files 请求,与安全护栏读过并放行的请求毫无区别。如果上传内容必须经过筛查,请在它到达网关之前完成。

/v1/batches/v1/fine_tuning/jobs 不受影响。它们的请求体是调用方提交的结构化 JSON,而不是上传的二进制块,这些请求体和对应的响应仍然会被扫描。

推理与思考内容

推理内容按方向划分,两侧并不对称。制定 PII 或关键词策略时请围绕这条边界来规划,不要假定安全护栏覆盖了具备推理能力的模型所接触的一切。

调用方发来的推理内容在扫描范围内。 被回放的推理内容和其他文本一样,都是调用方提供、进入模型的文本,因此会被扫描,并在相同的位置被脱敏:

路由扫描并脱敏的位置
/v1/responsesreasoning item 的 content[].text 及其 summary[].text
/v1/chat/completionsassistant 轮次的 reasoning_content
/v1/messagesthinking 块的文本——会被扫描,但请看下面的例外

Anthropic thinking 块永远不会被改写。 thinking 块内部的 mask 动作命中会被原样转发。对所有安全护栏类型都是如此,而不仅仅是内置的 pii——因为改写该块会让服务提供方为它签发的签名失效。

block 动作的规则仍然会在 thinking 块上生效,因此如果这类内容绝不能到达上游,请使用 block 动作。redacted_thinking 块则是另一种情况:它只携带服务提供方的加密数据,完全不向扫描贡献文本,因此没有任何规则能在其中命中,它同样会被原样中继。

模型生成的推理内容不在扫描范围内。 它在任何路由上都不会被扫描,也不会被脱敏——/v1/chat/completions/v1/messages/v1/responses 都是如此,流式和缓冲式都是如此。

这收窄了治理边界

模型生成的推理内容此前会在三条路径上被顺带扫描到:缓冲式的 /v1/messages/v1/responses,以及承载这两类信封的透传路由。现在不会了。如果一条 block 规则的关键词出现在模型生成的推理内容里、而不出现在模型返回的答案中,那么它此前会阻断这些响应,现在不再阻断。调用方可见的答案的扫描范围与此前完全一致。

如果你此前依赖了这一行为,请把该规则移到输入检查位置。否则请接受「模型生成的推理内容位于输出安全护栏范围之外」这一事实。

有一个例外同样不值得依赖。只要信封抽取不出任何文本,透传路由就会回落到把整个正文当作不透明文本扫描,而这种兜底会把推理内容一并卷进来。它适用于 AISIX 无法识别为已知信封的流量。它同样适用于能识别、但抽不出文本的信封,例如 output[] 中只有一个 reasoning item 的 Responses 响应。那是一种过度扫描的兜底,而不是对推理内容的处理。

脱敏无法触及 encrypted_content

/v1/responses 上,一个 reasoning item 可能携带 encrypted_content 字段。该字段存放的是服务提供方的加密数据。AISIX 会原样转发它,从不扫描,也从不改写。

因此对可读的 summary 做脱敏,并不会把敏感文本从服务提供方收到的那份密文副本中去掉。调用方能看到的明文被脱敏了,服务提供方拿到的仍然是该 item 完整的加密载荷。请把 reasoning item 的 encrypted_content 视为会以未脱敏形式离开你的边界的内容;如果这不可接受,请使用 block 动作而不是 mask 动作。

安全护栏匹配

选择钩子点后,需要确定安全护栏的作用范围:

  • 在 AISIX Cloud 中,创建安全护栏并把它附加到环境或选定的模型别名、MCP 服务器、调用方 API Key 或团队。Attachment 允许一个定义以明确优先级应用到多个 Scope。
  • 对于开源 AISIX 网关,在 resources.yaml 中声明安全护栏,并为它添加一条 guardrail_attachments 条目。两种来源判定作用范围的方式一致,因此文件里声明了、但没有任何 Attachment 的安全护栏不会检查任何流量。
启用前先附加

安全护栏的作用范围完全由它的 Attachment 决定。没有任何 Attachment 的安全护栏在任何地方都不生效,而这是一个合法状态、不是错误——它原本挂靠的模型、API Key 或团队可能已被删除,删除时会一并移除指向它们的 Attachment。控制台会把这样的安全护栏标记为未附加,网关也会为它记录一条告警。

由此有两点值得提前规划。删除最后一个 Attachment 不会放大安全护栏,而是让它静默——用这个动作让规则下线,而 enabled 用来表示规则是否存在。另外,通过 Admin API 创建的安全护栏在你 POST 一条 Attachment 之前不挂靠任何范围;控制台在创建表单里选定范围时会替你写入,直接调用 API 则不会。

当多个安全护栏同时适用于同一个请求时,AISIX 会把它们作为一条链路进行评估。任意一个安全护栏给出阻断判定,都会阻断请求或响应。

模型别名 Scope 和 MCP 服务器 Scope 选择的是单次请求不会同时具备的两个维度:MCP 工具调用不会解析出模型,模型请求也不会路由到 MCP 服务器。因此模型 Scope 的安全护栏永远不会检查 MCP 工具调用,MCP 服务器 Scope 的安全护栏也永远不会检查模型流量。若要同时覆盖两者,请把安全护栏附加到环境 Scope,或者两种 Attachment 各加一条。

如果同一个安全护栏通过 AISIX Cloud 中配置的多个匹配 Scope 附加到请求链路中,AISIX 只保留一份该安全护栏。优先级最高的 Attachment 生效;优先级相同时,Scope 越具体优先级越高,顺序为:调用方 API Key、团队、模型别名或 MCP 服务器、环境。模型别名与 MCP 服务器同级,因为单次请求不可能同时匹配这两者。

执行模式

安全护栏的 enforcement_mode 决定 AISIX 在安全护栏检测到匹配内容后如何处理。

选择以下两种执行模式之一:

  • block 拒绝匹配内容。如果省略 enforcement_mode,这是默认行为。
  • monitor 允许匹配内容通过,并记录安全护栏本应执行的操作。

基于检测的安全护栏,包括 PII 识别器、注入分类器和内容分类审核,都可能产生误报和漏报。当你需要先用真实流量调优策略再强制执行时,可以使用监控模式。

在 block 模式下,输入侧命中会在调用服务提供方前停止请求,输出侧命中会阻止调用方收到响应。

在监控模式下,调用方可见的请求和响应不会发生变化。原本在 block 模式下会以 422 Unprocessable Entity 拒绝的请求,会返回正常的上游结果。本应被脱敏护栏掩码的内容也会原样到达模型或调用方。

包含此行为的网关会把每次观察作为监控命中记录在该请求的用量记录中。一条命中会携带安全护栏名称、命中的检查位置,以及该安全护栏本应执行的动作:would_block 附带由代码生成的安全护栏种类和结果摘要,would_mask 则附带安全的命中次数。如果检查不可用,would_block 摘要还会包含由代码生成且取值范围有限的失败标签,例如 custom_timeout;它不会包含脚本提供的原因或被检查的内容。自定义脚本固定使用计数名称 custom,值为本应发生变化的文本片段数;脚本返回的计数名称和值会被忽略。

AISIX Cloud 会原样存储用量记录详情。因此,由旧版网关创建的记录可能保留脚本提供的旧版原因、计数名称和值,其中也可能包含由请求内容或密钥派生的值。升级网关只会保护新记录,不会改写历史记录;请相应限制对旧版用量数据的访问。

这些记录可用于先以监控模式上线新的安全护栏,观察命中率并调优策略,然后再切换到 block。对于 block 类观察,AISIX 还会在网关日志中以 info 级别记录命中:

guardrail in monitor mode observed a violation; not blocking (enforcement_mode=monitor)

流式输出

当强制执行的安全护栏覆盖 output hook 时,AISIX 会先保留流式响应内容,直到相关检查可以执行。根据安全护栏类型,AISIX 可能保留完整响应,也可能检查缓冲窗口。这样可以避免被阻断或未脱敏的内容在安全护栏判定前到达调用方,也能检测跨越流式分片的文本片段。

监控模式下的安全护栏不会使流式输出延迟。当所有输出安全护栏都以监控模式运行时,AISIX 会在流到达时转发。流结束后,AISIX 执行检查并记录所有匹配。

下方缓冲区上限不适用于仅包含监控模式的输出链路。AISIX 会交付并观测超大响应,而不是拒绝它。如果任何强制执行的安全护栏也覆盖输出钩子,AISIX 会保留流,以便强制执行链路在交付前检查。

暴露流式缓冲设置的安全护栏类型使用以下默认值:

{
"max_buffer_bytes": 262144,
"on_buffer_exceeded": "fail_closed"
}

对于 AISIX Cloud,请把这些字段添加到安全护栏的 config 对象中。对于开源 AISIX 网关,请直接添加到 resources.yaml 的安全护栏条目中。fail_closed 会阻断过大的输出。fail_open 会释放 AISIX 无法完整检查或改写的输出,因此只有在可用性比严格输出治理更重要时才使用。未暴露这些字段的安全护栏类型使用内置上限,并在溢出时按关闭式失败处理。

AISIX 无法扫描的帧

只有当安全护栏能读懂被保留的内容时,保留流式输出才有意义。本节的全部内容只在确实有输出侧安全护栏保留了流的前提下成立;没有这样一条安全护栏时,/v1/responses/v1/messages 会原样中继上游的帧,下面这些环节都不会运行。

在这两个路由上,AISIX 按帧读取缓冲后的响应;一个帧的 data: 负载是单个 JSON 文档、为空、或是 [DONE] 结束标记时,该帧才是可读的。在扫描运行之前,AISIX 会在这两个路由上先整理缓冲区,使阻断检查和脱敏改写看到完全相同的帧:

  • 一个帧中如果有多行 data:,会按照 server-sent events 规范把它们用换行符拼接后整体读取。这样的帧会被完整扫描和脱敏。
  • 上游没有写完终止符的最后一个帧,只要 AISIX 读得懂它,就会被补全并交付:负载可以解析,或者其中本来就没有可扫描的内容——空负载、[DONE] 结束标记,或者完全没有 data: 行。随后它会像其他帧一样被扫描和脱敏。
  • 负载不是单个 JSON 文档的已终止帧(例如纯文本)会从响应中移除,而不是未经扫描就放行。被移除的文本仍会进入阻断检查,因此这类帧中的违禁内容依然会阻断整个响应;但无法对它做结构化脱敏,所以该帧是被丢弃而不是被改写。
  • 两种方式都读不懂的、未终止的最后一个帧会在两项检查运行之前就被裁掉,它的文本既不会被放行,也不会被扫描。

调用方看到的现象是流式响应少了一些帧。AISIX 会为每条被裁剪的响应打一条 warn 级别日志,记录检查位置、丢弃的帧数与字节数以及原因,便于运维区分「被截断的响应」和「本来就短的响应」。完全不含 data: 行的帧(例如注释和保活帧)没有任何可扫描的内容,会原样保留;空负载和 [DONE] 结束标记同样保留。

当 AISIX 手上没有任何可扫描的内容时,它会直接拒绝该响应,而不是返回一个空的 200。这有两种情况:移除之后没有任何内容可返回;以及在 /v1/messages 上,被保留的流累积超过 1 MiB 仍未出现帧终止符。该拒绝携带失败标记 unscannable_body,表示安全护栏根本没有拿到可供扫描的内容——既不是命中了某条策略,也不是触及保留缓冲区上限(后者使用 output_buffer_exceeded)。在 /v1/responses 上它是一个 422

{
"error": {
"message": "response rejected: a guardrail could not evaluate it (unscannable_body)",
"type": "content_filter",
"code": "guardrail_unavailable"
}
}

/v1/messages 上,响应已经作为流开始返回,因此同样的拒绝会以终止性的 SSE error 事件送达,携带相同的消息。该事件使用 Anthropic 信封,因此携带的是 invalid_request_error 且没有 code 字段,而不是 OpenAI 风格路由会返回的 content_filterguardrail_unavailable

{
"type": "error",
"error": {
"type": "invalid_request_error",
"message": "response rejected: a guardrail could not evaluate it (unscannable_body)"
}
}

两种情况下该请求都会被记为安全护栏阻断。

这些拒绝不查询 fail_open。它们触发时,输出侧安全护栏已经保留了一批没有被任何环节扫描过的字节,放行这些字节并不是该设置所要求的行为。参见流式输出这一例外

缓冲并不意味着每个字节都被扫描过

AISIX 对一个帧所做的判断是它的负载能否解析——在不知道服务提供方可能发出哪些事件类型的前提下,这已是这一层能做出的最强判断。因此仍有一种形态会在内容未经扫描的情况下到达调用方:

  • 负载是合法 JSON、但事件类型不在 AISIX 建模范围内的帧。它能解析,所以整理缓冲区时会被保留;没有任何扫描环节、也没有任何脱敏环节能识别它,因此不会从中提取出文本,它的字节会原样中继出去。

这种情况是有意保留的:拒绝它会切掉今天本可以正确交付的响应。请把缓冲式输出安全护栏理解为对 AISIX 能读懂的响应文本的治理,而不是「放行的每个字节都已被检查」的保证。

有两种相邻的形态很容易被当成同一回事,但它们都不是。

Chat Completions 是丢失这类内容,而不是放行它。 /v1/chat/completions 同样会保留流式输出并扫描,但它不会按上面描述的方式整理帧,也从不原样中继上游字节——调用方收到的每个 chunk 都是从解码后的事件重新构造出来的。因此它的解码器读不懂的帧不会被未经扫描地交付,而是根本不会被交付。最清楚的例子是上游忽略 stream: true 而直接返回普通 JSON 响应体:Chat Completions 是依据请求自身的 stream 标志、而不是上游实际返回的内容来选择流式路径的,于是该响应体会进入 SSE 解码器,而解码器找不到任何 data: 行,解不出任何事件,调用方最终拿不到上游的任何内容。如果该模型配置了流式读取预算,这次尝试会改为失败转移,候选全部失败后调用方看到的是错误——两种情况下内容都是丢失,而不是未经扫描地放行。/v1/responses/v1/messages 不是这样:它们依据上游实际返回的内容,因此对 stream: true 返回 JSON 文档时会走缓冲扫描与脱敏路径。

透传路由是过度扫描,而不是扫描不足。 在那里解析不了的帧会把原始负载交给扫描,因此读不懂的帧是被过度扫描,而不是逃过扫描。透传路由同样会缓冲并扫描没有标注为 SSE 的响应体。这两条路径上安全护栏都只给出判定——透传响应从不被改写,因此配置了掩码动作的规则在这里只能阻断或放行,被放行的响应体会原样中继。

安全护栏无法完成检查时

有两类不同的原因会让安全护栏无法给出决策,它们覆盖的类型范围不同。

  • 后端不响应。 会调用外部服务的安全护栏类型可能无法访问它、超时、被限流,或调用被拒绝。这适用于 AWS Bedrock Guardrails、Azure AI Content Safety、Azure AI Content Safety 文本审核、阿里云内容安全、阿里云 AI 安全护栏、Lakera Guard、OpenAI Moderation、Presidio、语义筛查(它会调用向量嵌入模型)以及自定义脚本。自定义脚本还可能因为它自身的问题而给不出判定——抛异常、超时,或返回 AISIX 读不懂的判定。
  • 没有可交给安全护栏扫描的内容。 无法按 UTF-8 解码的请求体,或 AISIX 读不懂的响应。这类情况由网关代表护栏链发起,发生在任何安全护栏运行之前,因此覆盖所有类型。

fail_open 对这两类原因、在所有类型上都生效。内置关键词PII 安全护栏不会调用后端,因此只有第二类原因会落到它们身上——但第二类原因同样属于它们,所以该字段对这两个类型并不是空操作。

fail_open 现在对 keywordpii 生效

早期版本的网关对这两个类型完全忽略 fail_open。如果某条 keywordpii 安全护栏携带 fail_open: true——而设置它的时候该字段还什么都不做——那么网关一升级,它就会停止拒绝 AISIX 读不懂的请求体,无需任何运维操作,也不需要改动控制面。请在升级前而不是升级后检查这些配置行。

各设置分别管哪个检查侧

设置检查侧适用类型默认值当值为 true当值为 false
fail_open输入侧所有类型false放行未扫描的请求。阻断请求。
output_fail_open输出侧keywordpii 外的所有类型false放行未扫描的响应。阻断响应。
fail_open输出侧keywordpiifalse放行未扫描的响应。阻断响应。

会调用外部服务的类型带有自己的 output_fail_open 来管输出侧,于是 fail_open 只管输入侧。keywordpii 没有 output_fail_open——它们从不调用外部服务——因此这一个取值同时管它们的两个检查侧;给这两个类型传 output_fail_open 会被拒绝。

两侧都默认阻断:AISIX 无法完成检查的流量不会被放行。如果某个安全护栏的可用性比治理更重要,把它的 fail_open 设为 true——绕过行为仍会被记录,可以据此告警。

在 AISIX Cloud 中,fail_open 是安全护栏上的顶层字段,所有类型的创建和编辑表单上都有该选项;output_fail_open 位于安全护栏的 config 对象内,控制台为部分类型提供了该选项,而 Admin API 对所有拥有该字段的类型都接受它。在开源 AISIX 网关中,两者都直接写在 resources.yaml 的安全护栏条目上。

0.11.0 变更

fail_open 此前默认为 true。未显式设置该字段的安全护栏,现在会在无法访问其后端服务时返回 422 阻断,而不再像以前那样放行未扫描的请求。通过 AISIX Cloud 创建的安全护栏不受影响——控制面一直会为该字段存储显式值。受影响的是 resources.yaml 中省略了该字段的安全护栏,以及升级后新建的安全护栏。

0.11.0 变更

mandatory 字段已移除。它此前会让远程安全护栏故障即使在 fail_opentrue 时也阻断请求,并让无法访问的监控模式安全护栏变为致命故障。由于 fail_open 现在默认为 false,请改为显式设置——block 模式下的 fail_open: false 会拒绝安全护栏无法检查的内容。仍然携带 mandatory 的配置会被拒绝。

什么会拒绝 AISIX 读不懂的内容

unscannable_body 拒绝由网关代表护栏链发起,而不是来自任何一条安全护栏的判定,因此作用域内有哪些配置行,决定了它会不会触发。

只有当解析出的护栏链中至少有一条安全护栏读取该侧交互在该侧是关闭式失败时,AISIX 读不懂的请求、MCP 工具结果,或非流式的音频转录和翻译响应才会被拒绝。两个条件必须落在同一条安全护栏上。设想一条链里有一条「仅输出侧、关闭式失败」和一条「仅输入侧、开放式失败」的安全护栏:这条链既读取请求,也含有关闭式失败的配置行,但其中没有任何一条同时满足两个条件,因此没有任何一条能成为拒绝该请求的理由。

有两种落在该条件之外的情况,两者都会让流量停留在未配置安全护栏时的状态:

  • 作用域内没有任何安全护栏读取该侧。 只由输出侧挂载解析出来的护栏链从来就拿不到请求体,因此它不可能成为请求被拒绝的理由。
  • 作用域内读取该侧的安全护栏全都设了 fail_open: true 网关拿不到可扫描的内容,意味着这次检查没有执行,而这恰恰是 fail_open 所管辖的情形,因此该设置会放行这些流量而不是拒绝它们。AISIX 会在用量记录上以 unscannable_body 标记把这次放行记录为一次绕过。

hook_point 默认为 bothfail_open 默认为 false,因此这次收窄只会影响显式设置过其中之一的部署。

AISIX 解码不出来的转录文本

非流式的 /v1/audio/transcriptions/v1/audio/translations 响应,如果响应体不是合法 UTF-8,就属于上面这条规则的一种情形。AISIX 会尽力解码,把每个非法字节序列替换为替换字符,好让护栏链有内容可读;若不因此拒绝,中继给调用方的就仍然是原始字节——于是被替换掉的那些字节到达调用方时,没有被任何检查读过。

当上述条件成立时——作用域内有一条安全护栏既读取响应侧、又在该侧是关闭式失败,两个条件落在同一条安全护栏上——该响应会被拒绝。拒绝是一次 422,携带失败标记 unscannable_body,并且该请求会被记为安全护栏阻断:

{
"error": {
"message": "response rejected: a guardrail could not evaluate it (unscannable_body)",
"type": "content_filter",
"code": "guardrail_unavailable"
}
}

条件不成立时,原始字节照旧中继,而记录什么取决于它是以上一节的哪一种方式落在门外的。如果护栏链读取响应侧、但该侧的读取者全都设了 fail_open: true,那就是本该读取该转录、由失败策略把它放行的,因此这次放行会在用量记录上以 unscannable_body 标记记为一次绕过。如果作用域内根本没有任何安全护栏读取响应侧,那就从来不存在要筛查该转录这回事:转录不经扫描原样中继,也不会记录任何东西,与完全没有配置安全护栏的部署一致。

只要作用域内有安全护栏读取响应侧,无论门是否成立,转录文本中解得出来的那部分都仍然会被扫描——这一点与读不懂的请求体不同,后者根本不会被扫描。转录文本是调用方要阅读的正文,因此只有被解码替换掉的那些字节没有被读过。所以可读文本中的策略命中依然会按普通方式阻断该响应;也因此同一个请求上完全可能既记为 guardrail_blocked,又带有取值为 unscannable_bodyguardrail_bypassed_reason

这里说的只是解码不出来的字节,别的都不涉及。合法 UTF-8 的 textsrtvtt 转录不受影响,能按 JSON 解析出来的响应体也不受影响——jsonverbose_json 转录经 JSON 解析器读取,而它交不出非法文本。判定依据是服务提供方返回的字节,而不是调用方请求的 response_format:无论请求的是哪种格式,解析不出 JSON 的响应体都会落到纯文本这条路径上。

监控模式不会豁免一条配置行

enforcement_mode: monitor 降级的是安全护栏自身的判定。上述拒绝背后没有判定可供降级,因此监控模式并不会让配置行豁免于它们——全是监控模式的护栏链,仍然会拒绝扫描器读不懂的请求体,仍然会拒绝无法解析的 MCP 工具结果,也仍然会拒绝解码不出来的非流式转录或翻译响应。管辖它们的是 fail_open,在监控模式下与在 block 模式下一样。

后端故障是另外一半,在那里监控模式的行为与名字一致:无法访问的后端会像其他观测一样被记录,请求继续放行。如果希望后端无法访问时拒绝流量,请使用 block 模式并设置 fail_open: false

还有一些拒绝在纯监控模式的护栏链上根本不可能出现,原因不同:监控模式的安全护栏既不保留输出也不做掩码。因此 output_buffer_exceeded 和保留流式输出才会出现的 unscannable_body 没有可发生的保留过程,mask_writeback_failed 也没有可失败的掩码。只要链中出现一条处于 block 模式的安全护栏,这三个拒绝就都会触发,且不会被降级。请求侧的 unscannable_body、MCP 的那些检查,以及非流式的音频转录和翻译响应都不属于这一类——它们既不需要保留流,也不需要掩码——所以纯监控模式的护栏链仍会触发它们。参见安全护栏拒绝

流式输出这一例外

上面那个条件在流式输出这条路径上有意不再适用。

一旦输出侧安全护栏已经在保留流式响应,那么最终没有留下任何可扫描内容的流,无论 fail_open 取何值都会被拒绝。在那里遵从该设置并不等于跳过一次拒绝,而是要放行一批已经缓冲、却从未被任何环节扫描过的字节——那是另一个决定。output_buffer_exceeded 同样不受该条件约束,mask_writeback_failed 也是:无法写回响应体的掩码,意味着要把内容原样未脱敏地转发出去。

这并不是一个罕见的边角情况。keyword 沿用默认的保留式流式策略,pii 则设置了自己的策略,因此这两个类型上任何处于 block 模式的输出侧配置行都会走这条路径。monitor 模式的配置行不会:监控模式从不保留输出,因此它碰不到这条例外。

MCP 工具结果按常规条件判断:/mcp 从不保留流,这正是它的检查仍然遵从 fail_open 的原因。

当 AISIX 因安全护栏无法返回决策而允许流量时,请求或响应会在未经检查的情况下继续,并会在请求的用量记录上记录这次绕过,详见调用方响应与遥测

调用方响应与遥测

当安全护栏阻断代理流量时,AISIX 会保留所请求端点的错误约定:

  • 兼容 OpenAI 的路由返回 422content_filter
  • 透传路由返回 422,并使用同样的 OpenAI 风格 content_filter 错误信封。
  • 兼容 Anthropic 的非流式错误返回 422invalid_request_error
  • 兼容 Anthropic 的流式响应会以 Anthropic SSE 错误帧暴露阻断结果,错误类型取 Anthropic 合法值,且没有 code 字段。
  • Realtime 会话会以一个 WebSocket 错误帧加一次关闭来暴露阻断结果,因为此时已经没有状态行可以承载它。

各接口面的确切信封,以及关闭式失败的拒绝在消息中指名的失败标记词表,参见安全护栏拒绝

MCP 安全护栏阻断会遵循 MCP 协议形态。AISIX 返回 HTTP 200 和一个标记了 isError 的 JSON-RPC result,而不是协议错误,因此调用方 Agent 会把该拒绝当作工具输出读取并据此调整。参见响应头与错误码

安全护栏指标

AISIX 会把每次计时的安全护栏执行记录到 aisix_guardrail_latency_seconds Prometheus 直方图中。标签标识安全护栏名称、种类、阶段和结果,其中包括监控模式下的 would_block/would_mask 结果以及 fail-open 的 bypassed 结果。在包含此行为的网关上,关闭式失败的 blockedbypassed,以及监控模式下失败的 would_block,都会通过 error_type 标签携带取值范围有限的失败原因。普通策略命中使用 noneresult 标签在关闭式失败的拒绝上仍然是 blocked,不会另起取值,因此请用 error_type 来区分故障与策略决策。流式窗口模式可以为一条输出响应记录多次执行;同步的逐字段 PII 和关键词脱敏操作不计入该直方图。可使用该指标跟踪各安全护栏的延迟(P50/P95/P99)、比较本地与远程检测,并观察阻断率和绕过率。标签和 PromQL 示例请参见指标参考

绕过原因

开放式失败的安全护栏不会阻断请求或响应。AISIX 会把原因记录在该请求的用量记录上,字段名为 guardrail_bypassed_reason,在 AISIX Cloud 中显示为 Bypass reason。所有路由的用量事件都会携带该字段。唯一的例外是 AISIX 为服务提供方在批处理内部完成的工作补写的计费行:那里没有代理过任何请求,因此从未解析出护栏链,也就不存在被绕过的可能。

取值是安全护栏类型自己的失败标记——lakera_timeoutbedrock_5xxcustom_script_error 等等——再加上网关自己发起的一个取值 unscannable_body,用于它因无法扫描而放行的内容——请求体和响应体都算。这与关闭式失败的拒绝在消息中指名的是同一套受限词表,因此同一次故障无论配置成哪个方向读起来都一致。取值会被限制为小写 az、数字和下划线,最长 64 个字符;一次请求中第一次绕过胜出,因此绕过了两次的请求上报的是较早的那个标记。

阅读该字段时有两点需要注意。

它与 guardrail_blocked 不是互斥的。 一条护栏链可能在某个成员上开放式失败,同时被另一个成员拒绝;输入侧也可能在某个提示词上开放式失败,而服务提供方已经回答之后输出侧才拒绝了该响应。这两种都是确实有内容未经筛查的请求,而后者在合规上更为关键。「未经筛查地到达了服务提供方」是这两个字段合起来读出来的结论,绝不是单看这一个字段。

该字段为空并不证明每个字节都被筛查过。 有几条路径会无条件地扫描一份尽力而为的解码副本,完全不查询任何失败策略。它们既不拒绝,也不记录绕过:

  • Batch 和 Fine-tuning 的响应体。
  • 透传路由的请求体和响应体,两侧都始终按尽力而为解码的文本扫描。

由于关闭式失败的配置行在这些位置同样不会拒绝,两个方向上的不对称是一致的:这些位置的表现就像没有配置任何失败策略一样。

有一种形态看起来该列进上面这份清单,其实不该。/v1/audio/transcriptions/v1/audio/translations/v1/images/edits 上不是合法 UTF-8 的 multipart prompt 部分,会在任何安全护栏运行之前就被 400 拒绝——不论是否挂载了安全护栏,也不论 fail_open 取何值。那是对请求本身的结构性校验,不是安全护栏的决定,因此它既不作为安全护栏拒绝,也不会记录绕过。

还有一种形态确实查询了某个策略,只是查的不是这一个。被保留的流超出 max_buffer_bytes 时,如果设了 on_buffer_exceeded: fail_open,缓冲内容会未经扫描、未经脱敏地放行,并完全跳过流结束时的扫描。运维是通过另一个设置选择了这个取舍,因此它不会被记为绕过。

Prometheus 计数器是比该字段更窄的信号。aisix_guardrail_bypasses_total 以及 aisix_guardrail_latency_secondsresult="bypassed" 切片统计的是以绕过收场的安全护栏执行,因此后端故障会同时出现在指标和用量记录里;而网关发起的 unscannable_body 放行没有运行任何安全护栏,只会出现在用量记录里。

下一步

接下来,请选择服务提供方或配置网关内策略: