Presidio Guardrails
The Presidio guardrail detects and anonymizes sensitive data with Presidio, an open-source PII engine you run yourself. AISIX calls your Presidio analyzer for each request or response. Detected entities are either blocked or anonymized with an operator you choose, and anonymized traffic continues with the rewritten text.
Compared with the built-in PII guardrail, Presidio adds:
- NER/ML entities a regular expression cannot express, such as
PERSON,LOCATION,NRP, and the rest of Presidio's recognizer set. - Anonymization operators: replace with an entity placeholder, mask with asterisks, hash with SHA-256, or redact the span entirely.
- Analysis inside your own network, with no Presidio vendor key. Text sent for PII analysis stays within your Presidio deployment. The request sent to the configured model provider still leaves your infrastructure, after anonymization when masking occurs.
Presidio runs as two independent HTTP services, and AISIX addresses each one by base URL:
- The analyzer answers
POST /analyzewith the entity types found in a text, their offsets, and a confidence score. AISIX calls it once per text segment on every hook it is configured for. - The anonymizer answers
POST /anonymizewith the rewritten text. AISIX calls it only for segments that contain a detection whose effective action ismask, so a guardrail that only blocks never reaches it. Its URL is required in the configuration either way.
Presidio began as a Microsoft project and is now community-governed under the Data Privacy Stack organization. Release images are published to ghcr.io/data-privacy-stack/. The mcr.microsoft.com/presidio-* images still serve their existing tags but no longer receive new releases, so use the ghcr.io images for a new deployment.
In this guide, you will deploy both Presidio services, create a Presidio guardrail with per-entity actions, and verify anonymization and blocking.
Prerequisites
Before starting, prepare the following:
- Review Guardrail Behavior for hook points, enforcement modes, and remote failure handling.
- One of these configuration paths:
- AISIX Cloud with an environment, an attached gateway, and a write-scoped admin token. For On-Premises, follow the AISIX Cloud Quickstart. To request Hybrid Cloud access, contact API7.
- An open-source AISIX gateway that loads a declarative
resources.yamlfile.
- A working model alias and caller API key that can send Chat Completions requests.
- Docker, or a Kubernetes cluster, to run the Presidio analyzer and anonymizer.
curl. The AISIX Cloud path also usesjq.
Evaluate Presidio Locally
Both services listen on the port named by the PORT environment variable, which defaults to 3000, and both answer GET /health. Each one runs its Flask application under Gunicorn with WORKERS processes, and WORKERS defaults to 1.
Neither service authenticates its callers. Anything that can reach the analyzer can submit text to it, so deploy both services on a private network reachable only by your gateways. See Restrict Access to Presidio.
Run the Services with Docker
For a local evaluation, create a network and start the two services on it:
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
Connect the gateway container to aisix-guardrails, or provide equivalent network reachability in your deployment:
# Open-source quickstart container: aisix-quickstart
# On-Premises quickstart container: aisix-dp
docker network connect aisix-guardrails YOUR_AISIX_GATEWAY_CONTAINER
Confirm the analyzer responds from the host:
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"
}'
The response lists the detected EMAIL_ADDRESS entity with its offsets and score.
Create a Presidio Guardrail
The example below anonymizes emails and person names but blocks US Social Security Numbers on both hooks. Choose one configuration path, then use the shared verification procedure.
The configuration below assumes that you completed the local Docker evaluation and connected the gateway to the aisix-guardrails network. It therefore uses the presidio-analyzer and presidio-anonymizer container names. If you are deploying directly on Kubernetes, first complete Deploy on Kubernetes, then return to this section and use the cluster DNS URLs shown there.
Export the gateway values used by both paths:
# AISIX_PROXY has no trailing slash or endpoint path.
# The local quickstarts use 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
Export the control-plane connection details:
# AISIX_CP includes /api and has no trailing slash.
# The local On-Premises quickstart uses 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"
Create the guardrail and capture its 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 limits detection to the listed Presidio entities. An empty list analyzes with Presidio's full recognizer set.
❷ default_action: "mask" anonymizes listed entities unless an entry overrides the action. This example blocks US Social Security Numbers with action: "block".
❸ operator: "replace" substitutes masked values with entity placeholders such as <EMAIL_ADDRESS>. Use hash when downstream systems need a stable pseudonym instead of a placeholder.
❹ score_threshold drops analyzer results below the given confidence. Omit it to accept every result the analyzer returns. See Tune Detection before choosing a value.
A guardrail runs only where it is attached. Attach it to the whole environment so it applies to all traffic:
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"
}'
The env scope applies the guardrail to all traffic in the environment and takes no scope_id. Use model, mcp_server, api_key, team, or passthrough_route with a matching scope_id to narrow the attachment.
Enable the guardrail after its attachment exists:
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}'
The enabled configuration projects to attached gateways automatically.
Open-Source AISIX Gateway
Add the guardrail entry to guardrails and the attachment entry to guardrail_attachments. If either collection is absent, create it once. Preserve unrelated entries and collections:
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
❶ Presidio-specific fields sit directly on the guardrail entry rather than under config. Use analyzer and anonymizer URLs that are reachable from the gateway process.
❷ A guardrail applies only where an attachment puts it. Without a guardrail_attachments entry naming it, the guardrail loads but inspects no traffic.
Validate the complete file, then reload the gateway. See Reload a Resources File for the runnable Docker workflow.
Because this guardrail uses hook_point: both, AISIX applies the same anonymization to model responses before they reach the caller. With the example's fail-closed settings, original request values do not reach the upstream model, and original response values do not reach the caller. Setting fail_open or output_fail_open to true changes that guarantee when Presidio is unavailable. For streamed model responses, see Streaming Output.
Matched values are also kept out of gateway logs and usage records. Usage records contain only per-entity mask counts with entity names.
Verify Anonymization
AISIX Cloud projection is asynchronous. If the first request does not reflect the guardrail, wait for the gateway to apply the latest revision and retry. See Resource Projection for convergence checks.
Send a prompt containing an email address and a person name:
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
If the upstream model succeeds, the response starts with HTTP/1.1 200 OK. AISIX anonymizes the prompt before calling the upstream model, so the provider receives:
Send the invoice to <EMAIL_ADDRESS> and ask <PERSON> to confirm
Verify Blocking
A request containing a blocked entity is rejected:
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
The response starts with HTTP/1.1 422 Unprocessable Entity and includes the standard content-filter error:
{
"error": {
"message": "request blocked by content policy (guardrail 'presidio-pii-policy')",
"type": "content_filter"
}
}
The matched value is not echoed back.
Presidio deliberately rejects some placeholder identifiers published for use in examples, including the Social Security Number 123-45-6789. A test built on one of those values passes through unblocked and makes a working policy look broken. The value above comes from Presidio's own recognizer test suite and is detected by release 2.2.364, which the Compose and Kubernetes examples pin.
Tune Detection
Presidio scores each detection, and the score depends on the surrounding words as much as on the value itself. The same phone number scores 0.75 when the text reads phone 425-555-0143 and 0.4 when it reads call 425-555-0143, because the recognizer raises its confidence when a context word is nearby. A score_threshold of 0.5 therefore drops the second form silently.
Tune the guardrail with that in mind:
- Start with no
score_thresholdand inspect what the analyzer returns for representative traffic by calling/analyzedirectly. Add a threshold only to suppress a specific false positive. - Expect NER entities to over-match on short prompts.
PERSONin particular attaches to capitalized words that open a sentence. - Keep
entitiesexplicit. An empty list appliesdefault_actionto every entity Presidio recognizes, includingURLandDATE_TIME, which turns ordinary prompts into masked ones. - Roll out a new policy in monitor mode first, as described in Enforcement Modes, and read the recorded observations before enforcing it.
Deploy Presidio for Production
After verifying the local workflow, deploy Presidio with pinned images, health checks, and access controls that match your production environment.
Deploy with Docker Compose
For a persistent deployment on a single host, pin an image release rather than latest and give each service a health check. Publish no host port, so that only containers on the same network can reach the services:
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 sets the number of Gunicorn processes, which is also the number of requests the container can analyze at the same time. The default of 1 serializes every guardrail call. See Size the Analyzer.
❷ The analyzer loads its language model before it serves traffic, so give the health check a start period rather than a short retry budget.
Start the stack, then attach the gateway to the same presidio network. Compose registers each service name as a network alias, so the gateway can address both services by that name:
docker compose up -d
docker network connect presidio YOUR_AISIX_GATEWAY_CONTAINER
With this layout, the guardrail uses http://presidio-analyzer:3000 and http://presidio-anonymizer:3000 as its URLs.
Deploy on Kubernetes
The following manifest runs both services in a presidio namespace and restricts port 3000 to callers from explicitly labeled namespaces. The images run as an unprivileged user and need no writable paths outside /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
❶ The analyzer loads its language model at startup. A startup probe with a generous failure threshold keeps the liveness probe from restarting a slow first load, and keeps the pod out of the Service until it can answer. Give every probe a timeoutSeconds above the default of one second, because a loaded analyzer sometimes needs longer to answer /health.
❷ Both images already run as user 1001, so they tolerate a read-only root filesystem as long as /tmp is writable.
Label each namespace that runs an AISIX gateway allowed to call Presidio, then apply the manifest and wait for both deployments:
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
A gateway in an allowed namespace then uses http://presidio-analyzer.presidio.svc.cluster.local:3000 and http://presidio-anonymizer.presidio.svc.cluster.local:3000 as its URLs. The NetworkPolicy requires a cluster network plugin that enforces Kubernetes NetworkPolicy; without one, the namespace label does not restrict traffic.
If you came here before creating the guardrail, return to Create a Presidio Guardrail and use these cluster DNS URLs. The shared anonymization and blocking checks then verify the production deployment directly, so skip the following production verification subsection.
Verify the Production Deployment
The local checks above prove the evaluation containers only. After deploying the production topology, verify the complete gateway path again:
If you are replacing the local Docker evaluation on the same host, disconnect the gateway from aisix-guardrails before continuing:
docker network disconnect aisix-guardrails YOUR_AISIX_GATEWAY_CONTAINER
The evaluation and production networks advertise the same service names, so leaving the gateway attached to both makes name resolution ambiguous. You can remove the evaluation containers and network after disconnecting the gateway.
- Connect the gateway to the production Presidio network. For Docker Compose, this is the
presidionetwork. For Kubernetes, label the gateway namespace as shown above and confirm that the NetworkPolicy implementation admits it. - Update the guardrail to use URLs for the production topology. Docker Compose keeps
http://presidio-analyzer:3000andhttp://presidio-anonymizer:3000. Kubernetes useshttp://presidio-analyzer.presidio.svc.cluster.local:3000andhttp://presidio-anonymizer.presidio.svc.cluster.local:3000. - Wait for AISIX Cloud to project the update, or validate and reload the open-source resources file.
- Repeat Verify Anonymization and Verify Blocking. The
/healthchecks prove that each service started, but not that the gateway can reach the selected URLs or that the production configuration produces the intended policy result.
Size the Analyzer
The analyzer is the component that needs capacity planning. The anonymizer performs string substitution and stays small, typically under 100 MB in use.
Size the analyzer against these properties:
- Each worker loads its own copy of the language model. A single-worker analyzer settles at roughly 750 MB of resident memory, and each additional worker adds approximately the same amount. Set the memory limit from
WORKERS, not from a fixed figure. - A worker handles one request at a time. With the default
WORKERS: 1, guardrail calls queue behind each other, and per-request latency grows with concurrency even though the analysis itself is short. RaiseWORKERStoward the CPU allocation of the container, and add replicas beyond that. - AISIX analyzes each text segment separately and in sequence. A conversation with many messages produces one analyzer call per message on the input hook, and the model response produces another on the output hook. The guardrail
timeout_msapplies per call, so a long conversation multiplies the worst-case delay a stalled analyzer can add to one request. - Latency scales with text length, not just call count. Measure with prompts the size of your own traffic rather than with a short sample.
Because the guardrail runs inline, treat analyzer capacity as gateway capacity. Watch guardrail latency and the failure counters described in Guardrail Behavior. Keep fail_open set deliberately for both hooks: an undersized analyzer that starts timing out either blocks traffic or lets it through unscanned.
Restrict Access to Presidio
The Presidio API has no authentication, no authorization, and no rate limiting of its own. Treat the two services as internal components of the gateway, not as an API you expose:
- For local evaluation, bind host ports to loopback as shown above. In a production Docker deployment, publish no host port for either service and keep both on the gateway's private network.
- In Kubernetes, keep both Services as
ClusterIP. The manifest above admits only namespaces labeledaisix.ai/presidio-client=true; add apodSelectorto the policy as well when only selected pods in those namespaces should connect. - Terminate TLS at a proxy in front of the services if request text crosses a network boundary you do not control.
Presidio logs its own lifecycle events rather than the text it analyzes, so analyzed content does not reach its container logs. AISIX also keeps matched values out of gateway logs and usage records, which carry per-entity counts and entity names only.
Analyze a Language Other Than English
The published analyzer image bundles the English model en_core_web_lg and registers its recognizers for en only. A request for any other language fails with HTTP 500 and No matching recognizers were found to serve the request, which the guardrail treats as a remote failure rather than as a clean result.
Adding a language means building an image that carries the extra model and three consistent configuration files. The analyzer validates them against each other at startup and refuses to start when they disagree, reporting 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 maps each language to a model:
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 lists the same languages:
supported_languages:
- en
- es
default_score_threshold: 0
recognizers.yaml is the stock recognizer registry with the same language list. Copy it out of the image and edit its supported_languages block, because the file also enumerates every predefined recognizer:
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
Registering the language in all three files makes both the NLP engine and pattern recognizers available for that language.
Set the guardrail's language field to the added code once the image is deployed. One guardrail analyzes one language, so mixed-language traffic needs one guardrail per language, each attached to the traffic it covers. Repeat the production anonymization and blocking checks after changing the image or language.
Rewrite Limits
On endpoints that cannot rewrite request text in place, such as audio, images, and passthrough routes, a maskable detection blocks instead.
If the analyzer finds maskable PII but the anonymizer call fails, the remote failure policy applies. With fail_open: false on the input hook or output_fail_open: false on the output hook, AISIX blocks the content. Setting the applicable field to true records a bypass and continues with the original content without anonymization.
Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
| Every request is blocked, or every request bypasses the guardrail with a failure reason | The analyzer is returning an error for all traffic, and fail_open decides which of the two happens. A language the deployed image does not carry produces HTTP 500 on every call. | Call /analyze directly with the guardrail's language value. If it returns No matching recognizers were found to serve the request, deploy an image built for that language. |
The container exits during startup with Misconfigured engine, supported languages have to be consistent | The NLP, analyzer, and recognizer registry configurations list different languages. | Make the language list identical in all three files. |
| The first requests after a rollout fail, then recover | Traffic reached the analyzer before it finished loading its language model. | Add a startup probe, or a health check start period, as shown above. |
| Detection works when called directly but not through the gateway | The guardrail is not attached, is disabled, or has not yet projected to the gateway. | Confirm the attachment and enabled state, then check Resource Projection. |
| A known value is never detected | The value is a published placeholder that Presidio rejects, or its score falls below score_threshold. | Test with a structurally valid value and inspect the raw /analyze score. |
Next Steps
You have now deployed Presidio, connected it to AISIX, and verified anonymization and blocking. Use these guides to tune behavior or compare related guardrails:
- Guardrail Behavior: tune enforcement mode, streaming output, and remote failure handling.
- PII Detection and Redaction: use built-in sensitive-data detection when rule-based matching is enough.
- Choosing a Guardrail Provider: compare Presidio with other built-in and remote options.