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)
PERSON detection and scoreThreshold

Available starting v3.21.0-ea.1. Not present in v3.20.x releases.

Use engine.presidio.scoreThreshold to ignore low-confidence detections instead of acting on every match. This value does not behave the same way for every entity: the default Presidio recognizer for PERSON scores all name matches at a flat 0.85, so scoreThreshold works as an on/off switch for that entity, not a sensitivity range. A threshold at or below 0.85 still blocks every detected name; a threshold above 0.85 disables the PERSON rule entirely.

For clients where a false positive breaks the interaction, mask PERSON matches instead of blocking them.

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.
Automatic format detection

When a Chat Completion, Responses API, Messages API, Bedrock Mantle, Google Agent Platform, or MCP middleware precedes Content Guard in the chain, it marks the client's request format in the request context. Content Guard then extracts and masks text from relevant fields in that format automatically, and no clientRequestFormat value is required.

  1. 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).
  2. 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.
  3. 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
scoreThreshold: 0.85 # Ignore detections Presidio scores below 85% confidence. Available from v3.21.0-ea.1.

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,}'
About Catch-All Patterns

Avoid adding broad length-based patterns (such as "any string of 20 or more alphanumeric characters") to credential rules. They produce false positives on normal LLM traffic such as base64-encoded images and long UUIDs. To detect unknown or proprietary secret formats, pair this configuration with the Presidio engine using a custom recognizer.

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

Use Word Boundaries to Avoid Partial Matches

A pattern like (credit|card) matches card inside any word containing it, such as "wildcard" or "cardboard", not only standalone occurrences. Wrap alternatives in \b word boundaries to match whole words only: \b(credit|card)\b. This matters most for rules scanning free-form text such as system prompts or user messages, where an unintended substring match can block unrelated content.

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 and applies masking/block rules, then sends one aggregated response. Format preservation depends on clientRequestFormat: custom and messagesAPI send the aggregated response in the original format. With ccr the aggregated response is currently a single event that OpenAI-compatible clients cannot parse, and with responsesAPI the incremental delta events are dropped, so a client that builds its output from them receives nothing. Neither is intended behavior. Until both are fixed, use request-only rules on routes that serve streaming clients in those formats.

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.presidio.scoreThresholdMinimum confidence score (0-1) a Presidio detection must reach before the middleware acts on it. Available from v3.21.0-ea.1.No0 (act on every detection)
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 forDetects stream: trueNotes
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.
messagesAPIAnthropic Messages APIYesAuto-detects Messages API schema; jsonQueries disallowed.
mcpMCP tool calls (JSON-RPC)Yes (SSE)Auto-detects tool arguments and results. Usually left unset and set for you by the MCP middleware, in which case a rule's own jsonQueries take precedence over the auto-detected paths. Setting mcp explicitly disallows jsonQueries. See Guarding MCP Traffic.

With any format, rules that apply only to the request stream through unchanged. A response.rules block makes the middleware buffer the response regardless of format, since it has to inspect the complete completion before it can apply a rule. See the streaming warning above.

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's Go template support 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, responsesAPI, messagesAPI, and mcp, 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 when clientRequestFormat is custom. Setting it explicitly to ccr, responsesAPI, messagesAPI or mcp disallows jsonQueries, because those formats define their own extraction paths. When clientRequestFormat is left unset and the format is detected from the request context, for example on a route behind the MCP middleware, a rule's own jsonQueries take precedence. It replaces the detected format's paths for that rule.

Format-Aware Deny Responses​

You can set clientRequestFormat explicitly, or leave it unset and let the middleware detect it automatically from the request context. Either way, once a format is active, 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 status: "failed" and message in a structured error objectSSE events (response.failed) with the same status/error shape
messagesAPIMessages API JSON with message as assistant text contentSSE events (message_delta with stop_reason: refusal)
mcpJSON-RPC error echoing the request idJSON-RPC error as an SSE data: event

For a streaming client, the deny response's HTTP status code matters as much as its body shape. Most streaming clients treat any non-2xx status as a transport error. They retry the connection instead of reading the body. At a non-2xx status, this means the structured deny message above doesn't reach the user. The client only sees repeated connection failures. Set onDenyResponse.statusCode to 200 so the client reads the deny body as a normal, refused turn.

If onDenyResponse is omitted entirely, the behavior is unchanged: 403 Forbidden with plain text body.

Content Guard's own default onDenyResponse.statusCode is 403. If message is left unset when you override the status code, the body falls back to plain HTTP status text, for example OK at statusCode: 200, carrying no deny information at all. Set message alongside any custom statusCode to avoid this. Denials stay countable on traefik_hub_content_guard_requests_total{reason=...} no matter which status code you use.

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.

Rules don't read every item type. For clientRequestFormat: responsesAPI, a response rule covers the following:

Item typeFieldRead?
Assistant messageoutput_textYes
function_callargumentsYes
custom_tool_callinputNo
reasoningcontent[].textYes
reasoningsummary[].textNo

A request rule doesn't read a custom_tool_call's input either: it covers .input, .input[].content, .input[].content[].text, and .input[].arguments, none of which reach that field.

This matters most for coding agents. A rule guarding a route used by Codex or a similar agent won't see the content of any shell command it runs, since those arrive as custom_tool_call. It also won't see the agent's reasoning, which arrives as encrypted_content only.

Prefer block over mask on tool payloads

Use block instead of mask on any route carrying tool calls. A mask rule rewrites a matched tool argument in place, so on a function_call the agent executes an argument the model never wrote, with matched characters replaced but the JSON still valid.

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.

Metrics Early Access​

Early Access

This feature is currently in early access.

The Content Guard middleware emits traefik_hub_content_guard_requests_total, a counter of requests it processed.

MetricTypeDescription
traefik_hub_content_guard_requests_totalCounterRequests processed by the Content Guard middleware

Labels​

LabelDescriptionExample
methodHTTP method of the requestPOST
codeHTTP status code returned to the client200, 403
reasonBlock condition reason that blocked the request. Present only when the middleware blocks the requestssn

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.
PERSON Rule Blocks Every Request

If a PERSON rule with block: true blocks all traffic, even requests without real names:

  • Check scoreThreshold (available from v3.21.0-ea.1): the default Presidio recognizer scores every PERSON detection at a flat 0.85. Setting scoreThreshold above 0.85 disables the rule; any value at or below 0.85 still blocks on every detected name.
  • Use mask instead of block: masking redacts the name and lets the request continue, which avoids failing the whole interaction on a single detection.
  • Review what triggers the match: system prompts and boilerplate text from some clients can contain name-like patterns that Presidio detects as PERSON.

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