Content Guard
The Content Guard middleware detects and processes sensitive or restricted content in requests and responses using configurable rules. It supports two detection engines:
- Presidio Engine: Uses Microsoft Presidio for named entity recognition (PII detection like emails, phone numbers, SSNs).
- Regex Engine: Uses regular expression patterns for deterministic, low-latency pattern matching without external dependencies.
While the LLM Guard middleware offers maximum flexibility by integrating with any external content analysis service or LLM, Content Guard specializes in PII detection, data masking, and compliance-focused content filtering.
In the AI Gateway, it lets you:
- Block disallowed content (PII, secrets, policy-violating terms) in either direction.
- Mask sensitive fragments, keeping UX intact while remaining compliant.
Key Features and Benefits
- Prevent data leakage before prompts reach an LLM—or before completions reach users.
- Mask or block based on business policy.
- Two detection engines: Presidio for named entity recognition, or Regex for deterministic pattern matching.
- No external service required with the Regex engine—minimal latency and fully deterministic.
- Observability: Optional
reasonfield for tracking block reasons in OpenTelemetry spans.
Detection Engines
Content Guard supports mutually exclusive detection engines. You must configure exactly one engine per middleware instance.
Presidio Engine
The Presidio engine uses Microsoft Presidio for named entity recognition. It excels at detecting PII like names, addresses, and identification numbers using NLP-based detection.
Best for:
- Detecting named entities (PERSON, LOCATION, ORGANIZATION and more)
- PII detection with contextual awareness
- Compliance scenarios requiring proven PII detection
Trade-offs:
- Requires a Presidio service deployment besides Hub
- Higher latency due to HTTP calls
- Non-deterministic (ML-based detection may vary)
Regex Engine
The Regex engine uses regular expression patterns for content detection. It runs entirely within the gateway with no external dependencies.
Best for:
- Deterministic pattern matching (credit cards, API keys, custom formats)
- Low-latency requirements
- Layered security combined with other guards
- Environments where deploying a third-party engine is not feasible
Trade-offs:
- Requires you to define patterns manually
- No contextual awareness (pure pattern matching)
- Complex patterns may be difficult to maintain
Engine Comparison
| Feature | Presidio Engine | Regex Engine |
|---|---|---|
| Additional Deployment | Required | None |
| Latency | Higher (HTTP calls) | Minimal (in-process) |
| Determinism | Non-deterministic (ML-based) | 100% deterministic |
| Detection method | Input tokenization, named entity recognition | Pattern matching |
entities field | Presidio entity names | Regex patterns |
| Setup complexity | Higher (deploy Presidio) | Lower (no dependencies) |
Requirements
-
AI Gateway must be enabled:
helm upgrade traefik traefik/traefik -n traefik --wait \--reset-then-reuse-values \--set hub.aigateway.enabled=true
For the Presidio engine only:
- A Presidio service is required. Follow the Presidio documentation to install and configure Presidio as a service with Kubernetes.
For the Regex engine:
- No additional requirements. The engine runs entirely within the gateway.
How It Works
When the Content Guard middleware intercepts an HTTP request or response:
-
Identify Relevant JSON Fields: You specify which parts of the JSON body to analyze using
jsonQueries(forcustomformat) or the middleware auto-detects fields based onclientRequestFormat. -
Analyze Content: The configured engine checks whether the targeted text contains any specified entities:
- Presidio engine: Uses named entity types (e.g.,
PERSON,EMAIL_ADDRESS,LOCATION). For a complete list, see Presidio Supported Entities. - Regex engine: Uses regular expression patterns you define (e.g.,
\d{4}-\d{4}-\d{4}-\d{4}for credit cards).
- Presidio engine: Uses named entity types (e.g.,
-
Block or Mask:
- Block: If a rule has
block: trueand the engine finds a match, the middleware returns a deny response (default:403 Forbidden). You can customize this withonDenyResponse. - Mask: If a rule specifies a
mask, the matched portions are replaced with a chosen character pattern.
- Block: If a rule has
-
Observability: When blocking occurs, the optional
reasonfield is added to OpenTelemetry spans for tracking.
Configuration Examples
Presidio Engine Examples
Below are examples demonstrating how to block and mask content using the Presidio engine:
- Presidio (API Gateway)
- Presidio (Chat Variant)
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: content-guard-presidio
spec:
plugin:
content-guard:
engine:
presidio:
host: http://presidio
language: en
request:
rules:
# Block if the payload leaks an e-mail address.
- jsonQueries:
- ".customer.email"
reason: email_in_request
block: true
entities:
- EMAIL_ADDRESS
# Mask phone numbers but let the request continue.
- jsonQueries:
- ".customer.phone"
mask:
char: "*"
unmaskFromLeft: 2
unmaskFromRight: 2
entities:
- PHONE_NUMBER
response:
rules:
# Block any response that still contains PII.
- jsonQueries:
- ".data[].ssn"
reason: ssn_in_response
block: true
entities:
- US_SSN
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: content-guard-ccr-presidio
spec:
plugin:
content-guard:
clientRequestFormat: ccr
engine:
presidio:
host: http://presidio
language: en
response:
rules:
- mask:
char: "#"
unmaskFromRight: 3
entities:
- EMAIL_ADDRESS
Regex Engine Examples
The Regex engine uses regular expression patterns instead of named entities. No external service is required.
- Regex (API Gateway)
- Regex (Chat Variant)
- Regex (Raw Content)
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: content-guard-regex
spec:
plugin:
content-guard:
engine:
regex: {}
request:
rules:
# Block credit card numbers.
- jsonQueries:
- ".message"
- ".data.payment_info"
reason: credit_card_detected
block: true
entities:
- '\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}'
# Block API keys or secrets.
- jsonQueries:
- ".content"
reason: api_key_detected
block: true
entities:
- 'sk-[a-zA-Z0-9]{32,}'
- 'AKIA[0-9A-Z]{16}'
# Mask SSN patterns with partial reveal.
- jsonQueries:
- ".user.ssn"
mask:
char: "*"
unmaskFromRight: 4
entities:
- '\d{3}-\d{2}-\d{4}'
response:
rules:
# Mask email addresses in responses.
- jsonQueries:
- ".result"
mask:
char: "#"
entities:
- '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: content-guard-ccr-regex
spec:
plugin:
content-guard:
clientRequestFormat: ccr
engine:
regex: {}
request:
rules:
# Block prompt injection attempts (case-insensitive).
- reason: prompt_injection
block: true
entities:
- '(?i)ignore\s+(previous|above|all)\s+instructions'
- '(?i)bypass.*(login|password|auth|security)'
# Block requests containing potential secrets.
- reason: secret_detected
block: true
entities:
- 'sk-[a-zA-Z0-9]{32,}'
- 'ghp_[a-zA-Z0-9]{36}'
response:
rules:
# Mask phone numbers in AI responses.
- mask:
char: "X"
unmaskFromRight: 4
entities:
- '\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}'
# Mask credit card numbers with partial reveal.
- mask:
char: "*"
unmaskFromLeft: 4
unmaskFromRight: 4
entities:
- '\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}'
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: content-guard-regex-raw
spec:
plugin:
content-guard:
engine:
regex: {}
request:
rules:
# Block SSN in raw (non-JSON) request bodies.
# When jsonQueries is omitted, the entire body is scanned.
- reason: ssn_detected
block: true
entities:
- '\d{3}-\d{2}-\d{4}'
# Mask IPv4 addresses in raw content.
- mask:
char: "X"
entities:
- '\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b'
Use the (?i) prefix at the start of your regex pattern to enable case-insensitive matching.
For example, (?i)password matches "password", "PASSWORD", and "Password".
Client Request Format Examples
These examples demonstrate using clientRequestFormat with onDenyResponse for format-aware deny responses.
- Chat Completions (CCR)
- Responses API
- Custom (Raw)
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: content-guard-ccr
namespace: apps
spec:
plugin:
content-guard:
clientRequestFormat: ccr
engine:
regex: {}
request:
rules:
- block: true
entities:
- '\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}'
reason: credit-card
onDenyResponse:
statusCode: 200
message: "The request has been blocked due to policy violation."
response:
rules:
- block: true
entities:
- '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
reason: email
onDenyResponse:
statusCode: 200
message: "The response has been blocked due to policy violation."
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: content-guard-responses-api
namespace: apps
spec:
plugin:
content-guard:
clientRequestFormat: responsesAPI
engine:
presidio:
host: http://presidio
language: en
request:
rules:
- block: true
entities:
- EMAIL_ADDRESS
reason: email-in-request
onDenyResponse:
statusCode: 200
message: "PII detected in your request."
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: content-guard-custom
namespace: apps
spec:
plugin:
content-guard:
clientRequestFormat: custom
engine:
regex: {}
request:
rules:
- block: true
jsonQueries:
- ".payload.note"
entities:
- '\d{3}-\d{2}-\d{4}'
reason: ssn-detected
# With clientRequestFormat: custom, the deny response body is the raw
# message text. Set `contentType` when the default (application/json)
# doesn't match what your client expects.
onDenyResponse:
statusCode: 422
message: "Request blocked: sensitive identifier detected."
contentType: text/plain
When you define a response.rules block, the middleware must inspect the entire completion.
It buffers every SSE chunk, applies masking/block rules, then sends one aggregated response—format preserved, but real-time streaming is lost.
If you need live token updates, configure the middleware only on the request (omit response.rules) or apply it to non-stream endpoints.
This design ensures the client that requested a stream gets a stream and not an unexpected response format that could break their application.
Configuration Options
Engine Configuration
You must configure exactly one engine. The engine.presidio and engine.regex options are mutually exclusive.
| Key | Description | Required | Default |
|---|---|---|---|
engine.presidio | Presidio engine configuration object. | No (one engine required) | |
engine.presidio.host | The base URL of your Presidio analyzer instance. | Yes (if using Presidio) | |
engine.presidio.language | Language code used by Presidio for detection (e.g., en). | Yes (if using Presidio) | |
engine.presidio.entities | List of Presidio entity types to detect globally. | No | All entities |
engine.regex | Regex engine configuration object. Use regex: {} to enable. | No (one engine required) |
Client Request Format
The clientRequestFormat field controls how the middleware reads client payloads and formats deny responses:
clientRequestFormat | Best for | Stream support | Notes |
|---|---|---|---|
custom (default) | Generic JSON / REST payloads | No | You choose the JSON paths via jsonQueries. |
ccr | OpenAI Chat Completions | Yes | Auto-detects chat schema; jsonQueries disallowed. |
responsesAPI | OpenAI Responses API | Yes | Auto-detects Responses API schema; jsonQueries disallowed. |
The chat-completion-content-guard plugin name is deprecated and will be removed in a future release.
Use the content-guard plugin with clientRequestFormat: ccr instead.
Existing configurations using chat-completion-content-guard continue to work but should be migrated.
Rule Configuration
Rules are configured separately for requests and responses. Each rule can block or mask content based on entity matches.
| Key | Description | Required | Default |
|---|---|---|---|
request.rules | Array of rule objects for incoming traffic. | No | |
response.rules | Array of rule objects for outgoing traffic. | No | |
rules[].reason | Identifier for observability. Added to OpenTelemetry spans when blocking occurs. | No | rule.0, rule.1, etc. |
rules[].jsonQueries | List of gojq-style JSON paths to scan (e.g., ".messages[].content"). If omitted, scans entire body. | No | |
rules[].block | If true, any match triggers a deny response. Mutually exclusive with mask. | No | false |
rules[].mask | Masking configuration object. Mutually exclusive with block: true. | No | |
rules[].mask.char | Character used to replace matched text. | No | * |
rules[].mask.unmaskFromLeft | Number of characters to leave unmasked at the start. | No | 0 |
rules[].mask.unmaskFromRight | Number of characters to leave unmasked at the end. | No | 0 |
rules[].entities | List of entities to detect. Presidio: entity names (e.g., EMAIL_ADDRESS). Regex: regex patterns. | No |
Deny Response Configuration
By default, when a blocking rule matches, the middleware returns 403 Forbidden with a plain text body. You can customize this behavior using onDenyResponse on the request and/or response configuration blocks.
| Key | Description | Required | Default |
|---|---|---|---|
request.onDenyResponse | Custom deny response for blocked requests. | No | |
request.onDenyResponse.statusCode | HTTP status code (100-599). | No | 403 |
request.onDenyResponse.message | Response body message. | No | HTTP status text |
request.onDenyResponse.contentType | Content-Type header. | No | Auto-detected |
response.onDenyResponse | Custom deny response for blocked responses. | No | |
response.onDenyResponse.statusCode | HTTP status code (100-599). | No | 403 |
response.onDenyResponse.message | Response body message. | No | HTTP status text |
response.onDenyResponse.contentType | Content-Type header. | No | Auto-detected |
statusCode must be between 100 and 599. The message field is plain text only — Go template syntax is not supported (unlike LLM Guard, which supports Go templates in deny messages).
The contentType field is most useful with clientRequestFormat: custom, where the deny body is the raw message text and the default application/json header may not match.
With ccr and responsesAPI, the body is auto-wrapped in JSON (or SSE while streaming) and the auto-detected Content-Type usually matches the client's expectations — set contentType only if you need to override it.
jsonQueries is required only when clientRequestFormat is custom. When using ccr or responsesAPI, jsonQueries is disallowed because the middleware auto-detects the relevant fields.
Format-Aware Deny Responses
When clientRequestFormat is set, the deny response body is automatically formatted to match the client's expected format:
clientRequestFormat | Non-streaming response | Streaming response |
|---|---|---|
custom | Raw message text | Raw message text |
ccr | Chat Completion JSON with message as assistant content | SSE chunk (data: {...}) |
responsesAPI | Responses API JSON with message as a refusal content item | SSE events (response.completed) |
If onDenyResponse is omitted entirely, the behavior is unchanged: 403 Forbidden with plain text body.
Entity Configuration by Engine
The entities field behaves differently depending on the engine:
| Engine | entities Contains | Example |
|---|---|---|
| Presidio | Named entity types | EMAIL_ADDRESS, PHONE_NUMBER, US_SSN, PERSON |
| Regex | Regular expression patterns | \d{4}-\d{4}-\d{4}-\d{4}, (?i)password |
For a complete list of Presidio entities, see Presidio Supported Entities.
Custom Entities (Presidio Only)
You can define additional entities for Presidio to detect (such as specialized IDs or formats). These are typically added in Presidio's configuration itself, or via its "custom analyzer" endpoints. Once added, you can reference them in the entities array like built-in types. For more details, please see the Presidio Custom Analyzer documentation.
Request vs. Response Rules
- Request Rules: Block or mask disallowed content before it reaches the backend or AI. For example, block requests containing credit card numbers or mask SSNs before they reach the LLM.
- Response Rules: Block or mask sensitive data in responses before they reach the client. For example, mask phone numbers or email addresses that the AI might include in its response.
Rule Processing Order
Rules are processed sequentially in the order they are defined. Understanding this order is important:
-
Blocking rules return early: Once a blocking rule matches, no further rules are processed and a deny response is returned.
-
Masking rules for the same JSON path: When multiple masking rules target the same
jsonQueriespath, the last matching rule wins. Each rule operates on the original field value, not on the output of previous rules. The final masked value overwrites any previous masking. -
Combine patterns in a single rule: To apply multiple patterns to the same field, list them in the same rule's
entitiesarray rather than creating separate rules.
response:
rules:
# CORRECT: Multiple patterns in one rule - all patterns are applied
- jsonQueries:
- ".message"
mask:
char: "*"
unmaskFromRight: 4
entities:
- '\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}' # Credit cards
- '\d{3}[-.\s]?\d{3}[-.\s]?\d{4}' # Phone numbers
# INCORRECT: Separate rules for the same field - only last rule's masking is kept
# - jsonQueries: [".message"]
# mask: { char: "*" }
# entities: ['\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}']
# - jsonQueries: [".message"] # This overwrites the same field's credit card masking!
# mask: { char: "X" }
# entities: ['\d{3}[-.\s]?\d{3}[-.\s]?\d{4}']
- Different JSON paths work independently: Rules targeting different
jsonQueriespaths do not interfere with each other.
Troubleshooting
General Issues
403 Forbidden Without Clear Reason
When requests are blocked unexpectedly:
- Check the
reasonfield in your rules to identify which rule triggered the block. - Enable debug logging to see
Request blockedorResponse blockedmessages with thereasonvalue. - Review OpenTelemetry spans for the
reasonattribute. - Verify your
jsonQueriespaths are correctly targeting the intended fields.
Engine Configuration Error
If you see an error about engine configuration:
- "only one engine is allowed": You have both
engine.presidioandengine.regexconfigured. Remove one. - "host is required": When using the Presidio engine, you must specify
engine.presidio.host. - Verify YAML indentation is correct under the
enginekey.
Masking Not Applied Correctly
If masking isn't working as expected:
- Ensure
block: trueis not set on the same rule (blocking takes precedence). - Check that
unmaskFromLeftandunmaskFromRightvalues don't exceed the matched text length. - Verify the
charfield contains exactly one character.
Presidio Engine Issues
Presidio Service Not Reachable
If you receive HTTP 500 errors when using the Presidio engine:
- Verify the service is running:
kubectl get pods -n <namespace>to check Presidio pod status. - Check the host URL: Ensure
engine.presidio.hostincludes the correct protocol, hostname, and port. - Test connectivity: Use a debug pod to verify network access to the Presidio service.
- Review Presidio logs: Check the Presidio analyzer container logs for errors.
Presidio Entity Not Detected
If Presidio isn't detecting expected entities:
- Verify entity name: Ensure you're using the correct Presidio entity name (e.g.,
EMAIL_ADDRESS, notemail). See Presidio Supported Entities. - Check language setting: The
engine.presidio.languagemust match the content language (e.g.,enfor English). - Test with Presidio directly: Send a request directly to your Presidio service to verify detection works outside of Traefik.
- Custom entities: If using custom entities, ensure they're properly configured in your Presidio deployment.
Regex Engine Issues
Regex Pattern Not Matching
If your regex pattern isn't detecting content as expected:
- Test your regex: Use a tool like regex101.com with the "Go" flavor to validate your pattern.
- Escape special characters: In YAML, backslashes need proper escaping. Use single quotes for regex patterns:
'\d{3}-\d{2}-\d{4}'. - Case sensitivity: By default, patterns are case-sensitive. Use
(?i)prefix for case-insensitive matching. - Greedy vs. non-greedy: By default, quantifiers like
*and+are greedy (match as much as possible). Add?to make them non-greedy:.*?instead of.*. For example,<.*>matches the entire string<tag>content</tag>, while<.*?>matches only<tag>. - Anchors: Patterns match anywhere in the text. Use
^and$anchors if you need to match the entire string.
Invalid Regex Pattern Error
If your middleware fails to start with a regex compilation error:
- Check syntax: Go regex uses RE2 syntax, which doesn't support lookaheads (
(?=...)) or backreferences (\1). - Escape literal characters: To match special regex characters literally, escape them with a backslash (e.g.,
\.for a dot,\*for an asterisk). - YAML quoting: Use single quotes to avoid YAML interpreting backslashes:
'\d+'not"\d+".
Related Content
- Read the LLM Guard documentation for LLM-based content analysis.
- Read the Chat Completion documentation.
- Read the Semantic Cache documentation.
