Skip to main content

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:

  1. Block disallowed content (PII, secrets, policy-violating terms) in either direction.
  2. 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 reason field 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

FeaturePresidio EngineRegex Engine
Additional DeploymentRequiredNone
LatencyHigher (HTTP calls)Minimal (in-process)
DeterminismNon-deterministic (ML-based)100% deterministic
Detection methodInput tokenization, named entity recognitionPattern matching
entities fieldPresidio entity namesRegex patterns
Setup complexityHigher (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:

  1. Identify Relevant JSON Fields: You specify which parts of the JSON body to analyze using jsonQueries (for custom format) or the middleware auto-detects fields based on clientRequestFormat.

  2. 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).
  3. Block or Mask:

    • Block: If a rule has block: true and the engine finds a match, the middleware returns a deny response (default: 403 Forbidden). You can customize this with onDenyResponse.
    • Mask: If a rule specifies a mask, the matched portions are replaced with a chosen character pattern.
  4. Observability: When blocking occurs, the optional reason field 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:

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

Regex Engine Examples

The Regex engine uses regular expression patterns instead of named entities. No external service is required.

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,}'
Case-Insensitive Matching

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.

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."
Streaming

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.

KeyDescriptionRequiredDefault
engine.presidioPresidio engine configuration object.No (one engine required)
engine.presidio.hostThe base URL of your Presidio analyzer instance.Yes (if using Presidio)
engine.presidio.languageLanguage code used by Presidio for detection (e.g., en).Yes (if using Presidio)
engine.presidio.entitiesList of Presidio entity types to detect globally.NoAll entities
engine.regexRegex 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:

clientRequestFormatBest forStream supportNotes
custom (default)Generic JSON / REST payloadsNoYou choose the JSON paths via jsonQueries.
ccrOpenAI Chat CompletionsYesAuto-detects chat schema; jsonQueries disallowed.
responsesAPIOpenAI Responses APIYesAuto-detects Responses API schema; jsonQueries disallowed.
Deprecation Notice

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.

KeyDescriptionRequiredDefault
request.rulesArray of rule objects for incoming traffic.No
response.rulesArray of rule objects for outgoing traffic.No
rules[].reasonIdentifier for observability. Added to OpenTelemetry spans when blocking occurs.Norule.0, rule.1, etc.
rules[].jsonQueriesList of gojq-style JSON paths to scan (e.g., ".messages[].content"). If omitted, scans entire body.No
rules[].blockIf true, any match triggers a deny response. Mutually exclusive with mask.Nofalse
rules[].maskMasking configuration object. Mutually exclusive with block: true.No
rules[].mask.charCharacter used to replace matched text.No*
rules[].mask.unmaskFromLeftNumber of characters to leave unmasked at the start.No0
rules[].mask.unmaskFromRightNumber of characters to leave unmasked at the end.No0
rules[].entitiesList 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.

KeyDescriptionRequiredDefault
request.onDenyResponseCustom deny response for blocked requests.No
request.onDenyResponse.statusCodeHTTP status code (100-599).No403
request.onDenyResponse.messageResponse body message.NoHTTP status text
request.onDenyResponse.contentTypeContent-Type header.NoAuto-detected
response.onDenyResponseCustom deny response for blocked responses.No
response.onDenyResponse.statusCodeHTTP status code (100-599).No403
response.onDenyResponse.messageResponse body message.NoHTTP status text
response.onDenyResponse.contentTypeContent-Type header.NoAuto-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.

note

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:

clientRequestFormatNon-streaming responseStreaming response
customRaw message textRaw message text
ccrChat Completion JSON with message as assistant contentSSE chunk (data: {...})
responsesAPIResponses API JSON with message as a refusal content itemSSE 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:

Engineentities ContainsExample
PresidioNamed entity typesEMAIL_ADDRESS, PHONE_NUMBER, US_SSN, PERSON
RegexRegular 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:

  1. Blocking rules return early: Once a blocking rule matches, no further rules are processed and a deny response is returned.

  2. Masking rules for the same JSON path: When multiple masking rules target the same jsonQueries path, 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.

  3. Combine patterns in a single rule: To apply multiple patterns to the same field, list them in the same rule's entities array 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}']
  1. Different JSON paths work independently: Rules targeting different jsonQueries paths do not interfere with each other.

Troubleshooting

General Issues

403 Forbidden Without Clear Reason

When requests are blocked unexpectedly:

  • Check the reason field in your rules to identify which rule triggered the block.
  • Enable debug logging to see Request blocked or Response blocked messages with the reason value.
  • Review OpenTelemetry spans for the reason attribute.
  • Verify your jsonQueries paths 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.presidio and engine.regex configured. Remove one.
  • "host is required": When using the Presidio engine, you must specify engine.presidio.host.
  • Verify YAML indentation is correct under the engine key.
Masking Not Applied Correctly

If masking isn't working as expected:

  • Ensure block: true is not set on the same rule (blocking takes precedence).
  • Check that unmaskFromLeft and unmaskFromRight values don't exceed the matched text length.
  • Verify the char field 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.host includes 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, not email). See Presidio Supported Entities.
  • Check language setting: The engine.presidio.language must match the content language (e.g., en for 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+".