Presidio 安全护栏
Presidio 安全护栏使用 Presidio 检测和匿名化敏感数据。Presidio 是开源 PII 引擎,需要由你自行运行。AISIX 会为每个请求或响应调用 Presidio analyzer。检测到的实体可以被阻断,也可以按你选择的操作方式匿名化后继续转发。
与内置 pii 安全护栏相比,Presidio 增加了:
- NER/ML 实体:可识别正则难以表达的实体,例如
PERSON、LOCATION、NRP以及其它 Presidio 识别器实体。 - 匿名化操作方式:可替换为实体占位符、用星号遮盖、用 SHA-256 哈希,或直接删除片段。
- 在你自己的网络内分析:无需 Presidio 供应商 Key。发送用于 PII 分析的文本会留在你的 Presidio 部署中;发送给所配置模型服务提供方的请求仍会离开你的基础设施,发 生脱敏时会先完成匿名化。
Presidio 由两个独立的 HTTP 服务组成,AISIX 分别通过 base URL 访问:
- analyzer 响应
POST /analyze,返回文本中检测到的实体类型、偏移量和置信度分数。对于配置的每个检查位置,AISIX 都会为每个文本片段调用一次。 - anonymizer 响应
POST /anonymize,返回改写后的文本。只有当片段中存在生效动作为mask的检测结果时,AISIX 才会调用它,因此仅阻断的安全护栏永远不会用到 anonymizer。但无论如何,配置中都必须填写它的 URL。
Presidio 最初是 Microsoft 项目,现已转为由 Data Privacy Stack 组织进行社区治理。发行镜像发布在 ghcr.io/data-privacy-stack/。mcr.microsoft.com/presidio-* 上已有的标签仍然可用,但不再发布新版本,因此新部署请使用 ghcr.io 镜像。
本指南将介绍如何部署两个 Presidio 服务,创建带有按实体动作配置的 Presidio 安全护栏,并验证匿名化和阻断行为。
前提条件
开始前请准备:
- 阅读安全护栏行为,了解钩子点、执行模式和远程故障处理方式。
- 以下配置路径之一:
- AISIX Cloud,其中包含环境、已接入的网关和具有写权限范围的 Admin Token。对于 On-Premises,请按照 AISIX Cloud 快速入门操作。如需申请混合云访问权限,请联系 API7。
- 加载声明式
resources.yaml文件的开源 AISIX 网关。
- 可以发送 Chat Completions 请求的模型别名和调用方 API Key。
- Docker 或 Kubernetes 集群,用于运行 Presidio analyzer 和 anonymizer。
curl。AISIX Cloud 路径还会使用jq。
部署 Presidio
两个服务都监听 PORT 环境变量指定的端口(默认 3000),并且都提供 GET /health。它们都以 Gunicorn 运行各自的 Flask 应用,进程数由 WORKERS 决定,WORKERS 默认为 1。
两个服务都不对调用方做任何认证。只要能访问 analyzer,就能向它提交文本,因此请把两个服务部署在只有网关可以访问的内部网络中。参见限制 Presidio 的访问范围。
使用 Docker 本地评估
在本地评估时,创建网络并在其中启动两个 Presidio 服务:
docker network create aisix-guardrails
docker run -d --name presidio-analyzer --network aisix-guardrails \
-p 127.0.0.1:5002:3000 ghcr.io/data-privacy-stack/presidio-analyzer:latest
docker run -d --name presidio-anonymizer --network aisix-guardrails \
-p 127.0.0.1:5001:3000 ghcr.io/data-privacy-stack/presidio-anonymizer:latest
把网关容器连接到 aisix-guardrails,或在部署中提供等效网络连通性。开源快速入门使用以下命令:
docker network connect aisix-guardrails aisix-quickstart
从主机确认 analyzer 可响应:
curl -sS -X POST "http://127.0.0.1:5002/analyze" \
-H "Content-Type: application/json" \
-d '{
"text": "my email is alice@example.com",
"language": "en"
}'
响应会列出检测到的 EMAIL_ADDRESS 实体及其偏移量和置信度分数。
使用 Docker Compose 部署
在单台主机上长期运行时,请固定镜像版本而不是使用 latest,并为每个服务配置健康检查。不要发布宿主机端口,这样只有同一网络中的容器才能访问这两个服务:
name: presidio
services:
presidio-analyzer:
image: ghcr.io/data-privacy-stack/presidio-analyzer:2.2.364
environment:
WORKERS: "4"
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/health"]
interval: 10s
timeout: 3s
start_period: 90s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
memory: 4g
presidio-anonymizer:
image: ghcr.io/data-privacy-stack/presidio-anonymizer:2.2.364
environment:
WORKERS: "4"
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/health"]
interval: 10s
timeout: 3s
start_period: 30s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
memory: 512m
networks:
default:
name: presidio
❶ WORKERS 决定 Gunicorn 进程数,也就是该容器能同时分析的请求数。默认值 1 会让所有安全护栏调用串行排队。参见规划 analyzer 容量。
❷ analyzer 会先加载语言模型再开始提供服务,因此健康检查应设置足够长的启动等待时间,而不是依赖较短的重试次数。
启动这组服务,然后把网关接入同一个 presidio 网络。Compose 会把每个服务名注册为网络别名,网关可以直接用该名称访问:
docker compose up -d
docker network connect presidio aisix-quickstart
采用这种部署方式时,安全护栏使用 http://presidio-analyzer:3000 和 http://presidio-anonymizer:3000 作为 URL。
在 Kubernetes 上部署
以下清单会在 presidio 命名空间中运行两个服务,并将端口 3000 限制为仅允许显式添加标签的命名空间调用。镜像本身以非特权用户运行,除 /tmp 外不需要任何可写路径:
apiVersion: v1
kind: Namespace
metadata:
name: presidio
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: presidio-analyzer
namespace: presidio
spec:
replicas: 2
selector:
matchLabels:
app: presidio-analyzer
template:
metadata:
labels:
app: presidio-analyzer
spec:
containers:
- name: analyzer
image: ghcr.io/data-privacy-stack/presidio-analyzer:2.2.364
ports:
- containerPort: 3000
env:
- name: PORT
value: "3000"
- name: WORKERS
value: "2"
resources:
requests:
cpu: "1"
memory: 2Gi
limits:
memory: 3Gi
startupProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 60
readinessProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 20
timeoutSeconds: 3
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: presidio-analyzer
namespace: presidio
spec:
selector:
app: presidio-analyzer
ports:
- port: 3000
targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: presidio-anonymizer
namespace: presidio
spec:
replicas: 2
selector:
matchLabels:
app: presidio-anonymizer
template:
metadata:
labels:
app: presidio-anonymizer
spec:
containers:
- name: anonymizer
image: ghcr.io/data-privacy-stack/presidio-anonymizer:2.2.364
ports:
- containerPort: 3000
env:
- name: PORT
value: "3000"
- name: WORKERS
value: "2"
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
memory: 512Mi
startupProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 24
readinessProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /health
port: 3000
periodSeconds: 20
timeoutSeconds: 3
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: presidio-anonymizer
namespace: presidio
spec:
selector:
app: presidio-anonymizer
ports:
- port: 3000
targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-aisix-gateways
namespace: presidio
spec:
podSelector:
matchExpressions:
- key: app
operator: In
values:
- presidio-analyzer
- presidio-anonymizer
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
aisix.ai/presidio-client: "true"
ports:
- protocol: TCP
port: 3000
❶ analyzer 会在启动时加载语言模型。设置失败阈值足够大的 startup probe,可以避免 liveness probe 在首次加载较慢时重启容器 ,同时在容器能够响应之前不把它加入 Service。每个探针的 timeoutSeconds 都要大于默认的 1 秒,因为负载中的 analyzer 有时需要更长时间才能响应 /health。
❷ 两个镜像本身已经以用户 1001 运行,因此只要 /tmp 可写,它们就能在只读根文件系统下工作。
为每个允许运行 AISIX 网关并调用 Presidio 的命名空间添加标签,然后应用清单并等待两个 Deployment:
kubectl label namespace YOUR_GATEWAY_NAMESPACE aisix.ai/presidio-client=true
kubectl apply -f presidio.yaml
kubectl -n presidio rollout status deployment/presidio-analyzer
kubectl -n presidio rollout status deployment/presidio-anonymizer
获准命名空间中的网关可使用 http://presidio-analyzer.presidio.svc.cluster.local:3000 和 http://presidio-anonymizer.presidio.svc.cluster.local:3000 作为对应 URL。该 NetworkPolicy 需要集群网络插件实施 Kubernetes NetworkPolicy;如果没有此类插件,命名空间标签不会限制流量。
规划 analyzer 容量
需要做容量规划的是 analyzer。anonymizer 只做字符串替换,占用很小,运行时通常不到 100 MB。
按以下特性规划 analyzer:
- 每个 worker 都会加载一份自己的语言模型。 单 worker 的 analyzer 常驻内存约 750 MB,每增加一个 worker 大致再增加同样的量。内存上限应按
WORKERS计算,而不是取一个固定值。 - 一个 worker 同时只处理一个请求。 在默认的
WORKERS: 1下,安全护栏调用会互相排队,即使单次分析本身很快,请求延迟也会随并发上升。请把WORKERS提高到接近容器的 CPU 配额,再通过增加副本继续扩容。 - AISIX 对每个文本片段分别、依次分析。 消息较多的会话在输入检查位置会产生多次 analyzer 调用(每条消息一次),模型响应在输出检查位置还会再产生一次。安全护栏的
timeout_ms是按单次调用计算的,因此会话越长,一个卡住的 analyzer 能给单个请求带来的最坏延迟就成倍放大。 - 延迟同时取决于文本长度,而不只是调用次数。 请使用与你真实流量长度相当的提示词做测量,而不是用简短样本。
由于安全护栏在请求路径上同步执行,analyzer 的容量就是网关的容量。请关注安全护栏延迟以及安全护栏行为中描述的故障计数,并为两个检查位置明确设置 fail_open:容量不足的 analyzer 一旦开始超时,流量要么被阻断,要么在未经检查的情况下放行。
限制 Presidio 的访问范围
Presidio API 自身没有认证、没有鉴权,也没有限流。请把这两个服务视为网关的内部组件,而不是对外暴露的 API:
- 本地评估时,如上所示将宿主机端口绑定到环回地址。生产 Docker 部署不要为任一服务发布宿主机端口,并将两者保留在网关的私有网络中。
- 在 Kubernetes 中,将两个 Service 保持为
ClusterIP。上述清单只允许带有aisix.ai/presidio-client=true标签的命名空间;如果只允许这些命名空间中的特定 Pod 连接,还应为该策略添加podSelector。 - 如果请求文本需要跨越你无法控制的网络边界,请在服务前用代理终止 TLS。
Presidio 只记录自身的生命周期事件,不记录被分析的文本,因此被分析内容不会进入它的容器日志。AISIX 同样不会把命中的值写入网关日志和用量记录,这些记录只包含实体名称和按实体统计的次数。
分析英语以外的语言
发布的 analyzer 镜像只内置英语模型 en_core_web_lg,并且只为 en 注册识别器。请求其它语言会返回 HTTP 500 和 No matching recognizers were found to serve the request,安全护栏会把它视为远程故障,而不是一次正常的检测结果。
要增加语言,需要构建一个包含额外模型和三份彼此一致的配置文件的镜像。analyzer 在启动时会交叉校验这三份配置,一旦不一致就会拒绝启动,并报告 Misconfigured engine, supported languages have to be consistent:
FROM ghcr.io/data-privacy-stack/presidio-analyzer:2.2.364
USER root
RUN python -m spacy download es_core_news_md
USER 1001
COPY nlp.yaml recognizers.yaml analyzer.yaml /app/conf-multilang/
ENV NLP_CONF_FILE=/app/conf-multilang/nlp.yaml
ENV RECOGNIZER_REGISTRY_CONF_FILE=/app/conf-multilang/recognizers.yaml
ENV ANALYZER_CONF_FILE=/app/conf-multilang/analyzer.yaml
nlp.yaml 把每种语言映射到一个模型:
nlp_engine_name: spacy
models:
- lang_code: en
model_name: en_core_web_lg
- lang_code: es
model_name: es_core_news_md
analyzer.yaml 列出相同的语言:
supported_languages:
- en
- es
default_score_threshold: 0
recognizers.yaml 是内置的识别器注册表,语言列表也要保持一致。请从镜像中导出该文件再修改它的 supported_languages 段,因为该文件同时枚举了所有内置识别器:
docker run --rm --entrypoint sh \
ghcr.io/data-privacy-stack/presidio-analyzer:2.2.364 \
-c 'cat presidio_analyzer/conf/default_recognizers.yaml' > recognizers.yaml
三份文件都注册该语言,才能让基于规则的识别器对该语言生效。如果只在 nlp.yaml 中增加语言,analyzer 只会返回该语言的 NER 实体(例如 PERSON),而找不到 EMAIL_ADDRESS,因为基于规则的识别器仍然只注册给英语。
镜像部署完成后,把安全护栏的 language 字段设置为新增的语言代码。一个安全护栏只分析一种语言,因此混合语言流量需要为每种语言配置一个安全护栏,并分别挂载到对应流量上。
创建 Presidio 安全护栏
下面的示例会对邮箱和人名做匿名化,但在两个检查位置阻断美国社会安全号码(SSN)。请选择一种配置路径,然后使用共同的验证步骤。
导出两条路径都会用到的网关参数:
# AISIX_PROXY 不带尾部斜杠,也不带端点路径。
# 本地快速入门使用 http://127.0.0.1:3000。
export AISIX_PROXY="YOUR_AISIX_GATEWAY_URL"
export AISIX_API_KEY="YOUR_CALLER_API_KEY"
export AISIX_MODEL="gpt-4o-mini"
AISIX Cloud
导出控制面连接信息:
# AISIX_CP 包含 /api,且不带尾部斜杠。
# 本地 On-Premises 快速入门使用 http://localhost:8080/api。
export AISIX_CP="YOUR_AISIX_CLOUD_ADMIN_API_BASE_URL"
export AISIX_TOKEN="YOUR_ADMIN_TOKEN"
export ENV_ID="YOUR_ENVIRONMENT_ID"
创建安全护栏并记录其 ID:
export GUARDRAIL_ID=$(curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/guardrails" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "presidio-pii-policy",
"enabled": false,
"hook_point": "both",
"fail_open": false,
"kind": "presidio",
"config": {
"analyzer_url": "http://presidio-analyzer:3000",
"anonymizer_url": "http://presidio-anonymizer:3000",
"entities": [
{
"type": "EMAIL_ADDRESS"
},
{
"type": "PERSON"
},
{
"type": "US_SSN",
"action": "block"
}
],
"default_action": "mask",
"operator": "replace",
"score_threshold": 0.5,
"language": "en"
}
}' | jq -r '.guardrail.id')
❶ entities 会将检测限制为列出的 Presidio entities。entities 为空时,会使用 Presidio 的完整识别器集合。
❷ default_action: "mask" 会对列出的实体做匿名化,除非条目自行覆盖动作。本示例通过 action: "block" 阻断美国社会安全号码。
❸ operator: "replace" 会把命中的值替换为实体占位符,例如 <EMAIL_ADDRESS>。当下游系统需要稳定的假名而不是占位符时,请使用 hash。
❹ score_threshold 会丢弃低于该置信度的分析结果。省略该字段则接受 analyzer 返回的全部结果。选择取值前请先阅读调优检测效果。
安全护栏只在挂载到的位置生效。将其挂载到整个环境,使其应用于所有流量:
curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/guardrails/$GUARDRAIL_ID/attachments" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"scope_type": "env"
}'
env 作用域会将安全护栏应用于该环境中的所有流量,且不接受 scope_id。如需缩小挂载范围,请使用 model、api_key 或 team,并提供对应的 scope_id。
挂载创建完成后再启用安全护栏:
curl -sS -X PATCH "$AISIX_CP/environments/$ENV_ID/guardrails/$GUARDRAIL_ID" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
启用后的配置会自动投射到已接入的网关。
开源 AISIX 网关
在已经定义示例模型和调用方 API Key 的资源文件中添加该安全护栏:
guardrails:
- name: presidio-pii-policy
enabled: true
hook_point: both
fail_open: false
kind: presidio
analyzer_url: http://presidio-analyzer:3000
anonymizer_url: http://presidio-anonymizer:3000
entities:
- type: EMAIL_ADDRESS
- type: PERSON
- type: US_SSN
action: block
default_action: mask
operator: replace
score_threshold: 0.5
language: en
guardrail_attachments:
- guardrail_id: presidio-pii-policy
scope_type: env
priority: 100
模型服务提供方字段直接位于安全护栏条目下,而不是 config 下。请使用网关进程可以访问的 analyzer 和 anonymizer URL。安全护栏只在 Attachment 指定的范围内生效:请添加 guardrail_attachments 条目引用它,否则它虽然会被加载,但不会检查任何流量。
请验证完整文件,然后重新加载网关。可运行的 Docker 工作流参见重新加载资源文件。
由于该安全护栏使用 hook_point: both,AISIX 也会在模型响应返回调用方前应用相同的匿名化。使用示例中的失败关闭设置时,原始请求值不会到达上游模型,原始响应值不会到达调用方。当 Presidio 不可用时,将 fail_open 或 output_fail_open 设为 true 会改变这一保证。对于流式模型响应,请参见流式输出。
命中的值也不会写入网关日志和用量记录。用量记录只包含按实体名称统计的脱敏次数。
验证匿名化
AISIX Cloud 投射是异步的。如果第一个请求尚未体现安全护栏,请等待网关应用最新修订后重试。收敛检查参见资源投射。
发送同时包含邮箱地址和人名的提示词:
curl -sSi -X POST "$AISIX_PROXY/v1/chat/completions" \
-H "Authorization: Bearer ${AISIX_API_KEY}" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{
"model": "${AISIX_MODEL}",
"messages": [
{
"role": "user",
"content": "Send the invoice to alice@example.com and ask Alice Johnson to confirm"
}
]
}
EOF
如果上游模型调用成功,响应会以 HTTP/1.1 200 OK 开头。AISIX 在调用上游模型前匿名化提示词,因此服务提供方收到的是:
Send the invoice to <EMAIL_ADDRESS> and ask <PERSON> to confirm
验证阻断
包含被阻断实体的请求会被拒绝:
curl -sSi -X POST "$AISIX_PROXY/v1/chat/completions" \
-H "Authorization: Bearer ${AISIX_API_KEY}" \
-H "Content-Type: application/json" \
--data-binary @- <<EOF
{
"model": "${AISIX_MODEL}",
"messages": [
{
"role": "user",
"content": "my ssn is 987-65-4321"
}
]
}
EOF
响应以 HTTP/1.1 422 Unprocessable Entity 开头,并包含标准内容过滤错误:
{
"error": {
"message": "request blocked by content policy (guardrail 'presidio-pii-policy')",
"type": "content_filter"
}
}
命中的原始值不会回显。
Presidio 会主动排除部分公开用于示例的占位标识符,其中包括社会安全号码 123-45-6789。用这类值做测试时请求会直接放行,让一条本来正常工作的策略看起来失效。上面的值来自 Presidio 自己的识别器测试套件,并且会被 Compose 与 Kubernetes 示例固定的 2.2.364 版本检测到。
调优检测效果
Presidio 会为每个检测结果打分,而这个分数既取决于值本身,也取决于它周围的词。同一个电话号码,文本为 phone 425-555-0143 时得分 0.75,文本为 call 425-555-0143 时只有 0.4,因为附近出现上下文词会提高识别器的置信度。因此 score_threshold 取 0.5 会静默丢弃后一种写法。
据此调优安全护栏:
- 先不设置
score_threshold,直接调用/analyze观察真实流量的分析结果。只有在需要抑制某个具体误报时才加上阈值。 - 短提示词上的 NER 实体容易过度匹配,
PERSON尤其容易匹配句首的首字母大写单词。 - 明确列出
entities。entities为空时会把default_action应用到 Presidio 能识别的所有实体,包括URL和DATE_TIME,会让普通提示词也被大量脱敏。 - 新策略先按执行模 式中描述的监控模式上线,读取记录到的观察结果后再强制执行。
重写限制
对于无法就地改写请求文本的端点(例如音频、图像和透传路由),可 mask 的命中也会改为阻断。
如果 analyzer 找到可脱敏 PII,但 anonymizer 调用失败,会执行远程故障处理策略。当输入 Hook 的 fail_open: false 或输出 Hook 的 output_fail_open: false 时,AISIX 会阻止内容。将适用字段设为 true 会记录一次绕过,并继续处理未经匿名化的原始内容。
故障排查
| 现象 | 原因 | 处理方式 |
|---|---|---|
| 所有请求都被阻断,或所有请求都带着故障原因绕过安全护栏 | analyzer 对所有流量都返回错误,具体表现为阻断还是绕过取决于 fail_open。当 language 是镜像中不存在的语言时,每次调用都会返回 HTTP 500。 | 用安全护栏配置的 language 值直接调用 /analyze。如果返回 No matching recognizers were found to serve the request,请部署包含该语言的镜像。 |
容器启动时退出,并报告 Misconfigured engine, supported languages have to be consistent | NLP、analyzer 和识别器注册表三份配置列出的语言不一致。 | 让三份文件中的语言列表完全一致。 |
| 发布后最初的一批请求失败,随后恢复 | 流量在 analyzer 完成语言模型 加载之前就到达了。 | 按上文所述添加 startup probe 或健康检查的启动等待时间。 |
| 直接调用能检出,但经过网关不生效 | 安全护栏没有挂载、未启用,或尚未投射到网关。 | 确认挂载和启用状态,然后检查资源投射。 |
| 某个已知的值始终检不出来 | 该值是 Presidio 主动排除的公开占位标识符,或者其得分低于 score_threshold。 | 改用结构合法的值测试,并查看 /analyze 返回的原始置信度分数。 |
下一步
你已经部署 Presidio、将其接入 AISIX 并验证了匿名化和阻断。使用下面的指南调整行为或比较相关安全护栏:
- 安全护栏行为:调整执行模式、流式输出和远程故障处理方式。
- PII 检测与脱敏:当基于规则的匹配已经足够时,使用内置敏感数据检测。
- 选择安全护栏服务提供方:对比 Presidio 和其它内置、远程选项。