Guardrail
Scope: namespaced · Modes: catalog (bundled engine) · external (BYO HTTP) · Licensed: guardrail (Enterprise)
A Guardrail defines a data-path guardrail — content moderation / PII handling that runs inside the request path of a Gateway. Reference it from a Gateway via spec.guardrailRefs; the operator emits the upstream LiteLLMGuardrail wiring for you.
Both modes require the licensed guardrail feature, and both are routed through a small license-gate proxy the operator injects in front of the guardrail. The proxy verifies the signed license and fails closed: if the license lapses (expired, revoked, or removed) every request receives a guardrail block verdict instead of being forwarded — the guardrail stops adding value the moment you are no longer licensed. Unlicensed clusters get a Refused Guardrail and nothing is wired.
Spec
| Field | Type | Description |
|---|---|---|
mode | enum catalog | external (required) | Bundled engine vs. your own HTTP endpoint |
catalog | string | Bundled engine key (mode=catalog), e.g. pseudonymizer |
external | object | Your HTTP guardrail (mode=external) |
guardrailName | string | Override the LiteLLM guardrail_name (defaults to the catalog key or CR name) |
modes | []enum pre_call | post_call | during_call | logging_only | Execution modes; one LiteLLMGuardrail is emitted per mode. Defaults to the engine's modes (the pseudonymizer runs pre_call+post_call) or [pre_call] for external |
defaultOn | bool | Run on every request even without an explicit key/team opt-in. Catalog engines default on; external defaults off |
params | object (arbitrary JSON) | Provider params forwarded to LiteLLM under additional_provider_specific_params (merged over the engine defaults) |
logLevel | enum debug | info | warn | error | Verbosity of the license-gate proxy. debug logs one record per guardrail request — see Troubleshooting. Changing it restarts the proxy pod |
blockedReason | string | Message the proxy returns when it blocks a request because the feature is not licensed. Make it distinctive to recognize a license-gate block in gateway logs |
external
| Field | Description |
|---|---|
apiBase | Your guardrail HTTP endpoint (LiteLLM generic_guardrail_api; it POSTs to {apiBase}/beta/litellm_basic_guardrail_api) |
apiKeySecretRef | Same-namespace Secret whose value the proxy forwards to your endpoint as x-api-key |
unreachableFallback | fail_closed (default) or fail_open — LiteLLM's behavior if the guardrail is unreachable |
Modes
catalog— the operator deploys the bundled engine as one shared, ref-counted release for the cluster (innavique-guardrail-system) and puts a per-Guardrail license-gate proxy in front. The first engine ispseudonymizer(PII pseudonymization: masks people/organization names before the model sees them and restores them in the response). Since 0.4.0 it serves the Wäg guardrail protocol on a second endpoint as well, against the same mapping store.external— the operator deploys only the license-gate proxy, pointed at yourapiBase. Still licensed and proxied (unlike external MCP servers, BYO guardrails are not free).
How it wires into a Gateway
Add the Guardrail to a Gateway's guardrailRefs. Once the Guardrail is Ready, the Gateway emits a LiteLLMGuardrail per mode whose apiBase is the Guardrail's license-gate proxy (status.endpoint) — never the raw engine. Removing the reference (or deleting the Guardrail) prunes the emitted wiring.
Example — bundled PII pseudonymizer
apiVersion: core.navique.com/v1alpha1
kind: Guardrail
metadata:
name: pii-pseudonymizer
namespace: forge
spec:
mode: catalog
catalog: pseudonymizer
---
apiVersion: core.navique.com/v1alpha1
kind: Gateway
metadata:
name: forge-gw
namespace: forge
spec:
# … database/instance/secrets …
guardrailRefs:
- { name: pii-pseudonymizer }Example — bring your own HTTP guardrail
apiVersion: core.navique.com/v1alpha1
kind: Guardrail
metadata:
name: my-moderation
namespace: forge
spec:
mode: external
modes: [pre_call]
external:
apiBase: https://guardrail.example.com
unreachableFallback: fail_closed
apiKeySecretRef: { name: my-guardrail-key, key: apiKey }Troubleshooting a failed guardrail call
A guardrail block is indistinguishable to the caller by design, which leaves three failure modes that look identical from the gateway:
- the gateway never called the guardrail and failed the request for another reason,
- the license-gate proxy blocked it (unlicensed / expired / revoked),
- the engine blocked it, or could not be reached.
The proxy is the only component that sees all three, so it reports them two ways.
Counters (always on)
Every proxy serves Prometheus counters on port 9090, separate from the data port (8080) so they cannot shadow a path on your guardrail engine:
kubectl port-forward -n forge svc/pii-pseudonymizer-guardrail-proxy 9090:9090
curl -s localhost:9090/metrics| Metric | Answers |
|---|---|
navique_guardrail_proxy_requests_total{decision="forwarded"} | did the gateway call the guardrail at all? A flat counter means it never did |
…{decision="blocked_unlicensed"} | did the license gate block it? |
…{decision="upstream_error"} | was the engine unreachable? |
navique_guardrail_proxy_upstream_verdicts_total{action="…"} | what did the engine decide — NONE, GUARDRAIL_INTERVENED or BLOCKED? |
navique_guardrail_proxy_upstream_duration_seconds_{sum,count} | how slow are the engine calls? |
navique_guardrail_proxy_license_valid | is the mounted license currently satisfying guardrail? |
Per-request logging
Set spec.logLevel: debug on the Guardrail (this restarts the proxy pod) to get one structured record per request:
{"level":"DEBUG","msg":"guardrail request","request_id":"9f2c…","method":"POST",
"path":"/beta/litellm_basic_guardrail_api","decision":"forwarded","status":200,
"duration_ms":37,"upstream_action":"BLOCKED","upstream_blocked_reason":"…"}decision is what the proxy did; upstream_action is the engine's own verdict, read back from the response without altering it. The X-Request-Id header is reused from the caller when present, otherwise minted, forwarded to the engine and echoed on the response — so one id stitches the gateway, proxy and engine logs together.
WARNING
upstream_blocked_reason is the engine's own message and may quote the content it matched on. Raise logLevel while investigating, then lower it again.
Two things the proxy deliberately does not hide
- An unreachable engine returns HTTP 502, not a block. "The engine is down" is not "the engine blocked you": LiteLLM applies the
unreachableFallback(fail_closed/fail_open) to a transport failure, and synthesizing a block would silently override afail_openchoice. These failures are logged aterrorlevel regardless oflogLevel. - License-gate blocks are logged when they flip. The proxy logs a warning the moment the license stops satisfying
guardrail(and an info line when it starts again), so a lapse is visible without debug logging. Note that/healthzand/readyzstay200while failing closed — the pod is healthy, the enforcement is per-request, so don't look to pod readiness for the license state; usenavique_guardrail_proxy_license_valid.
Notes
- A determined operator could hand-author a raw upstream
LiteLLMGuardrailto skip the license gate; the platform avoids admission webhooks, so the in-core gate is best-effort — only the supported, managed path is gated. - The license-gate proxy is a separate, closed-source image (
ghcr.io/scigility/navique-guardrail-proxy); override with--guardrail-proxy-imageor the PlatformConfig registry host-mirror.
Status
{ phase, endpoint (the proxy URL the Gateway wires), guardrailNames, unreachableFallback, conditions, observedGeneration }. Short name gr.