Enable the Injection Classifier
Deploy an ML classifier that scores LLM prompts, most LLM responses, and MCP tool arguments/responses for prompt-injection/jailbreak intent on a continuous scale, alongside the pattern-based detectors. It's off by default. This does not extend to the separate OpenClaw (K8s ops chat-agent) integration: its response hooks publish findings and sensor status from their configured scanner to CEL response policies, but that scanner deliberately excludes PromptGuard, so OpenClaw responses do not carry classifier findings or scores.
The shipped classifier is measured at roughly a 23.4% false-positive rate on real traffic. Run it audit-only and review individual finding scores against your own tenant's traffic before writing any policy that warns or denies on it — see Content safety for the full calibration workflow.
Enable the classifier
Coverage has known gaps on both sides of the cloud proxy (Bedrock, Claude-on-Vertex). On requests, only Anthropic- and Gemini-shaped prompts are extracted for scoring — other Bedrock model families it fronts (Amazon Nova/Titan, Meta Llama, Mistral, Cohere) are forwarded without a prompt ever reaching the classifier, and so is Bedrock's Converse API (
/model/{id}/converse,.../converse-stream) for any model, Claude included: Converse's request/response shapes differ from Anthropic Messages, so the extraction that feeds the classifier only runs forInvokeModel/InvokeModelWithResponseStream. On responses, cloud-proxy streaming responses bypass classifier scoring entirely, and a handful of non-Claude Bedrock model response shapes aren't parsed for scanning at all.Set
llmProxy.bufferForDLP: truebefore enabling this on production streaming traffic. With the defaultllmProxy.bufferForDLP: false, a non-cloud-proxy streaming response is scanned once per chunk rather than once for the whole reply — each chunk is a separate sidecar HTTP/inference call (multiplying load and consumingPROMPTGUARD_MAX_INFLIGHTfar faster than a "once per response" budget assumes), and an attack split across a chunk boundary can still be missed entirely. Buffering trades that risk for added latency and memory (the full response is assembled before scanning); weigh that trade-off for your traffic before turning the classifier on for streaming responses.Buffering doesn't remove the chunk-boundary risk entirely, only shrinks it:
llmProxy.bufferMaxBytes(default 512 KB) caps how much of a response gets assembled for the full-response scan, falling back to per-chunk scanning for whatever comes after the cap is reached — the same coveragebufferForDLP: truewas meant to avoid, just for the tail end of large responses instead of the whole thing, not the exact byte where the cap is crossed. RaisebufferMaxBytesif your traffic regularly produces replies near or over the default.Set
promptguard.enabled: truein your Helm values, and pin the image to a specific published tag rather than the mutablelatestdefault:promptguard:enabled: truereplicaCount: 2 # fail-open by design — a single replica's restart is a detection gap, not an outageimage:tag: sha-<commit-sha> # replace <commit-sha> with a real published tag — check ghcr.io/nirmata/aicontrols-promptguard's tags; pin one for production, since the default ("latest") is mutablesha-<commit-sha>is stable for ordinary use, but a same-commit CI re-run can still overwrite it (the base image and dependencies it builds from aren't pinned to digests). For byte-for-byte reproducibility, settagto the digest printed in that image's GitHub Actions run summary instead —sha256:<digest>— which the chart also accepts.If you already have
serviceMonitor.enabled: true(common — it's the same switch that wires up the rest of the chart's Prometheus metrics), also setpromptguard.networkPolicy.allowIngressto your Prometheus deployment's pod/namespace selector, or Prometheus can't scrape the sidecar's own/metricsendpoint. The default NetworkPolicy this chart generates for the sidecar (promptguard.networkPolicy.enabled: true, also the default) only admits the main proxy pods, so without an explicit allowance the sidecar's ownpromptguard_scan_*request/error/rejection counters go unscraped — Helm prints a NOTES warning about this after install, but installs the chart either way. ThePromptguardFailingOpenalert itself isn't affected by this gap: it readsaicontrols_content_scan_errors_total, a metric the main proxy exposes and the main proxy's ownServiceMonitoralready scrapes, independent of this NetworkPolicy.promptguard:networkPolicy:allowIngress:- from:- namespaceSelector:matchLabels:kubernetes.io/metadata.name: monitoringpodSelector:matchLabels:app.kubernetes.io/name: prometheusports:- protocol: TCPport: 8000Replace the selector with wherever your Prometheus actually runs. If your cluster restricts pod ingress by some other mechanism (a service mesh policy, for example), set
promptguard.networkPolicy.enabled: falseinstead.upgradeorinstallthe chart. When enabled, it's wired intocontent.pluginsautomatically. The score/flag field it populates depends on which surface was scanned —object.request.classifierInjectionScore/hasClassifierInjectionfor prompts,object.llm.response.classifierInjectionScore/hasClassifierInjectionfor LLM responses,object.mcp.response.classifierInjectionScore/hasClassifierInjectionfor MCP tool responses — but a classifier-only finding has no effect on the decision by itself on any of them. Enabling the sidecar alone changes nothing enforceable: you must separately enable a policy that reads the classifier's score (the shipped, disabled-by-defaultllm-injection-classifier-warn/llm-injection-classifier-denytemplates cover the request-side field only — write your own for the response-side fields, or one of your own for prompts) before it can warn or deny anything. Until you do, the classifier only ever informs whichever of these fields applies for any policy you write against it — audit-only in practice, not because of a setting, but because nothing enforces on it yet.
What's not protected by default
The classifier sidecar has no request authentication of its own — the chart restricts who can reach it with a NetworkPolicy (by default, only the proxy's own pods), but that's a label match, not a cryptographic identity check. In a namespace where other principals can create pods, anything able to create one with matching labels can call the sidecar directly. Treat the namespace this chart is installed into as a trust boundary: don't grant pod-create permissions there to principals you wouldn't trust with the sidecar's inference capacity.
Findings aren't durably recorded by default
That inline evaluation is not the same as a durable record: there is currently no durable, per-finding record of the classifier's own score anywhere, for any surface or decision. The one table that stores individual finding detail (plugin, score, entity) is written by the separate, opt-in Session Intelligence content safety scanner (sessionIntelligence.enabled: true + sessionIntelligence.content_safety.enabled: true, requiring a persistent store) — and its plugin list deliberately excludes the classifier, to avoid a second, duplicate call to the sidecar for content the primary scan already scored. A request some other policy denies or holds still gets a coarse Security Events row for that decision — it just never carries the classifier's own score. If you need to review the raw score while calibrating a threshold, capture it through your own external logging for now.
Whether whatever IS recorded (the coarse decision, and Session Intelligence's own pattern/injection-guard findings) survives a pod restart depends on your storage backend: with the default embedded SQLite (storage.database.driver: sqlite), set storage.persistentVolume.enabled: true or the database lives on ephemeral pod storage and starts empty again after a restart; with an external PostgreSQL (storage.database.driver: postgres), everything already persists regardless of persistentVolume.enabled — that setting only applies to the embedded-SQLite path.
This is one of several content-safety plugins the chart can deploy this way (alongside credential/PII pattern matching, which is on by default in a standard deployment); check your chart version's values.yaml comments for the full content.plugins list and current defaults.
Tune the scan timeout
Each content-safety plugin has its own per-call timeout (content.plugins[].timeoutSeconds, e.g. promptguard.timeoutSeconds for this one, default 1s). Separately, content.scanTimeoutSeconds bounds the whole parallel plugin fan-out for one scan call across every enabled plugin together (default 5s) — raise it if a plugin doing real work under load (a large-document classifier scan, several plugins contending for CPU) legitimately needs more time than the default outer deadline allows:
content:
scanTimeoutSeconds: 8