AIGateway
AIGateway is the top-level resource for the Stacklok AI Gateway. One resource
configures the whole gateway: its listeners, the model providers it can reach,
the routes that map logical model names onto them, authentication, content
screening, audit and journaling, and its resilience and timeout behavior. Attach
access lists and MCP policy with AIPolicy.
API: ai-gateway.stacklok.dev/v1alpha1 · Scope: Namespaced · Short
names: aigw
Example
apiVersion: ai-gateway.stacklok.dev/v1alpha1
kind: AIGateway
metadata:
name: my-aigateway
namespace: default
spec:
auth: {}
gateway:
listeners:
- port: 0
providers:
- credentials:
type: APIKey
endpoint:
hostname: <string>
name: <string>
schema: OpenAI
routes:
- backendRefs:
- provider: <string>
name: <string>
Schema
spec
AIGatewaySpec defines the desired state of the AI Gateway.
| Field | Type | Description |
|---|---|---|
audit | object | Audit configures audit event emission. When nil or Enabled=false, no audit events are emitted. |
authrequired | object | Auth defines authentication and authorization configuration. |
gatewayrequired | object | Gateway defines listener configuration for the Envoy Gateway. |
guardrails | object | Guardrails configures cloud prompt-injection screening of inbound prompts. Engine-discriminated and config-driven; the operator synthesizes a webhook from this block that screens prompts via a cloud provider and blocks (Mode=Enforce) or only records (Mode=Monitor) per policy. Sits alongside spec.webhooks. NOTE: distinct from the historical "guardrails" wording on spec.processor (ProcessorConfig) — that refers to the in-cluster PII/PCI ext-proc and its "guardrails" Deployment. This top-level field is the new cloud prompt-injection guardrail and is unrelated to PII scanning. |
journaling | object | Journaling configures capture of prompt and response payloads for compliance and forensics. Distinct from Audit: audit records control- plane and policy decisions, while journaling records the request and response bodies themselves. |
monitoring | object | Monitoring configures observability features. |
networkPolicies | object | NetworkPolicies configures NetworkPolicy generation for all components. |
policies | object | Policies enables AIPolicy CRD enforcement for group-based access control. |
processor | object | Processor defines PII processor configuration. |
providersrequired | object[] | Providers is the list of LLM provider backends. |
resilience | object | Resilience configures retry, circuit breaker, passive health checking, and upstream timeouts for all routes managed by this gateway. When nil, upstream errors pass through to the client unchanged (no retries). |
routesrequired | object[] | Routes defines model routing rules. Maximum 120 entries — the hard ceiling of 15 collapsed rules times 8 matches per rule (see below); the practical ceiling is lower and depends on how many routes share a backend. The operator collapses routes that share the same backendRefs and timeout into ONE AIGatewayRoute rule with one match per model, chunked at 8 matches per rule — e.g. 40 model routes split across 3 providers of <=8 models each become 3 rules; a single backend of 20 models becomes 3 rules (8+8+4). GeminiNative (path-matched) routes and the no-match default route are never collapsed; each keeps its own rule. Gateway API caps HTTPRoute.spec.rules at 16, and the upstream AI Gateway controller always appends a route-not-found rule (returning 404 for unmatched traffic) as the final entry, consuming one slot — so the collapsed rule count must stay at or below 15, and each rule holds at most 8 matches, giving the 15*8=120 model ceiling. The reconciler enforces this at reconcile time (see the RoutesValid status condition) since the grouping is not CEL-expressible; a spec with too many distinct backend/timeout combinations sets RoutesValid=False and the AIGatewayRoute is not updated (retains last-known-good) rather than failing admission. Inserting or removing a route that changes which routes share a group shifts every subsequent group's rule index (rule/i), and therefore its RLS generic_key route scope — the same class of budget-counter reset that already applies to reordering routes today, just now scoped to the group rather than the individual route. |
version | string | Version is the AI Gateway version. The operator derives the compatible Envoy Gateway version from a built-in compatibility matrix. default "v0.5.0" · pattern ^v\d+\.\d+\.\d+$ |
webhooks | object[] | Webhooks registers external in-cluster receivers that validate request traffic (Validating) and observe responses (Observation). Each entry names a same-namespace Service destination authenticated with a projected, audience-bound Kubernetes ServiceAccount token. Sits alongside spec.audit and spec.journaling; the operator and main processor wire these in (see docs/plan/webhooks.md). |
spec.audit
Audit configures audit event emission. When nil or Enabled=false, no audit events are emitted.
| Field | Type | Description |
|---|---|---|
enabled | boolean | Enabled turns audit event emission on. Off by default. default false |
webhookSampling | string | WebhookSampling is the fraction [0,1] of webhook invocation audit events to emit, written as a decimal string ("0", "0.1", "1", "1.0"). It downsamples only aigw.processor.webhook.invocation; every other webhook event (denied, error, drop, circuit_*) emits at full rate. A string (rather than a float, which Kubernetes discourages in CRDs) and a pointer so "unset" (emit all, the default applied at render time) is distinguishable from an explicit "0" (emit none). Has no effect unless audit.enabled is true and spec.webhooks is configured. pattern ^(0(\.[0-9]+)?|1(\.0+)?)$ |
spec.auth
Auth defines authentication and authorization configuration.
| Field | Type | Description |
|---|---|---|
authz | object | Authz configures platform-owned role definitions consumed by AIPolicy enforcement and admin-surface authorization checks. |
oidc | object | OIDC configures OpenID Connect authentication. |
virtualAPIKeys | object | VirtualAPIKeys configures the optional API key service. |
workloadIssuers | object[] | WorkloadIssuers configures additional internal JWT issuers that mint short-lived tokens carrying an end-user identity on behalf of a platform service (e.g. Atrium Front Door TxTokens, EdDSA-signed). Tokens validate against the same jwt_authn filter as the user IdP and resolve to the SAME x-user-id identity spine; they flow through the SAME claim-based AIPolicy authorization. Distinct from spec.auth.oidc (the user IdP). Configuring a workload issuer makes the gateway's identity headers authoritative (ingress-stripped) and requires spec.policies enforcement to be enabled. |
spec.auth.authz
Authz configures platform-owned role definitions consumed by AIPolicy enforcement and admin-surface authorization checks.
| Field | Type | Description |
|---|---|---|
rolesrequired | map<string, array> | Roles maps a role name to the list of principal matchers that grant that role. A principal gets the role if any matcher matches. |
spec.auth.oidc
OIDC configures OpenID Connect authentication.
| Field | Type | Description |
|---|---|---|
audiencerequired | string | Audience is the expected aud claim value in OIDC tokens. Must match the audience/client_id registered in your OIDC provider. Also used as the audience in JWTs synthesized by the virtual API key service. minLength 1 |
claimToHeaders | object[] | ClaimToHeaders maps JWT claims to request headers. |
issuerrequired | string | Issuer is the OIDC issuer URL. Must start with https://. pattern ^https:// |
remoteJWKSrequired | object | RemoteJWKS defines the remote JWKS endpoint for JWT validation. |
spec.auth.oidc.claimToHeaders[]
ClaimToHeaders maps JWT claims to request headers.
| Field | Type | Description |
|---|---|---|
claimrequired | string | Claim is the JWT claim name to extract. |
headerrequired | string | Header is the request header name to set. |
spec.auth.oidc.remoteJWKS
RemoteJWKS defines the remote JWKS endpoint for JWT validation.
| Field | Type | Description |
|---|---|---|
cacheDuration | string | CacheDuration is how long Envoy caches the fetched JWKS before refetch. Maps to Envoy Gateway SecurityPolicy remoteJWKS.cacheDuration (upstream default 300s when omitted). When unset the operator omits the key so existing SecurityPolicies render byte-identical. NOTE: this is also the de-facto revocation latency for the issuer's signing keys — a rotated/ revoked key stays trusted for up to this duration. Use the GEP-2257 duration subset (Envoy's pattern), e.g. "300s", "5m", "1h30m". pattern ^([0-9]{1,5}(h|m|s|ms)){1,4}$ |
urirequired | string | URI is the JWKS endpoint URL. Must use http or https scheme. pattern ^https?:// · minLength 1 |
spec.auth.virtualAPIKeys
VirtualAPIKeys configures the optional API key service.
| Field | Type | Description |
|---|---|---|
defaultTTL | string | DefaultTTL is the default time-to-live for newly created keys (e.g., "2160h" for 90 days). Deprecated: no longer consumed by the api-key-service. Virtual-key TTL is now the directory module's concern (key storage and lifecycle moved into the directory), and the pod no longer reads a DEFAULT_TTL env var. Retained to avoid a breaking schema change for existing manifests; a future cleanup PR may remove it. default "2160h" |
directory | object | Directory configures the gRPC endpoint of the toolhive-enterprise directory module that the api-key-service validates virtual keys against. Required in practice when Enabled is true — the api-key-service has no other source of truth for virtual keys since virtual-key storage and validation moved into the directory module. A nil Directory when the service is enabled surfaces at runtime as a dial error in the api-key-service (it has no endpoint to reach), not at admission. |
enabledrequired | boolean | Enabled deploys the api-key-service. The service currently hosts two roles: (1) it issues and validates virtual API keys (long-lived keys exchanged for a short-lived JWT carrying the issuing user's identity), and (2) it serves the management API (/v1/policies, /v1/me, /v1/budgets, /v1/models, /v1/mcp-servers) used by the Stacklok Enterprise UI. Set to true to enable either capability; operators who only want the management API still need this flag, because there is no separate gate for it today. The management-API role is co-located on this service as an interim hosting model and may move to its own service in a future release, pending an RFC on the management API's responsibilities; the deployment gate will be revisited if and when that happens. default false |
image | string | Image is the container image for the api-key-service. When empty the operator falls back to its built-in default (set by the operator Helm chart). An explicit empty string is rejected at admission so a typo combined with a chart that's missing the env doesn't surface as an unhelpful runtime reconcile error. minLength 1 |
imagePullPolicy | string | ImagePullPolicy defines the pull policy for the api-key-service container image. default "IfNotPresent" · enum: Always | Never | IfNotPresent |
maxReplicas | integer | MaxReplicas is the upper bound for HPA scaling. When set, the operator creates a HorizontalPodAutoscaler targeting the api-key-service Deployment. format int32 · min 1 |
maxTTL | string | MaxTTL is the maximum allowable TTL for any key (e.g., "8760h" for 365 days). Deprecated: no longer consumed by the api-key-service. Virtual-key TTL is now the directory module's concern (key storage and lifecycle moved into the directory), and the pod no longer reads a MAX_TTL env var. Retained to avoid a breaking schema change for existing manifests; a future cleanup PR may remove it. default "8760h" |
messageTimeout | string | MessageTimeout is the ext-proc message timeout for virtual key validation. default "2s" |
replicas | integer | Replicas is the number of api-key-service pods. default 2 · format int32 · min 1 |
resources | object | Resources defines compute resource requirements for the api-key-service pods. |
selfService | boolean | SelfService allows authenticated users to create their own keys. default true |
targetCPUUtilization | integer | TargetCPUUtilization is the CPU utilization target for HPA scaling (percentage). default 70 · format int32 · min 1 · max 100 |
tls | object | TLS provisions HTTPS for the api-key-service management API. When set the operator creates a cert-manager Certificate and the pod additionally serves HTTPS on port 8443 alongside HTTP on port 8080. The HTTP endpoint on port 8080 is kept because Envoy Gateway's SecurityPolicy remoteJWKS fetch uses an internal cluster not governed by BackendTLSPolicy; a follow-up EnvoyPatchPolicy is required to configure Envoy's internal JWT fetch cluster before the JWKS URI can switch to HTTPS. Requires cert-manager to be installed in the cluster. |
spec.auth.virtualAPIKeys.directory
Directory configures the gRPC endpoint of the toolhive-enterprise directory module that the api-key-service validates virtual keys against. Required in practice when Enabled is true — the api-key-service has no other source of truth for virtual keys since virtual-key storage and validation moved into the directory module. A nil Directory when the service is enabled surfaces at runtime as a dial error in the api-key-service (it has no endpoint to reach), not at admission.
| Field | Type | Description |
|---|---|---|
endpointrequired | string | Endpoint is the directory's gRPC address, e.g. "enterprise-manager.<namespace>.svc:9091". minLength 1 |
insecure | boolean | Insecure skips the per-RPC ServiceAccount-token credential the api-key-service otherwise presents to the directory's caller-auth gate. Only for local/e2e clusters running the directory with ENTERPRISE_MANAGER_GRPC_INSECURE=true — never set true in production. default false |
spec.auth.virtualAPIKeys.resources
Resources defines compute resource requirements for the api-key-service pods.
| Field | Type | Description |
|---|---|---|
limits | object | Limits defines the maximum resources allowed. |
requests | object | Requests defines the minimum resources required. |
spec.auth.virtualAPIKeys.resources.limits
Limits defines the maximum resources allowed.
| Field | Type | Description |
|---|---|---|
cpu | string | CPU is the CPU resource quantity (e.g., "500m", "2"). |
memory | string | Memory is the memory resource quantity (e.g., "512Mi", "2Gi"). |
spec.auth.virtualAPIKeys.resources.requests
Requests defines the minimum resources required.
| Field | Type | Description |
|---|---|---|
cpu | string | CPU is the CPU resource quantity (e.g., "500m", "2"). |
memory | string | Memory is the memory resource quantity (e.g., "512Mi", "2Gi"). |
spec.auth.virtualAPIKeys.tls
TLS provisions HTTPS for the api-key-service management API. When set the operator creates a cert-manager Certificate and the pod additionally serves HTTPS on port 8443 alongside HTTP on port 8080. The HTTP endpoint on port 8080 is kept because Envoy Gateway's SecurityPolicy remoteJWKS fetch uses an internal cluster not governed by BackendTLSPolicy; a follow-up EnvoyPatchPolicy is required to configure Envoy's internal JWT fetch cluster before the JWKS URI can switch to HTTPS. Requires cert-manager to be installed in the cluster.
| Field | Type | Description |
|---|---|---|
issuerRefrequired | object | IssuerRef references a cert-manager Issuer or ClusterIssuer used to provision the api-key-service TLS certificate. |
spec.auth.virtualAPIKeys.tls.issuerRef
IssuerRef references a cert-manager Issuer or ClusterIssuer used to provision the api-key-service TLS certificate.
| Field | Type | Description |
|---|---|---|
kind | string | Kind is either "Issuer" or "ClusterIssuer". default "ClusterIssuer" · enum: Issuer | ClusterIssuer |
namerequired | string | Name of the Issuer or ClusterIssuer. |
spec.auth.workloadIssuers[]
WorkloadIssuers configures additional internal JWT issuers that mint short-lived tokens carrying an end-user identity on behalf of a platform service (e.g. Atrium Front Door TxTokens, EdDSA-signed). Tokens validate against the same jwt_authn filter as the user IdP and resolve to the SAME x-user-id identity spine; they flow through the SAME claim-based AIPolicy authorization. Distinct from spec.auth.oidc (the user IdP). Configuring a workload issuer makes the gateway's identity headers authoritative (ingress-stripped) and requires spec.policies enforcement to be enabled.
| Field | Type | Description |
|---|---|---|
audiencesrequired | string[] | Audiences is the set of acceptable aud values for this issuer's tokens. MUST be disjoint from spec.auth.oidc.audience (a shared audience would let an Okta token validate on this path and vice-versa). MaxItems=8 mirrors Envoy Gateway and is load-bearing for the audience-distinctness CEL cost. |
issuerrequired | string | Issuer is the expected iss claim. Internal issuers use non-resolvable URIs (e.g. https://frontdoor.atrium.internal); jwt_authn matches iss as an opaque string, so https:// is NOT required here (unlike spec.auth.oidc). Must differ from spec.auth.oidc.issuer and from every other workload issuer (enforced cross-field — see reconciler validation). minLength 1 · maxLength 253 |
namerequired | string | Name is the jwt_authn provider name for this issuer and the principal.jwt .provider in generated authorization rules. Unique across workloadIssuers (enforced by listMapKey). Must not collide with the reserved provider names "okta" (user IdP) or "api-key-service" (virtual-key issuer), which are emitted by the controller and invisible to list-scoped CEL. pattern ^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$ · maxLength 63 |
remoteJWKSrequired | object | RemoteJWKS is the JWKS endpoint used to verify this issuer's signatures (EdDSA/Ed25519 or any alg the JWKS advertises — alg cannot be pinned at the Envoy Gateway layer, so the JWKS MUST advertise only trusted-alg keys). |
subjectClaim | string | SubjectClaim names the claim carrying the end-user identity, mapped (and OVERWRITTEN) onto x-user-id so this issuer shares the identity spine with the user IdP. The CRD default is authoritative for admitted objects; the controller's empty-string fallback to "sub" only covers non-admission paths (e.g. unit-test structs). default "sub" · minLength 1 · maxLength 128 |
spec.auth.workloadIssuers.remoteJWKS
RemoteJWKS is the JWKS endpoint used to verify this issuer's signatures (EdDSA/Ed25519 or any alg the JWKS advertises — alg cannot be pinned at the Envoy Gateway layer, so the JWKS MUST advertise only trusted-alg keys).
| Field | Type | Description |
|---|---|---|
cacheDuration | string | CacheDuration is how long Envoy caches the fetched JWKS before refetch. Maps to Envoy Gateway SecurityPolicy remoteJWKS.cacheDuration (upstream default 300s when omitted). When unset the operator omits the key so existing SecurityPolicies render byte-identical. NOTE: this is also the de-facto revocation latency for the issuer's signing keys — a rotated/ revoked key stays trusted for up to this duration. Use the GEP-2257 duration subset (Envoy's pattern), e.g. "300s", "5m", "1h30m". pattern ^([0-9]{1,5}(h|m|s|ms)){1,4}$ |
urirequired | string | URI is the JWKS endpoint URL. Must use http or https scheme. pattern ^https?:// · minLength 1 |
spec.gateway
Gateway defines listener configuration for the Envoy Gateway.
| Field | Type | Description |
|---|---|---|
bufferLimit | string | BufferLimit is the connection buffer limit for LLM request bodies. Raised from 10Mi to 50Mi to accommodate large request bodies (e.g. gemini-2.5-pro requests that embed large system prompts and MCP tool definitions) that exceed the previous default. Clusters using the CRD default previously received 413 Payload Too Large for such requests while clusters with an explicit 50Mi setting succeeded — this aligns the two. Memory impact: worst case is bufferLimit × connectionLimit per proxy pod (e.g. 50Mi × 500 = ~25 GiB); size proxy pod memory limits accordingly. default "50Mi" |
connectionLimit | integer | ConnectionLimit is the max concurrent client connections per proxy pod. default 500 · format int32 |
controllerName | string | ControllerName is the controller name for the GatewayClass resource. This must match the controllerName configured in the Envoy Gateway installation that should reconcile this GatewayClass. When running alongside an existing Envoy Gateway installation, set this to a unique value to avoid conflicts. Must be a domain-prefixed path (e.g., "gateway.envoyproxy.io/gatewayclass-controller"). default "gateway.envoyproxy.io/gatewayclass-controller" · pattern ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/.+$ · minLength 1 |
gatewayClassName | string | GatewayClassName is the name of the GatewayClass resource. default "eg" |
listenersrequired | object[] | Listeners defines the Gateway listener port and protocol. Exactly one listener must be specified; multi-listener gateways are not yet supported. |
proxy | object | Proxy configures the Envoy proxy Deployment created by Envoy Gateway. |
streamTimeouts | object | StreamTimeouts bounds streaming traffic without imposing a hard total request cap. It is the recommended way to protect the data path: the idle timeout reaps stalled connections (and resets on every byte, so it never cuts a healthy stream), while the max-stream-duration provides an absolute ceiling. Applied gateway-wide (the underlying ClientTrafficPolicy and BackendTrafficPolicy are one-per-gateway). When nil, the operator applies its defaults (streamIdleTimeout=5m, maxStreamDuration=15m). |
stripHeaders | string[] | StripHeaders lists identity headers to strip from inbound requests before JWT processing to prevent spoofing. |
timeouts | object | Timeouts defines an optional total request deadline applied to all routes when no per-route timeout is set. This is an opt-in absolute cap on the entire request (including the streamed response body): once set, Envoy 504s any request — healthy or not — that outlives it. It is DISABLED by default so long-running streaming turns (chat completions with stream=true, long-context Claude requests) are never cut mid-stream. To bound streaming traffic without a hard total cap, use StreamTimeouts (idle + max-stream-duration) instead. Only set Timeouts when you deliberately want every request to fail past a fixed wall-clock budget. Note: the operator also applies a default maxStreamDuration ceiling (15m); a requestTimeout LONGER than the effective maxStreamDuration is pre-empted by that ceiling (the stream is cut first), so to allow longer requests you must raise BOTH this value and spec.gateway.streamTimeouts.maxStreamDuration. The operator surfaces a TimeoutConfigValid=False condition + Warning event when this happens. |
tls | object | GatewayTLS configures the gateway-level TLS certificate (cert-manager). |
spec.gateway.listeners[]
Listeners defines the Gateway listener port and protocol. Exactly one listener must be specified; multi-listener gateways are not yet supported.
| Field | Type | Description |
|---|---|---|
portrequired | integer | Port is the listener port number. format int32 · min 1 · max 65535 |
protocol | string | Protocol is the listener protocol. default "HTTPS" · enum: HTTPS | HTTP |
tls | object | TLS configures TLS for this listener. |
spec.gateway.listeners.tls
TLS configures TLS for this listener.
| Field | Type | Description |
|---|---|---|
certificateRefrequired | object | CertificateRef references a Kubernetes Secret containing the TLS cert. |
spec.gateway.listeners.tls.certificateRef
CertificateRef references a Kubernetes Secret containing the TLS cert.
| Field | Type | Description |
|---|---|---|
namerequired | string | Name of the referenced resource. |
spec.gateway.proxy
Proxy configures the Envoy proxy Deployment created by Envoy Gateway.
| Field | Type | Description |
|---|---|---|
image | string | Image is the full container image reference, including tag, for the Envoy data-plane proxy pod that Envoy Gateway creates from the EnvoyProxy resource. When empty, Envoy Gateway's own compiled-in default applies. Setting an explicit empty string is rejected at admission so a typo doesn't surface as a confusing reconcile error. A tag or digest is REQUIRED. The operator chart already hard-fails a dataPlaneProxy.image.repository set without a tag, because an untagged reference resolves to :latest and lands the proxy in the ImagePullBackOff this field exists to escape. Without this rule the CR path would be laxer than the chart path for the same setting, so the guarantee could be bypassed through the very field that provides it. The check finds the ':' AFTER the last '/', so a registry port (registry.internal:5000/envoy) is not mistaken for a tag. minLength 1 · maxLength 512 |
replicas | integer | Replicas is the number of proxy pods. Passed to the EnvoyProxy resource; the proxy Deployment is owned by Envoy Gateway, not this operator. default 2 · format int32 |
resources | object | Resources defines compute resource requirements for the proxy pods. |
servicePorts | object[] | ServicePorts defines NodePort mappings for the proxy service. Only used when ServiceType is NodePort. |
serviceType | string | ServiceType is the Kubernetes Service type for the proxy. default "ClusterIP" · enum: ClusterIP | NodePort | LoadBalancer |
shutdown | object | Shutdown tunes how the Envoy proxy pod drains in-flight connections on a rollout. It maps to the EnvoyProxy spec.shutdown block. When unset, the operator DERIVES the drain timeout from the effective maxStreamDuration ceiling plus a teardown margin, so a rolling update never severs a still-running stream. Envoy Gateway in turn sizes the proxy pod's terminationGracePeriodSeconds to drainTimeout + 5m, so only drainTimeout needs to be set. |
spec.gateway.proxy.resources
Resources defines compute resource requirements for the proxy pods.
| Field | Type | Description |
|---|---|---|
limits | object | Limits defines the maximum resources allowed. |
requests | object | Requests defines the minimum resources required. |
spec.gateway.proxy.resources.limits
Limits defines the maximum resources allowed.
| Field | Type | Description |
|---|---|---|
cpu | string | CPU is the CPU resource quantity (e.g., "500m", "2"). |
memory | string | Memory is the memory resource quantity (e.g., "512Mi", "2Gi"). |
spec.gateway.proxy.resources.requests
Requests defines the minimum resources required.
| Field | Type | Description |
|---|---|---|
cpu | string | CPU is the CPU resource quantity (e.g., "500m", "2"). |
memory | string | Memory is the memory resource quantity (e.g., "512Mi", "2Gi"). |
spec.gateway.proxy.servicePorts[]
ServicePorts defines NodePort mappings for the proxy service. Only used when ServiceType is NodePort.
| Field | Type | Description |
|---|---|---|
namerequired | string | Name of the port. |
nodePort | integer | NodePort is the static NodePort number. format int32 |
portrequired | integer | Port is the service port number. format int32 |
protocol | string | Protocol is the port protocol. default "TCP" |
targetPort | integer | TargetPort is the container port. format int32 |
spec.gateway.proxy.shutdown
Shutdown tunes how the Envoy proxy pod drains in-flight connections on a rollout. It maps to the EnvoyProxy spec.shutdown block. When unset, the operator DERIVES the drain timeout from the effective maxStreamDuration ceiling plus a teardown margin, so a rolling update never severs a still-running stream. Envoy Gateway in turn sizes the proxy pod's terminationGracePeriodSeconds to drainTimeout + 5m, so only drainTimeout needs to be set.
| Field | Type | Description |
|---|---|---|
drainTimeout | string | DrainTimeout is how long Envoy waits for open connections to drain before it force-closes them on shutdown. To avoid cutting an in-flight LLM stream during a rollout it must cover a full stream, so when unset the operator derives it from the effective maxStreamDuration ceiling (spec.gateway.streamTimeouts.maxStreamDuration) plus a fixed teardown margin; when the ceiling is unbounded ("0s") it falls back to a fixed default. Set an explicit value only to override that derivation. WARNING: an explicit value below maxStreamDuration re-introduces the mid-stream sever this derivation exists to prevent — Envoy force-closes the connection before a full-length stream finishes. The operator flags this via the advisory TimeoutConfigValid status condition (reason DrainTimeoutBelowMaxStreamDuration) but does not reject it. Keep any override at or above the ceiling, or leave it unset. Accepts the Envoy Gateway Duration form (e.g. "16m", "960s"); no fractional values or us/ns units. pattern ^([0-9]{1,5}(h|m|s|ms)){1,4}$ |
minDrainDuration | string | MinDrainDuration is the minimum drain time Envoy observes even when all connections close early, allowing endpoint deprogramming to complete. When unset, Envoy Gateway's default (10s) applies. Accepts the Envoy Gateway Duration form (e.g. "10s", "30s"); no fractional values or us/ns units. pattern ^([0-9]{1,5}(h|m|s|ms)){1,4}$ |
spec.gateway.streamTimeouts
StreamTimeouts bounds streaming traffic without imposing a hard total request cap. It is the recommended way to protect the data path: the idle timeout reaps stalled connections (and resets on every byte, so it never cuts a healthy stream), while the max-stream-duration provides an absolute ceiling. Applied gateway-wide (the underlying ClientTrafficPolicy and BackendTrafficPolicy are one-per-gateway). When nil, the operator applies its defaults (streamIdleTimeout=5m, maxStreamDuration=15m).
| Field | Type | Description |
|---|---|---|
maxStreamDuration | string | MaxStreamDuration is the absolute ceiling on a single stream's lifetime, enforced regardless of activity. Unlike StreamIdleTimeout it does not reset on activity. Accepts the Envoy Gateway Duration form (e.g. "15m", "30m"); no fractional values or us/ns units. When unset, the operator applies 15m. "0s" makes the stream unbounded (only the idle timeout and any explicit RequestTimeout then apply). pattern ^([0-9]{1,5}(h|m|s|ms)){1,4}$ |
streamIdleTimeout | string | StreamIdleTimeout is the maximum time a stream may go without any upstream or downstream activity before Envoy terminates it. It resets on every byte, so it never cuts a healthy stream — it only reaps stalled connections. Accepts the Envoy Gateway Duration form (e.g. "5m", "90s"); no fractional values or us/ns units. When unset, the operator applies 5m. "0s" disables idle reaping (not recommended: a hung upstream then leaks the connection). pattern ^([0-9]{1,5}(h|m|s|ms)){1,4}$ |
spec.gateway.timeouts
Timeouts defines an optional total request deadline applied to all routes when no per-route timeout is set. This is an opt-in absolute cap on the entire request (including the streamed response body): once set, Envoy 504s any request — healthy or not — that outlives it. It is DISABLED by default so long-running streaming turns (chat completions with stream=true, long-context Claude requests) are never cut mid-stream. To bound streaming traffic without a hard total cap, use StreamTimeouts (idle + max-stream-duration) instead. Only set Timeouts when you deliberately want every request to fail past a fixed wall-clock budget. Note: the operator also applies a default maxStreamDuration ceiling (15m); a requestTimeout LONGER than the effective maxStreamDuration is pre-empted by that ceiling (the stream is cut first), so to allow longer requests you must raise BOTH this value and spec.gateway.streamTimeouts.maxStreamDuration. The operator surfaces a TimeoutConfigValid=False condition + Warning event when this happens.
| Field | Type | Description |
|---|---|---|
requestTimeout | string | RequestTimeout is the maximum total duration allowed for a single request, including the streamed response body. This is an absolute cap: the request is terminated when it elapses even if data is still flowing. Accepts the Envoy Gateway Duration form (e.g. "5m"). "0s" disables the cap. pattern ^([0-9]{1,5}(h|m|s|ms)){1,4}$ |
spec.gateway.tls
GatewayTLS configures the gateway-level TLS certificate (cert-manager).
| Field | Type | Description |
|---|---|---|
certificateRef | object | CertificateRef references an existing cert-manager Certificate. |
dnsNames | string[] | DNSNames is the list of DNS names for the certificate. |
duration | string | Duration is the certificate duration (e.g., "2160h" for 90 days). default "2160h" |
issuerRef | object | IssuerRef configures the cert-manager issuer. When set, the operator creates a cert-manager Certificate resource. |
renewBefore | string | RenewBefore is how long before expiry to renew (e.g., "720h" for 30 days). default "720h" |
spec.gateway.tls.certificateRef
CertificateRef references an existing cert-manager Certificate.
| Field | Type | Description |
|---|---|---|
namerequired | string | Name of the referenced resource. |
spec.gateway.tls.issuerRef
IssuerRef configures the cert-manager issuer. When set, the operator creates a cert-manager Certificate resource.
| Field | Type | Description |
|---|---|---|
kind | string | Kind is either "Issuer" or "ClusterIssuer". default "ClusterIssuer" · enum: Issuer | ClusterIssuer |
namerequired | string | Name of the Issuer or ClusterIssuer. |
spec.guardrails
Guardrails configures cloud prompt-injection screening of inbound prompts. Engine-discriminated and config-driven; the operator synthesizes a webhook from this block that screens prompts via a cloud provider and blocks (Mode=Enforce) or only records (Mode=Monitor) per policy. Sits alongside spec.webhooks. NOTE: distinct from the historical "guardrails" wording on spec.processor (ProcessorConfig) — that refers to the in-cluster PII/PCI ext-proc and its "guardrails" Deployment. This top-level field is the new cloud prompt-injection guardrail and is unrelated to PII scanning.
| Field | Type | Description |
|---|---|---|
bedrock | object | Bedrock configures the AWS Bedrock Guardrails provider. Required when engine is BedrockGuardrails (enforced by the CEL rules above). The AWS principal the adapter authenticates as (the IRSA-projected role by default, or the static credentials in credentialsSecretRef) must be granted the bedrock:ApplyGuardrail action on the guardrail ARN. |
enabled | boolean | Enabled turns prompt-injection screening on. Off by default. default false |
enginerequired | string | Engine selects the cloud guardrail provider. v1: BedrockGuardrails. enum: BedrockGuardrails |
failurePolicy | string | FailurePolicy decides the request outcome when the guardrail call fails. Fail (default): deny the request (fail-closed); Ignore: let it proceed (fail-open). default "Fail" · enum: Fail | Ignore |
maxReplicas | integer | MaxReplicas is the upper bound for HPA scaling. When set, the operator creates a HorizontalPodAutoscaler targeting the guardrails-adapter Deployment. format int32 · min 1 |
mode | string | Mode decides whether a flagged request is blocked (Enforce) or only observed (Monitor). Maps onto the synthesized webhook Type. default "Enforce" · enum: Enforce | Monitor |
phasesrequired | string[] | Phases lists the request-lifecycle points at which screening runs. v1 supports only Request. |
replicas | integer | Replicas is the number of guardrails-adapter pods. It is also the HPA floor (minReplicas) when MaxReplicas is set. default 1 · format int32 · min 1 |
resources | object | Resources defines compute resource requests/limits for the guardrails-adapter container. When unset, the operator uses modest built-in defaults (requests: 50m/64Mi, limits: 500m/256Mi). |
targetCPUUtilization | integer | TargetCPUUtilization is the CPU utilization target for HPA scaling (percentage). The operator applies a runtime fallback of 75 when this field is nil and MaxReplicas is set. No kubebuilder default is emitted because the paired CEL rule above (`has(targetCPUUtilization) -> has(maxReplicas)`) needs `nil` to be observable as "user did not set this", which a default-everything-to-75 would erase. format int32 · min 1 · max 100 |
timeoutSeconds | integer | TimeoutSeconds bounds a single guardrail screening call. default 5 · format int32 · min 1 · max 120 |
unscreenableContentPolicy | string | UnscreenableContentPolicy decides the request outcome when user content is present but cannot be extracted into screenable text (e.g. an image-only or multimodal message, or an unrecognized content shape). Deny (default): block it (fail-closed); Admit: let it through unscreened. This is separate from FailurePolicy so that unscreenable-but-benign content can be admitted while genuine guardrail call failures still fail closed. The decision is recorded in the audit trail either way. default "Deny" · enum: Admit | Deny |
spec.guardrails.bedrock
Bedrock configures the AWS Bedrock Guardrails provider. Required when engine is BedrockGuardrails (enforced by the CEL rules above). The AWS principal the adapter authenticates as (the IRSA-projected role by default, or the static credentials in credentialsSecretRef) must be granted the bedrock:ApplyGuardrail action on the guardrail ARN.
| Field | Type | Description |
|---|---|---|
credentialsSecretRef | object | CredentialsSecretRef optionally references a Secret of static AWS credentials for the non-IRSA fallback. When omitted, the adapter uses the pod's IRSA-projected role. The Secret must contain a single key "credentials" whose value is an AWS credentials file in INI format with a [default] profile (aws_access_key_id, aws_secret_access_key, optional aws_session_token) — the same format used by the Envoy AI Gateway AWS rotator. Name-only reference, resolved in the AIGateway namespace. |
guardrailIdrequired | string | GuardrailID is the Bedrock guardrail identifier. minLength 1 · maxLength 64 |
guardrailVersionrequired | string | GuardrailVersion is the Bedrock guardrail version: either the string "DRAFT" or a positive integer string (e.g. "1", "12"). The Bedrock API rejects any other value; rejecting it at admission gives a cleaner error. pattern ^(DRAFT|[1-9][0-9]*)$ · maxLength 12 |
regionrequired | string | Region is the AWS region hosting the guardrail. pattern ^[a-z][a-z0-9-]*[a-z0-9]$ · maxLength 63 |
spec.guardrails.bedrock.credentialsSecretRef
CredentialsSecretRef optionally references a Secret of static AWS credentials for the non-IRSA fallback. When omitted, the adapter uses the pod's IRSA-projected role. The Secret must contain a single key "credentials" whose value is an AWS credentials file in INI format with a [default] profile (aws_access_key_id, aws_secret_access_key, optional aws_session_token) — the same format used by the Envoy AI Gateway AWS rotator. Name-only reference, resolved in the AIGateway namespace.
| Field | Type | Description |
|---|---|---|
namerequired | string | Name of the referenced resource. |
spec.guardrails.resources
Resources defines compute resource requests/limits for the guardrails-adapter container. When unset, the operator uses modest built-in defaults (requests: 50m/64Mi, limits: 500m/256Mi).
| Field | Type | Description |
|---|---|---|
limits | object | Limits defines the maximum resources allowed. |
requests | object | Requests defines the minimum resources required. |
spec.guardrails.resources.limits
Limits defines the maximum resources allowed.
| Field | Type | Description |
|---|---|---|
cpu | string | CPU is the CPU resource quantity (e.g., "500m", "2"). |
memory | string | Memory is the memory resource quantity (e.g., "512Mi", "2Gi"). |
spec.guardrails.resources.requests
Requests defines the minimum resources required.
| Field | Type | Description |
|---|---|---|
cpu | string | CPU is the CPU resource quantity (e.g., "500m", "2"). |
memory | string | Memory is the memory resource quantity (e.g., "512Mi", "2Gi"). |
spec.journaling
Journaling configures capture of prompt and response payloads for compliance and forensics. Distinct from Audit: audit records control- plane and policy decisions, while journaling records the request and response bodies themselves.
| Field | Type | Description |
|---|---|---|
buffer | object | Buffer tunes the in-memory dispatch channel and per-record capture caps. When unset, defaults are applied. |
enabled | boolean | Enabled turns journaling on. Off by default. default false |
includeResponses | boolean | IncludeResponses journals response bodies in addition to requests. Off by default — responses may include bearer tokens, session state, or regenerated PII and must be an explicit opt-in. default false |
spec.journaling.buffer
Buffer tunes the in-memory dispatch channel and per-record capture caps. When unset, defaults are applied.
| Field | Type | Description |
|---|---|---|
maxInFlight | integer | MaxInFlight bounds records in the in-memory dispatch channel. When full, records are dropped; each drop emits an audit event with a monotonic sequence number so SIEM can detect gaps. default 1024 · format int32 · min 1 · max 100000 |
maxResponseCapture | string | MaxResponseCapture caps aggregated streaming-response bytes per record. Over-cap streams are flagged truncated=true. Accepts Kubernetes resource-quantity notation (e.g. "2Mi", "512Ki"). default "2Mi" · pattern ^[0-9]+(Ki|Mi|Gi|K|M|G)?$ |
spec.monitoring
Monitoring configures observability features.
| Field | Type | Description |
|---|---|---|
grafanaDashboards | object | GrafanaDashboards configures Grafana dashboard ConfigMap generation. |
otelCollector | object | OTelCollector configures OpenTelemetry collector settings for guardrails. |
tokenMetrics | object | TokenMetrics configures token usage metric emission from the guardrails ext-proc. |
spec.monitoring.grafanaDashboards
GrafanaDashboards configures Grafana dashboard ConfigMap generation.
| Field | Type | Description |
|---|---|---|
enabledrequired | boolean | Enabled toggles Grafana dashboard ConfigMap generation. default false |
spec.monitoring.otelCollector
OTelCollector configures OpenTelemetry collector settings for guardrails.
| Field | Type | Description |
|---|---|---|
egressCIDRs | string[] | EgressCIDRs optionally allowlists IP ranges for the main-processor's NetworkPolicy egress to this collector, in addition to the always-present namespaceSelector rule. A namespaceSelector cannot match an off-cluster endpoint or a bare hostname/IP, so a collector reachable only that way needs an explicit CIDR to receive traffic under NetworkPolicy enforcement. Each entry must be a valid IPv4 or IPv6 CIDR (e.g. "10.0.0.0/8", "203.0.113.5/32"). Leave empty to rely solely on the namespaceSelector rule. This regex is defense-in-depth only, not the enforcement: it rejects obviously malformed shapes and bounds octet/prefix ranges, but RE2 (no lookahead) cannot fully validate every case (e.g. IPv6 double "::" compression rules). filterValidCIDRs (internal/controller/resources.go), via netip.ParsePrefix, is the real gate before anything reaches the K8s API — see ConditionOTelCollectorEgressCIDRsInvalid. |
endpoint | string | Endpoint is the OTLP gRPC endpoint for traces and metrics. |
protocol | string | Protocol selects the OTLP exporter protocol the main-processor and api-key-service pods use to reach Endpoint. Defaults to "grpc" when unset, matching the operator's own OTLP exporter default. Set to "http/protobuf" to target an HTTP-only collector receiver. default "grpc" · enum: grpc | http/protobuf |
spec.monitoring.tokenMetrics
TokenMetrics configures token usage metric emission from the guardrails ext-proc.
| Field | Type | Description |
|---|---|---|
enabledrequired | boolean | Enabled toggles token usage metric emission from the main processor ext-proc. default false |
labels | object[] | Labels maps request header names to metric attribute names. Only listed headers are extracted and attached to the aigw.token.usage metric. Use only low-cardinality headers (model, department, team) to avoid metric cardinality explosion. |
prometheusAddr | string | PrometheusAddr is the address to serve Prometheus /metrics on (e.g., ":9090"). When empty, the Prometheus HTTP endpoint is disabled. |
spec.monitoring.tokenMetrics.labels[]
Labels maps request header names to metric attribute names. Only listed headers are extracted and attached to the aigw.token.usage metric. Use only low-cardinality headers (model, department, team) to avoid metric cardinality explosion.
| Field | Type | Description |
|---|---|---|
attributerequired | string | Attribute is the metric attribute name (e.g., "model"). minLength 1 |
headerrequired | string | Header is the lowercase HTTP request header name (e.g., "x-ai-eg-model"). minLength 1 |
spec.networkPolicies
NetworkPolicies configures NetworkPolicy generation for all components.
| Field | Type | Description |
|---|---|---|
enabledrequired | boolean | Enabled toggles NetworkPolicy creation for all components. default false |
spec.policies
Policies enables AIPolicy CRD enforcement for group-based access control.
| Field | Type | Description |
|---|---|---|
defaultAction | string | DefaultAction is applied when no AIPolicy matches the request principal. 'Deny' is the default and recommended posture: rule generation today emits Allow-rules exclusively, so combining 'Allow' default with the current PR's ruleset produces an effectively open gateway. 'Allow' is accepted so that a later phase introducing Deny-rules (Allow + Deny-exceptions composition) does not require a CRD migration — operators opting into 'Allow' today must do so deliberately with that future ruleset in mind. default "Deny" · enum: Allow | Deny |
enabled | boolean | Enabled toggles AIPolicy enforcement. When unset, defaults to true so that `spec.policies: {}` opts into enforcement rather than silently disabling it. Use a pointer so an explicit `enabled: false` survives round-tripping without being overwritten by the default. default true |
spec.processor
Processor defines PII processor configuration.
| Field | Type | Description |
|---|---|---|
diagnostics | object | Diagnostics tunes Debug-level diagnostic log emission from the processor. Every field is Debug-only: nothing here changes runtime behavior outside the log stream, and nothing emits unless the main-processor pod runs with LOG_LEVEL=DEBUG. Default values are chosen so an operator enabling DEBUG for unrelated diagnostics does not inherit PII emission as a side effect. See ProcessorDiagnostics for the per-field threat model. |
extractionFailureAction | string | ExtractionFailureAction controls behaviour when extracting text from the request body fails (e.g. malformed JSON). "Block" (default, fail-closed) blocks the request with a 403; "Continue" (fail-open) lets it through unscanned. Applies to the processor's extraction stage and is independent of the detection engine. default "Block" · enum: Block | Continue |
image | string | Image is the container image for the main processor. When empty, the operator falls back to its built-in default (`MAIN_PROCESSOR_IMAGE` env var, set by the operator Helm chart), so most installs should leave this unset and let the chart pin the version. Setting an explicit empty string is rejected at admission so a typo combined with a chart that's missing the env doesn't surface as an unhelpful runtime reconcile error. minLength 1 |
imagePullPolicy | string | ImagePullPolicy defines the pull policy for the guardrails container image. default "IfNotPresent" · enum: Always | Never | IfNotPresent |
maxReplicas | integer | MaxReplicas is the upper bound for HPA scaling. When set, the operator creates a HorizontalPodAutoscaler targeting the guardrails Deployment. format int32 · min 1 |
messageTimeout | string | MessageTimeout is the ext-proc message timeout: how long Envoy waits for the main-processor to ack each request/response body chunk. The budget must outlast every downstream call made while the processor holds the chunk — most importantly the NER /analyze RTT — so it is bounded below by NERProviderConfig.Timeout. The default (60s) sits comfortably above the 5s NER default and covers the 200-500 KB request bodies typical of agent CLI traffic (Claude Code, Gemini CLI). The CEL rule on ProcessorConfig enforces messageTimeout > nerProvider.timeout at admission; an operator who lowers this value should lower nerProvider.timeout in lock-step. default "60s" |
mutationFailureAction | string | MutationFailureAction controls behaviour when redacting (mutating) the body fails. "Block" (default, fail-closed) blocks the request with a 403; "Continue" (fail-open) lets it through with no redaction applied. Applies to the processor's mutation stage and is independent of the detection engine. default "Block" · enum: Block | Continue |
nerProvider | object | NERProvider configures the ML/external NER backend that performs PII detection. The Type discriminator selects the provider implementation and the matching sub-block carries provider-specific settings. When nil, no detection runs and the processor passes traffic through; the operator gates the EnvoyExtensionPolicy on this field, so an unconfigured processor receives no traffic. |
placeholder | string | Placeholder is the redaction string substituted for matched bytes when the active action is Redact. Empty falls back to a default derived from the entity type (e.g. "[REDACTED-EMAIL_ADDRESS]"). |
replicas | integer | Replicas is the number of guardrail ext-proc pods. default 2 · format int32 · min 1 |
requestAction | string | RequestAction is the action the processor takes when the NER provider reports a PII match on the request path. "Block" (default) returns 403, "Redact" replaces the matched bytes with Placeholder, "LogOnly" emits telemetry and lets the request through unchanged. default "Block" · enum: Block | Redact | LogOnly |
resources | object | Resources defines compute resource requirements for the guardrails pods. |
responseAction | string | ResponseAction is the action the processor takes when the NER provider reports a PII match on the response path. Same enum as RequestAction; when empty, the processor falls back to RequestAction. enum: Block | Redact | LogOnly |
targetCPUUtilization | integer | TargetCPUUtilization is the CPU utilization target for HPA scaling (percentage). default 75 · format int32 · min 1 · max 100 |
terminationGracePeriodSeconds | integer | TerminationGracePeriodSeconds is how long the kubelet waits after sending SIGTERM before it SIGKILLs a main-processor pod. On SIGTERM the processor calls grpcServer.GracefulStop(), which keeps every in-flight ext-proc stream (i.e. an in-progress streaming LLM completion) open until it finishes; the kubelet then kills the pod once this period elapses. The Kubernetes default (30s) is far shorter than a long agentic generation, so a rollout that lands mid-stream severs the upstream connection and the caller sees an unexpected EOF with no response. When unset, the operator DERIVES this from the effective maxStreamDuration ceiling (spec.gateway.streamTimeouts.maxStreamDuration) plus a fixed teardown margin, so the grace always covers a full-length stream and moves in lock-step with the ceiling instead of drifting from it. Set an explicit value only to pin a tighter or wider known bound; when the ceiling is unbounded ("0s") the operator falls back to a fixed default. format int64 · min 1 |
tls | object | TLS overrides the mTLS settings for the Envoy-to-main-processor hop. Setting it also opts this gateway into that mTLS regardless of the operator's default, which is how a CR asked for mTLS before the operator carried a chart-level switch (certManager.processorTLS.enabled). Prefer leaving this unset and enabling mTLS on the operator chart, so the issuer is named once for the whole platform: omitting it means "no per-gateway override", NOT "mTLS off". Do not add +nullable here. Every field below is optional, so an author opting in with a fully empty object can end up submitting a JSON null instead of `{}` — that is how the staging manifests broke. Marking null valid stores it, and a stored null unmarshals back to a nil pointer that reads as "no override" while the author meant "opt in": plaintext on a hop carrying full request and response bodies. Non-nullable makes a null a rejection on server-side apply (Flux, `kubectl apply --server-side`) and a prune-to-absent on a client-side create or update, so it is never stored as a silent off. Pinned by test-integration/processor_tls_validation_test.go. |
spec.processor.diagnostics
Diagnostics tunes Debug-level diagnostic log emission from the processor. Every field is Debug-only: nothing here changes runtime behavior outside the log stream, and nothing emits unless the main-processor pod runs with LOG_LEVEL=DEBUG. Default values are chosen so an operator enabling DEBUG for unrelated diagnostics does not inherit PII emission as a side effect. See ProcessorDiagnostics for the per-field threat model.
| Field | Type | Description |
|---|---|---|
includePIIInDebugLogs | boolean | IncludePIIInDebugLogs, when true, includes the matched substring (the raw bytes the PII detector flagged) in the "redact match diagnostic" Debug record. Default false. SECURITY: enabling this writes raw user PII to whatever log sink the main-processor pod forwards to at Debug level. Two gates must both be open for the substring to land in a log: this flag AND LOG_LEVEL=DEBUG on the pod. The Info-level absence contract pinned by the processor unit tests holds regardless of this flag. Intended for incident response only — leave false in production unless an active investigation requires the matched substring to be diagnosable from a support bundle. Note on the wire representation: this field combines a kubebuilder default of false with a JSON omitempty modifier. On a bool that pairing means "absent" and "explicitly false" are indistinguishable on Get — both round-trip as the default. That is intentional and aligned with the safety property (absent === safe default). Do not promote this to a *bool tri-state unless a future feature genuinely needs to distinguish unset from false. default false |
spec.processor.nerProvider
NERProvider configures the ML/external NER backend that performs PII detection. The Type discriminator selects the provider implementation and the matching sub-block carries provider-specific settings. When nil, no detection runs and the processor passes traffic through; the operator gates the EnvoyExtensionPolicy on this field, so an unconfigured processor receives no traffic.
| Field | Type | Description |
|---|---|---|
circuitBreaker | object | CircuitBreaker overrides the failure threshold and reset timeout for the provider call path. |
concurrency | integer | Concurrency caps the number of Presidio /analyze calls the processor fans out in parallel per request (one goroutine per text node). Higher values reduce latency on multi-node payloads at the cost of more simultaneous connections to the Presidio backend. Takes effect on the processor rollout triggered by this config change. default 8 · format int32 · min 1 · max 128 |
failureAction | string | FailureAction controls behaviour when the provider circuit breaker is open or the provider returns an error. "fail-closed" (default) blocks the request with a 403 guardrail_circuit_open response; "fail-open" logs a warning and allows the request to proceed with no matches. default "fail-closed" · enum: fail-closed | fail-open |
presidio | object | Presidio carries Presidio-specific settings. Required when type=presidio. |
resultCache | object | ResultCache opts this gateway's NER scan results into the shared Redis/Valkey cache. Provider-agnostic (it caches whatever the selected provider returned for a given text), which is why it sits at this level rather than inside the presidio sub-block. |
timeout | string | Timeout is the per-request timeout applied to calls into the NER provider. Uses Go duration format (e.g., "500ms", "1s", "5s"). The default (5s) is sized for Microsoft Presidio's spaCy engine — the only provider this CRD wires today — running on the request-body sizes the gateway sees in practice: 100-500ms per /analyze on a warm replica with dedicated CPU, 200-3000ms when the pod shares a CPU-bound node, and several seconds on the 200-500 KB bodies typical of agent CLI traffic (Claude Code, Gemini CLI). 5s sits above warm-replica p99 and the contended/large-body case while staying short enough that the gateway fail-closes in seconds when Presidio is stuck. Operators with strict latency SLOs can lower this: a tighter timeout trips the breaker sooner, which is the right behaviour when the SLO is the dominant pressure. ProcessorConfig.MessageTimeout must remain strictly greater than this value or Envoy aborts the ext-proc stream before NER returns; the CEL rule on ProcessorConfig enforces that ordering at admission. default "5s" · pattern ^([0-9]+([.][0-9]+)?(ns|us|ms|s|m|h))+$ |
typerequired | string | Type selects the provider implementation. Currently only `presidio` is supported; the enum is narrow on purpose so unsupported values fail at admission instead of producing a silently-empty processor config. enum: presidio |
spec.processor.nerProvider.circuitBreaker
CircuitBreaker overrides the failure threshold and reset timeout for the provider call path.
| Field | Type | Description |
|---|---|---|
failureThreshold | integer | FailureThreshold is the consecutive failure count that trips the breaker. format int32 · min 1 |
resetTimeout | string | ResetTimeout is how long the breaker stays open before allowing a probe request. Uses Go duration format (e.g., "30s", "1m"). pattern ^([0-9]+([.][0-9]+)?(ns|us|ms|s|m|h))+$ |
spec.processor.nerProvider.presidio
Presidio carries Presidio-specific settings. Required when type=presidio.
| Field | Type | Description |
|---|---|---|
allowlist | string[] | Allowlist defines text strings that should be ignored by Presidio detection. Useful for reducing false positives on known company names or product names. Honoured server-side by Presidio's AllowListRecognizer at analysis time; allowlisted strings never appear in match results and do not bias scoring of nearby tokens. |
allowlistMatch | string | AllowlistMatch selects how Presidio compares Allowlist entries to detected text. "Exact" (default) is case-insensitive equality on the matched value. "Regex" treats each Allowlist entry as a regular expression evaluated by Presidio against the matched value, so patterns like `acme-[a-z0-9]+` skip the per-variant enumeration that exact mode forces. Inert when Allowlist is empty. default "Exact" · enum: Exact | Regex |
autoscaling | object | Autoscaling selects how the Presidio Deployment is scaled. When nil/Operator (the default), the operator creates an HPA targeting the Deployment when MaxReplicas is set. When `External`, the operator skips HPA creation and stops re-emitting `replicas` on subsequent reconciles so an external autoscaler (Keda, KPA, vendor) can write to the Deployment without being reverted. When `None`, no HPA is created and `replicas` is owned by the operator (manual scaling via the CRD). |
entities | string[] | Entities is the list of Presidio entity types to detect. The default covers the common structured-PII entities (CREDIT_CARD, US_SSN, PHONE_NUMBER, EMAIL_ADDRESS, ABA_ROUTING_NUMBER, IBAN_CODE, IP_ADDRESS, US_ITIN, IN_AADHAAR, AU_TFN, UK_NHS) alongside the NER-model entities (PERSON, LOCATION, DATE_TIME). Extend via manifest to add additional Presidio built-ins or custom recognizers. See https://microsoft.github.io/presidio/supported_entities/ for the full list of supported built-in and custom entity types. default ["PERSON","LOCATION","DATE_TIME","CREDIT_CARD","US_SSN","PHONE_NUMBER","EMAIL_ADDRESS","ABA_ROUTING_NUMBER","IBAN_CODE","IP_ADDRESS","US_ITIN","IN_AADHAAR","AU_TFN","UK_NHS"] |
entityActions | object[] | EntityActions overrides the default action / placeholder / mode for a specific Presidio entity type (e.g. "US_SSN", "EMAIL_ADDRESS", "CREDIT_CARD"). Entries are keyed by entity name and layered on top of processor.defaults; entities without an entry use the global default. Entity names are not constrained to a closed set so custom Presidio recognizers are supported. Per-entity overrides are Presidio-vocabulary-specific, so this list lives in the provider sub-block rather than at the processor level. |
image | string | Image is the container image for the Presidio Analyzer deployment. When empty, the operator uses its `PRESIDIO_DEFAULT_IMAGE` env var (the operator Helm chart wires this from values.yaml with a digest pin), so most installs should leave this unset and let the chart pin the version. Override for air-gapped clusters or to consume a newer Presidio release without bumping the operator. If both this field and the operator's PRESIDIO_DEFAULT_IMAGE are empty the operator fails the Deployment reconcile with a clear error rather than silently using a mutable tag. minLength 1 |
imagePullPolicy | string | ImagePullPolicy defines the pull policy for the Presidio Analyzer image. default "IfNotPresent" · enum: Always | Never | IfNotPresent |
language | string | Language is the ISO 639-1 two-letter language code passed to Presidio's /analyze endpoint. Currently restricted to "en" because the vendored default_recognizers.yaml only declares supported_languages: [en], so a different language stamped onto a custom recognizer would either be silently dropped by Presidio's loader or wedge the rollout. The field is kept (rather than removed) so non-en support can be enabled by widening the Enum once the vendored defaults grow corresponding language coverage and we have e2e tests for that path. default "en" · enum: en |
maxReplicas | integer | MaxReplicas is the upper bound for HPA scaling of the Presidio Analyzer deployment. When set, the operator creates a HorizontalPodAutoscaler targeting the Presidio Deployment. format int32 · min 1 |
recognizers | object[] | Recognizers declares user-defined Presidio Pattern recognizers that the operator materializes into a recognizers YAML and mounts into the Presidio Analyzer pod at startup. Each entry mirrors Presidio's PatternRecognizer shape (name, supportedEntity, patterns, optional contextWords and denyList). The ConfigMap is rendered alongside the upstream-vendored default recognizer set so built-ins continue to work; setting this field never disables predefined recognizers. Recognizers are Presidio-specific (numeric per-pattern scores, implicit-proximity context words, deny-lists) and do not generalize across DLP providers, which is why they live in the provider sub-block rather than at the processor level. The operator auto-unions every supportedEntity into Entities at reconcile time, so an entity declared on a recognizer does not also need to be added to spec.processor.nerProvider.presidio.entities. |
replicas | integer | Replicas is the number of Presidio Analyzer pods. When `autoscaling.mode` is `Operator` (default) or `None`, the operator owns this field and writes it on every reconcile. When `autoscaling.mode` is `External`, the operator does not write `spec.replicas` to the Deployment at all — an external scaler (Keda, KPA, vendor) owns the field, and this CRD value is silently ignored after the Deployment's first creation. Set this only as a manual scaling target in `Operator`/`None` modes. default 2 · format int32 · min 1 |
resources | object | Resources defines compute resource requirements for the Presidio Analyzer pods. The operator applies a runtime fallback of 500m/2 CPU + 1Gi/2Gi memory when this field is nil; the fallback accommodates the spaCy en_core_web_md model (~800MB resident). The defaults are not surfaced in the CRD schema (so kubectl explain does not show them) because they live in the controller, not in admission — set this field explicitly to pin observable values. |
scoreThreshold | integer | ScoreThreshold is the minimum confidence score as a percentage (0-100) for a Presidio detection to be acted upon. The operator converts this to a 0.0-1.0 float when calling the Presidio API (e.g., 50 becomes 0.5). Higher values reduce false positives. default 50 · format int32 · min 0 · max 100 |
targetCPUUtilization | integer | TargetCPUUtilization is the CPU utilization target for HPA scaling (percentage) of the Presidio Analyzer deployment. The operator applies a runtime fallback of 75 when this field is nil and MaxReplicas is set. No kubebuilder default is emitted because the paired CEL rule above (`has(targetCPUUtilization) -> has(maxReplicas)`) needs `nil` to be observable as "user did not set this", which a default-everything-to-75 would erase. format int32 · min 1 · max 100 |
spec.processor.nerProvider.presidio.autoscaling
Autoscaling selects how the Presidio Deployment is scaled. When nil/Operator (the default), the operator creates an HPA targeting the Deployment when MaxReplicas is set. When `External`, the operator skips HPA creation and stops re-emitting `replicas` on subsequent reconciles so an external autoscaler (Keda, KPA, vendor) can write to the Deployment without being reverted. When `None`, no HPA is created and `replicas` is owned by the operator (manual scaling via the CRD).
| Field | Type | Description |
|---|---|---|
mode | string | Mode selects who owns replicas. Operator (default) means the operator creates an HPA when MaxReplicas is set and hands replica ownership to that HPA; with MaxReplicas unset it re-emits `replicas` on every reconcile. External means the operator stays out of replicas entirely so an external autoscaler (e.g. Keda ScaledObject) can drive the Deployment. None disables operator-managed HPA but keeps `replicas` pinned to the CRD value (manual scaling). default "Operator" · enum: Operator | External | None |
spec.processor.nerProvider.presidio.entityActions[]
EntityActions overrides the default action / placeholder / mode for a specific Presidio entity type (e.g. "US_SSN", "EMAIL_ADDRESS", "CREDIT_CARD"). Entries are keyed by entity name and layered on top of processor.defaults; entities without an entry use the global default. Entity names are not constrained to a closed set so custom Presidio recognizers are supported. Per-entity overrides are Presidio-vocabulary-specific, so this list lives in the provider sub-block rather than at the processor level.
| Field | Type | Description |
|---|---|---|
entityrequired | string | Entity is the NER provider entity name to override (e.g. "US_SSN", "EMAIL_ADDRESS", "PHONE_NUMBER", "CREDIT_CARD", or a custom recognizer's supportedEntity). Matched case-sensitively against the provider's reported entity type. minLength 1 · maxLength 128 |
mode | string | Mode controls enforcement for this entity. "enforce" (default) applies the configured action; "shadow" detects and emits telemetry/logging but never blocks or redacts. default "enforce" · enum: enforce | shadow |
placeholder | string | Placeholder overrides the redaction placeholder for matches of this entity. Empty falls back to ProcessorConfig.Placeholder. maxLength 64 |
requestAction | string | RequestAction overrides the default action on the request path. enum: Block | Redact | LogOnly |
responseAction | string | ResponseAction overrides the default action on the response path. enum: Block | Redact | LogOnly |
spec.processor.nerProvider.presidio.recognizers[]
Recognizers declares user-defined Presidio Pattern recognizers that the operator materializes into a recognizers YAML and mounts into the Presidio Analyzer pod at startup. Each entry mirrors Presidio's PatternRecognizer shape (name, supportedEntity, patterns, optional contextWords and denyList). The ConfigMap is rendered alongside the upstream-vendored default recognizer set so built-ins continue to work; setting this field never disables predefined recognizers. Recognizers are Presidio-specific (numeric per-pattern scores, implicit-proximity context words, deny-lists) and do not generalize across DLP providers, which is why they live in the provider sub-block rather than at the processor level. The operator auto-unions every supportedEntity into Entities at reconcile time, so an entity declared on a recognizer does not also need to be added to spec.processor.nerProvider.presidio.entities.
| Field | Type | Description |
|---|---|---|
contextWords | string[] | ContextWords are tokens whose presence near a match boosts the match's confidence (Presidio's implicit-proximity context-word enhancement). Useful for relaxing a low-score pattern that would otherwise fall below scoreThreshold while still suppressing matches in unrelated text. |
denyList | string[] | DenyList is a vocabulary of terms that match as the SupportedEntity even when no pattern fires. Each entry is regex-escaped by Presidio, joined into a single alternation, and evaluated under `re.DOTALL | re.MULTILINE | re.IGNORECASE` with word-boundary anchors `(?:^|(?<=\W))(...)(?:(?=\W)|$)`. So matches are case-insensitive, word-boundary substring matches anywhere in the input — NOT exact-string equality: - `"PROJ-1"` fires on `"PROJ-1"`, `"proj-1"`, `" PROJ-1."`. - `"PROJ-1"` does NOT fire on `"PROJ-12"` (`1` is followed by `2`, not `\W`). Useful for fixed vocabularies (titles, internal product code names) where regex would be overkill. Storage warning: the rendered recognizers YAML lives in a ConfigMap (`{cr-name}-presidio-recognizers`). Anyone with `get configmap` in the namespace, including most read-only RBAC roles, can read every entry verbatim. Avoid putting sensitive content in DenyList[]: internal product code names, employee names, or any vocabulary whose disclosure carries the same risk as disclosing the matched data itself. |
namerequired | string | Name identifies the recognizer in Presidio's logs and metrics. Must be unique within the recognizers list. The operator does not impose the upstream PascalCase recognizer-name convention because Presidio accepts any unique string for custom recognizers. The leading character is constrained to alphanumeric so the name remains safe to use as a CLI flag value, filename component, or label value (a leading `-` breaks flag parsing; a leading `.` fails kube admission on metadata.name; a leading `_` is rejected by some label validators). pattern ^[A-Za-z0-9][A-Za-z0-9._-]*$ · minLength 1 · maxLength 128 |
patterns | object[] | Patterns is the list of regex patterns this recognizer matches on. Each pattern carries its own confidence score; matches are surfaced at the highest pattern score, optionally boosted by ContextWords. |
supportedEntityrequired | string | SupportedEntity is the entity label this recognizer emits on a match (e.g. "INTERNAL_ACCOUNT_ID"). Convention is upper snake case to match Presidio's built-in entity vocabulary. The operator auto-unions every SupportedEntity into the Entities list at reconcile time, so the same name does not also need to be declared there. Custom recognizers may NOT target an entity emitted by one of Presidio's built-in recognizers (e.g. EMAIL_ADDRESS, US_SSN). The upstream registry has no name-dedup or replace-by-name path — declaring a custom recognizer with a built-in supportedEntity does not shadow the built-in; both fire independently. Presidio's analyze-time dedup collapses identical-span same-entity hits but keeps partial overlaps as separate `RecognizerResult` rows, so stacking on a built-in produces unpredictable results. The rejection list mirrors `internal/guardrails/provider/presidio`'s BuiltinEntities; a drift test in that package fails CI when the two views disagree. Pick a unique entity name, or wait for upstream to ship `replace_recognizer` semantics. pattern ^[A-Z][A-Z0-9_]*$ · minLength 1 · maxLength 128 |
spec.processor.nerProvider.presidio.recognizers.patterns[]
Patterns is the list of regex patterns this recognizer matches on. Each pattern carries its own confidence score; matches are surfaced at the highest pattern score, optionally boosted by ContextWords.
| Field | Type | Description |
|---|---|---|
namerequired | string | Name identifies the pattern in Presidio's logs and metrics. Must be unique within the parent recognizer's patterns list. minLength 1 · maxLength 128 |
regexrequired | string | Regex is the pattern Presidio evaluates against incoming text. The operator pre-compiles each pattern with Go's RE2 engine before applying the recognizers ConfigMap, so non-compiling patterns fail reconcile rather than wedging the Presidio rollout. Presidio itself runs the third-party Python `regex` package (imported as `re`), whose syntax is a superset of RE2 — patterns using lookaround or backreferences are accepted by the engine but rejected by the operator's pre-compile because the YAML loader can't express them anyway; use ContextWords for proximity matching instead. RE2 acceptance does NOT imply linear-time evaluation in Presidio: `regex` is a backtracking engine, so polynomial patterns like `(a+)+x` compile under RE2 syntax but can blow up on crafted inputs. The operator's actual ReDoS guard is the per-call `REGEX_TIMEOUT_SECONDS` env var on the Presidio container, which the operator pins to a tight default (5s) so a bad pattern caps a single request rather than the worker. The 1024-character bound matches the project's regex length limit on other custom-pattern surfaces. minLength 1 · maxLength 1024 |
score | integer | Score is the pattern's confidence as a percentage (0-100). The operator converts this to the 0.0-1.0 float Presidio expects when rendering the recognizers YAML. Combines with ContextWords boosting and PresidioConfig.ScoreThreshold filtering. default 50 · format int32 · min 0 · max 100 |
spec.processor.nerProvider.presidio.resources
Resources defines compute resource requirements for the Presidio Analyzer pods. The operator applies a runtime fallback of 500m/2 CPU + 1Gi/2Gi memory when this field is nil; the fallback accommodates the spaCy en_core_web_md model (~800MB resident). The defaults are not surfaced in the CRD schema (so kubectl explain does not show them) because they live in the controller, not in admission — set this field explicitly to pin observable values.
| Field | Type | Description |
|---|---|---|
limits | object | Limits defines the maximum resources allowed. |
requests | object | Requests defines the minimum resources required. |
spec.processor.nerProvider.presidio.resources.limits
Limits defines the maximum resources allowed.
| Field | Type | Description |
|---|---|---|
cpu | string | CPU is the CPU resource quantity (e.g., "500m", "2"). |
memory | string | Memory is the memory resource quantity (e.g., "512Mi", "2Gi"). |
spec.processor.nerProvider.presidio.resources.requests
Requests defines the minimum resources required.
| Field | Type | Description |
|---|---|---|
cpu | string | CPU is the CPU resource quantity (e.g., "500m", "2"). |
memory | string | Memory is the memory resource quantity (e.g., "512Mi", "2Gi"). |
spec.processor.nerProvider.resultCache
ResultCache opts this gateway's NER scan results into the shared Redis/Valkey cache. Provider-agnostic (it caches whatever the selected provider returned for a given text), which is why it sits at this level rather than inside the presidio sub-block.
| Field | Type | Description |
|---|---|---|
enabled | boolean | Enabled turns the cache on. Off by default: the cache is a latency optimization, and an install that never opts in must keep the main-processor pod template it had before this field existed (a stamped backend address would otherwise force a rolling restart on upgrade). default false |
ttl | string | TTL is how long a cached scan result lives. Defaults to 24h when unset. Entries cannot go stale: the cache key is content- and config-addressed, so anything that would change the answer changes the key. This is therefore purely a memory and disclosure-window knob, never a correctness one — which is what makes a long default defensible, since agentic sessions routinely outlive a shorter window and a session resumed after a break should still be warm. It rides the watched scanning ConfigMap rather than pod env, and the operator deliberately keeps it out of the main-processor's checksum/config annotation, so retuning it hot-reloads: it rolls no pod, rebuilds no NER provider, and resets no circuit-breaker state. That last point is the reason for the care — a retune during a Presidio outage must not reopen the breaker and re-herd a down backend into 403s under the default fail-closed action. Footprint, for sizing: an entry holds entity types and byte offsets and never the scanned text, so entry size is independent of how large the scanned turn was. Budget roughly 250 bytes per entry with Redis overhead, times the number of DISTINCT turns in the window. pattern ^([0-9]+([.][0-9]+)?(ns|us|ms|s|m|h))+$ · maxLength 32 |
spec.processor.resources
Resources defines compute resource requirements for the guardrails pods.
| Field | Type | Description |
|---|---|---|
limits | object | Limits defines the maximum resources allowed. |
requests | object | Requests defines the minimum resources required. |
spec.processor.resources.limits
Limits defines the maximum resources allowed.
| Field | Type | Description |
|---|---|---|
cpu | string | CPU is the CPU resource quantity (e.g., "500m", "2"). |
memory | string | Memory is the memory resource quantity (e.g., "512Mi", "2Gi"). |
spec.processor.resources.requests
Requests defines the minimum resources required.
| Field | Type | Description |
|---|---|---|
cpu | string | CPU is the CPU resource quantity (e.g., "500m", "2"). |
memory | string | Memory is the memory resource quantity (e.g., "512Mi", "2Gi"). |
spec.processor.tls
TLS overrides the mTLS settings for the Envoy-to-main-processor hop. Setting it also opts this gateway into that mTLS regardless of the operator's default, which is how a CR asked for mTLS before the operator carried a chart-level switch (certManager.processorTLS.enabled). Prefer leaving this unset and enabling mTLS on the operator chart, so the issuer is named once for the whole platform: omitting it means "no per-gateway override", NOT "mTLS off". Do not add +nullable here. Every field below is optional, so an author opting in with a fully empty object can end up submitting a JSON null instead of `{}` — that is how the staging manifests broke. Marking null valid stores it, and a stored null unmarshals back to a nil pointer that reads as "no override" while the author meant "opt in": plaintext on a hop carrying full request and response bodies. Non-nullable makes a null a rejection on server-side apply (Flux, `kubectl apply --server-side`) and a prune-to-absent on a client-side create or update, so it is never stored as a silent off. Pinned by test-integration/processor_tls_validation_test.go.
| Field | Type | Description |
|---|---|---|
issuerRef | object | IssuerRef configures the cert-manager issuer for the main-processor and guardrails-adapter serving certs. When omitted, the operator falls back to its chart-configured default issuer (certManager.defaultIssuer on the operator chart). Reconciliation fails closed if neither the CR nor the operator default supplies an issuer name. |
spec.processor.tls.issuerRef
IssuerRef configures the cert-manager issuer for the main-processor and guardrails-adapter serving certs. When omitted, the operator falls back to its chart-configured default issuer (certManager.defaultIssuer on the operator chart). Reconciliation fails closed if neither the CR nor the operator default supplies an issuer name.
| Field | Type | Description |
|---|---|---|
kind | string | Kind is either "Issuer" or "ClusterIssuer". default "ClusterIssuer" · enum: Issuer | ClusterIssuer |
namerequired | string | Name of the Issuer or ClusterIssuer. |
spec.providers[]
Providers is the list of LLM provider backends.
| Field | Type | Description |
|---|---|---|
credentialsrequired | object | Credentials defines how to authenticate to the upstream provider. |
endpointrequired | object | Endpoint defines the upstream provider endpoint. |
namerequired | string | Name is the unique identifier for this provider. pattern ^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$ · maxLength 63 |
pathPrefix | string | PathPrefix customizes the upstream request path for an OpenAI-compatible backend; e.g. OpenRouter uses "/api/v1". Only valid when schema is OpenAI — the upstream prefix mechanism (AIServiceBackend schema.prefix) only applies to the OpenAI translator. Registry-owned aliases (e.g. GeminiAIStudio) already carry their own prefix and must not set this field. pattern ^/?[A-Za-z0-9._~!$&'()*+,;=:@%/-]*$ · maxLength 128 |
schemarequired | string | Schema is the provider API schema. Mirrors the upstream Envoy AI Gateway AIServiceBackend schema enum (aigateway.envoyproxy.io/v1alpha1) verbatim except for two operator translations: GeminiAIStudio → {name: OpenAI, prefix: /v1beta/openai} for Google AI Studio's OpenAI-compatibility endpoint, and GoogleGenerativeLanguage targets the public Gemini API at generativelanguage.googleapis.com (v1beta/models/{model}:generateContent). GCPVertexAI talks to a regional Vertex AI aiplatform endpoint. AWSAnthropic serves Anthropic Claude models on AWS Bedrock via the InvokeModel API (native Anthropic Messages wire shape), distinct from AWSBedrock's Converse API: the InvokeModel path returns the Anthropic-native usage block the processor already prices, so Bedrock Claude traffic is billed under the `awsanthropic` pricing family. The exact upstream fork version this mirrors is pinned in FORK-VERSION; it is deliberately not restated here so this comment does not go stale on every fork bump. enum: OpenAI | Anthropic | AWSBedrock | AWSAnthropic | AzureOpenAI | GCPVertexAI | GeminiAIStudio | GoogleGenerativeLanguage |
spec.providers.credentials
Credentials defines how to authenticate to the upstream provider.
| Field | Type | Description |
|---|---|---|
projectName | string | ProjectName is the GCP project ID slug (the unique identifier used in API calls — e.g. "my-prod-7a3b" — not the human-readable display name). Required when Type=GCPCredentials, otherwise unused. Together with Region, this scopes Vertex requests to a project/region pair on the upstream BackendSecurityPolicy. The pattern matches GCP's documented project ID shape (lowercase letter followed by 5-29 chars of [a-z0-9-], ending in [a-z0-9]). pattern ^[a-z][a-z0-9-]{4,28}[a-z0-9]$ · maxLength 30 |
region | string | Region is the cloud provider region (for AWSCredentials, AzureCredentials, GCPCredentials). The pattern accepts lowercase canonical region tokens such as us-east-1, us-central1, europe-west4, eastus2 — and rejects whitespace, uppercase, and trailing newlines that would propagate into the upstream BackendSecurityPolicy. pattern ^[a-z][a-z0-9-]*[a-z0-9]$ · maxLength 63 |
secretRef | object | SecretRef references the Kubernetes Secret containing the credentials. |
typerequired | string | Type is the authentication mechanism. enum: APIKey | AnthropicAPIKey | AWSCredentials | AzureAPIKey | AzureCredentials | GCPCredentials |
spec.providers.credentials.secretRef
SecretRef references the Kubernetes Secret containing the credentials.
| Field | Type | Description |
|---|---|---|
key | string | Key within the Secret data. default "apiKey" |
namerequired | string | Name of the Secret. |
spec.providers.endpoint
Endpoint defines the upstream provider endpoint.
| Field | Type | Description |
|---|---|---|
hostnamerequired | string | Hostname of the upstream provider API. minLength 1 |
port | integer | Port of the upstream provider API. default 443 · format int32 |
spec.resilience
Resilience configures retry, circuit breaker, passive health checking, and upstream timeouts for all routes managed by this gateway. When nil, upstream errors pass through to the client unchanged (no retries).
| Field | Type | Description |
|---|---|---|
circuitBreaker | object | CircuitBreaker bounds connection and request concurrency to each upstream cluster. |
passiveHealthCheck | object | PassiveHealthCheck configures Envoy outlier detection so that hosts returning consecutive errors are ejected from the load balancing set. |
retry | object | Retry configures request retries on the upstream data path. |
upstreamConnection | object | UpstreamConnection tunes how Envoy recycles pooled connections to each upstream provider. Recycling Envoy's own connections on a shorter clock than the provider's edge prevents the provider from closing a connection mid-response, which otherwise surfaces to clients as an abrupt socket close. |
spec.resilience.circuitBreaker
CircuitBreaker bounds connection and request concurrency to each upstream cluster.
| Field | Type | Description |
|---|---|---|
maxConnections | integer | MaxConnections caps concurrent TCP connections to the upstream cluster. format int32 · min 1 |
maxParallelRequests | integer | MaxParallelRequests caps in-flight requests to the upstream cluster. format int32 · min 1 |
maxParallelRetries | integer | MaxParallelRetries caps in-flight retry attempts to the upstream cluster so a retry storm cannot exhaust upstream capacity. format int32 · min 1 |
maxPendingRequests | integer | MaxPendingRequests caps requests waiting for a ready connection. format int32 · min 1 |
spec.resilience.passiveHealthCheck
PassiveHealthCheck configures Envoy outlier detection so that hosts returning consecutive errors are ejected from the load balancing set.
| Field | Type | Description |
|---|---|---|
baseEjectionTime | string | BaseEjectionTime is how long an ejected host stays out of the load balancing set before rejoining. Accepts Go duration syntax. pattern ^([0-9]+([.][0-9]+)?(ns|us|ms|s|m|h))+$ |
consecutive5xxErrors | integer | Consecutive5xxErrors is the number of consecutive 5xx responses that cause a host to be ejected. format int32 · min 1 |
consecutiveGatewayErrors | integer | ConsecutiveGatewayErrors is the number of consecutive gateway errors (502, 503, 504) that cause a host to be ejected. format int32 · min 1 |
interval | string | Interval is the scan cadence for outlier detection. Accepts Go duration syntax (e.g. "10s", "1m"). pattern ^([0-9]+([.][0-9]+)?(ns|us|ms|s|m|h))+$ |
maxEjectionPercent | integer | MaxEjectionPercent is the upper bound on the percentage of hosts that outlier detection may eject at once, preventing mass ejection from collapsing the cluster. format int32 · min 1 · max 100 |
spec.resilience.retry
Retry configures request retries on the upstream data path.
| Field | Type | Description |
|---|---|---|
numAttemptsPerPriority | integer | NumAttemptsPerPriority is the number of attempts made against hosts at the same priority before failing over to the next priority group. Defaults to 1 (switch to the next priority on every retry). format int32 · min 1 · max 10 |
numRetries | integer | NumRetries is the maximum retry attempts per request. format int32 · min 0 · max 10 |
perRetryTimeout | string | PerRetryTimeout bounds each individual retry attempt. To bound the whole request lifetime across all attempts use spec.gateway.timeouts.requestTimeout or spec.routes[].timeouts.requestTimeout. pattern ^([0-9]+([.][0-9]+)?(ns|us|ms|s|m|h))+$ |
retryOn | object | RetryOn selects the upstream conditions that trigger a retry. At least one trigger or HTTP status code must be declared whenever NumRetries is greater than zero. |
spec.resilience.retry.retryOn
RetryOn selects the upstream conditions that trigger a retry. At least one trigger or HTTP status code must be declared whenever NumRetries is greater than zero.
| Field | Type | Description |
|---|---|---|
httpStatusCodes | integer[] | HTTPStatusCodes pairs with the retriable-status-codes trigger. The trigger has no effect unless at least one status code is listed here. |
triggers | string[] | Triggers is the list of retry conditions. Combined with HTTPStatusCodes by OR; a request is retried when any trigger matches. |
spec.resilience.upstreamConnection
UpstreamConnection tunes how Envoy recycles pooled connections to each upstream provider. Recycling Envoy's own connections on a shorter clock than the provider's edge prevents the provider from closing a connection mid-response, which otherwise surfaces to clients as an abrupt socket close.
| Field | Type | Description |
|---|---|---|
connectionIdleTimeout | string | ConnectionIdleTimeout is how long a pooled upstream connection may sit with no active requests before Envoy closes it. Closing idle connections locally avoids reusing one the provider's edge has already reaped. Maps to spec.timeout.http.connectionIdleTimeout on the BackendTrafficPolicy. Accepts the Envoy Gateway Duration form (e.g. "55s", "5m"); no fractional values or us/ns units. When unset, Envoy's 1h default applies. pattern ^([0-9]{1,5}(h|m|s|ms)){1,4}$ |
maxConnectionDuration | string | MaxConnectionDuration is the absolute lifetime of an upstream connection regardless of activity. On reaching it Envoy drains the connection gracefully (HTTP/2 GOAWAY: in-flight streams finish, no new streams open), so setting it below the provider's own max connection age prevents the provider from closing a connection mid-response. Maps to spec.timeout.http.maxConnectionDuration on the BackendTrafficPolicy. Accepts the Envoy Gateway Duration form (e.g. "10m", "30m"); no fractional values or us/ns units. When unset, Envoy leaves connection lifetime unbounded. pattern ^([0-9]{1,5}(h|m|s|ms)){1,4}$ |
spec.routes[]
Routes defines model routing rules. Maximum 120 entries — the hard ceiling of 15 collapsed rules times 8 matches per rule (see below); the practical ceiling is lower and depends on how many routes share a backend. The operator collapses routes that share the same backendRefs and timeout into ONE AIGatewayRoute rule with one match per model, chunked at 8 matches per rule — e.g. 40 model routes split across 3 providers of <=8 models each become 3 rules; a single backend of 20 models becomes 3 rules (8+8+4). GeminiNative (path-matched) routes and the no-match default route are never collapsed; each keeps its own rule. Gateway API caps HTTPRoute.spec.rules at 16, and the upstream AI Gateway controller always appends a route-not-found rule (returning 404 for unmatched traffic) as the final entry, consuming one slot — so the collapsed rule count must stay at or below 15, and each rule holds at most 8 matches, giving the 15*8=120 model ceiling. The reconciler enforces this at reconcile time (see the RoutesValid status condition) since the grouping is not CEL-expressible; a spec with too many distinct backend/timeout combinations sets RoutesValid=False and the AIGatewayRoute is not updated (retains last-known-good) rather than failing admission. Inserting or removing a route that changes which routes share a group shifts every subsequent group's rule index (rule/i), and therefore its RLS generic_key route scope — the same class of budget-counter reset that already applies to reordering routes today, just now scoped to the group rather than the individual route.
| Field | Type | Description |
|---|---|---|
backendRefsrequired | object[] | BackendRefs defines which providers handle matched requests. |
displayName | string | DisplayName is the human-readable label surfaced to model-discovery clients (e.g. Claude Code's model picker) via GET /v1/models `display_name`. When empty, the operator falls back to the route's Model id. maxLength 253 |
inputSchema | string | InputSchema declares the wire-format shape this route accepts from clients. When omitted, the route accepts OpenAI-shaped requests at /v1/chat/completions, /v1/embeddings, etc. — backwards-compatible with every existing AIGateway. Set to "GeminiNative" to accept Gemini-shaped requests at /v1beta/models/{model}:generateContent and :streamGenerateContent (e.g., from Gemini CLI in proxy mode). The gateway translates the request to the backend provider's schema; any provider schema is permitted as a backend. default "OpenAI" · enum: OpenAI | GeminiNative |
match | object | Match defines when this route applies. If omitted, this is the default route. |
namerequired | string | Name is a unique identifier for this route. maxLength 63 |
timeouts | object | Timeouts overrides the gateway-level total request cap for this specific route. Takes precedence over spec.gateway.timeouts. Like the gateway-level field, this is an opt-in absolute deadline on the entire request (including the streamed response) and is disabled by default — set it only when you want this route's requests to fail past a fixed wall-clock budget. Streaming bounds (idle + max-stream-duration) are configured gateway-wide via spec.gateway.streamTimeouts, not per route. Note: the operator also applies a default maxStreamDuration ceiling (15m); a per-route requestTimeout LONGER than the effective maxStreamDuration is pre-empted by that ceiling (the stream is cut first), so to allow longer requests you must raise BOTH this value and spec.gateway.streamTimeouts.maxStreamDuration. The operator surfaces a TimeoutConfigValid=False condition + Warning event when this happens. |
spec.routes.backendRefs[]
BackendRefs defines which providers handle matched requests.
| Field | Type | Description |
|---|---|---|
priority | integer | Priority for failover. Lower values are preferred. default 0 · format int32 |
providerrequired | string | Provider must match the name of a defined provider. |
weight | integer | Weight for weighted load balancing. default 1 · format int32 |
spec.routes.match
Match defines when this route applies. If omitted, this is the default route.
| Field | Type | Description |
|---|---|---|
modelrequired | string | Model is the model name to match (matched against x-ai-eg-model header). |
spec.routes.timeouts
Timeouts overrides the gateway-level total request cap for this specific route. Takes precedence over spec.gateway.timeouts. Like the gateway-level field, this is an opt-in absolute deadline on the entire request (including the streamed response) and is disabled by default — set it only when you want this route's requests to fail past a fixed wall-clock budget. Streaming bounds (idle + max-stream-duration) are configured gateway-wide via spec.gateway.streamTimeouts, not per route. Note: the operator also applies a default maxStreamDuration ceiling (15m); a per-route requestTimeout LONGER than the effective maxStreamDuration is pre-empted by that ceiling (the stream is cut first), so to allow longer requests you must raise BOTH this value and spec.gateway.streamTimeouts.maxStreamDuration. The operator surfaces a TimeoutConfigValid=False condition + Warning event when this happens.
| Field | Type | Description |
|---|---|---|
requestTimeout | string | RequestTimeout is the maximum total duration allowed for a single request, including the streamed response body. This is an absolute cap: the request is terminated when it elapses even if data is still flowing. Accepts the Envoy Gateway Duration form (e.g. "5m"). "0s" disables the cap. pattern ^([0-9]{1,5}(h|m|s|ms)){1,4}$ |
spec.webhooks[]
Webhooks registers external in-cluster receivers that validate request traffic (Validating) and observe responses (Observation). Each entry names a same-namespace Service destination authenticated with a projected, audience-bound Kubernetes ServiceAccount token. Sits alongside spec.audit and spec.journaling; the operator and main processor wire these in (see docs/plan/webhooks.md).
| Field | Type | Description |
|---|---|---|
authrequired | object | Auth configures how the gateway authenticates to the receiver. |
events | string[] | Events optionally filters which catalog events this webhook receives. When empty, the receiver gets the phase-default events. Receivers ignore events they do not recognize. |
failurePolicy | string | FailurePolicy decides the request outcome when the webhook call fails. default "Fail" · enum: Fail | Ignore |
includeRequestHeaders | string[] | IncludeRequestHeaders is a glob allowlist of request headers projected onto the envelope. A fixed reserved-header denylist always strips credential and identity carriers (and any X-Stacklok-* header) regardless of this allowlist. |
includeResponseBody | boolean | IncludeResponseBody opts this response-phase webhook into receiving the assembled response body on the response.completed event. The body rides in result.body as structured JSON, always a JSON array: a streamed SSE response is the array of its data: event payloads, each kept verbatim in its provider-native shape (no reassembly into a logical completion); a non-streamed response is its single JSON object wrapped as a one-element array. Any Content-Encoding is decoded before assembly. Off by default: response bodies are large and carry un-redacted model output, so capture is an explicit per-webhook opt-in. The aggregate byte cap is a deployment-level knob (AIGW_WEBHOOK_MAX_RESPONSE_BODY_CAPTURE on the main-processor, set via the operator chart), not a CRD field. Has no effect on request-phase webhooks. default false |
includeResponseHeaders | string[] | IncludeResponseHeaders is a glob allowlist of response headers projected onto response-phase envelopes. The same reserved-header denylist applies. |
includeSubjectClaims | string[] | IncludeSubjectClaims is an allowlist of the caller's validated OIDC claim names projected onto subject.claims, on request- and response-phase envelopes alike. Only scalar string claims are eligible (the wire type is an object of strings); array, object, and numeric claims are never projected. Empty — the default — omits the claims block entirely. Entries are EXACT claim names, matched case-sensitively; the single wildcard "*" projects every eligible claim. Unlike the header allowlists above this is not glob-matched, because JWT claim names are case-sensitive and may contain '/' (namespaced claims such as `https://example.com/roles`) — see ProjectClaims in internal/webhooks. Off by default because a claim set is caller identity: it routinely carries email, name, and preferred_username, which a receiver has no business seeing unless it was registered to act on them. Name the specific claims a receiver needs (`iss`, `sub`, whichever claim its identity lookup keys on) rather than reaching for "*". |
namerequired | string | Name uniquely identifies this webhook within the AIGateway. Uniqueness is enforced by the API server (listType=map keyed on name). Keep this literal in sync with GuardrailsWebhookName, BudgetsAdmissionWebhookName, BudgetsUsageWebhookName, and BudgetsCaptureWebhookName (CEL markers cannot reference Go identifiers). pattern ^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$ · minLength 1 · maxLength 63 |
phaserequired | string | Phase is the request-lifecycle point at which this webhook fires. enum: Request | Response |
servicerequired | object | Service is the in-cluster receiver destination, in the same namespace as the AIGateway. Modeled on admissionregistration.k8s.io/v1.ServiceReference. |
timeoutSeconds | integer | TimeoutSeconds bounds a single webhook call. default 5 · format int32 · min 1 · max 120 |
typerequired | string | Type is the contract this webhook participates in. enum: Validating | Observation |
spec.webhooks.auth
Auth configures how the gateway authenticates to the receiver.
| Field | Type | Description |
|---|---|---|
serviceAccountToken | object | ServiceAccountToken configures projected, audience-bound ServiceAccount token authentication. Required when Type is ServiceAccountToken. |
typerequired | string | Type selects the authentication mode. default "ServiceAccountToken" · enum: ServiceAccountToken |
spec.webhooks.auth.serviceAccountToken
ServiceAccountToken configures projected, audience-bound ServiceAccount token authentication. Required when Type is ServiceAccountToken.
| Field | Type | Description |
|---|---|---|
audiencerequired | string | Audience the receiver expects in the projected token. Must be non-empty. minLength 1 · maxLength 253 |
expirationSeconds | integer | ExpirationSeconds is the requested token lifetime. The kubelet clamps to a cluster minimum (commonly 600s); omitting it takes the kubelet default. format int64 · min 600 |
spec.webhooks.service
Service is the in-cluster receiver destination, in the same namespace as the AIGateway. Modeled on admissionregistration.k8s.io/v1.ServiceReference.
| Field | Type | Description |
|---|---|---|
namerequired | string | Name of the receiver Service. minLength 1 · maxLength 253 |
path | string | Path is the HTTP path the envelope is POSTed to. maxLength 511 |
port | integer | Port is the Service port the receiver serves HTTPS on. Defaults to 443 when omitted, matching the convention on the provider endpoint port. default 443 · format int32 · min 1 · max 65535 |
status
AIGatewayStatus defines the observed state of the AI Gateway.
| Field | Type | Description |
|---|---|---|
conditions | object[] | Conditions represent the latest available observations of the AIGateway's state. |
endpoint | string | Endpoint is the gateway's externally-reachable endpoint URL. |
observedGeneration | integer | ObservedGeneration is the most recent generation observed by the controller. format int64 |
readyProviders | integer | ReadyProviders is the count of providers in a ready state. format int32 |
totalProviders | integer | TotalProviders is the total number of configured providers. format int32 |
webhooks | object[] | Webhooks reports the per-webhook provisioning-ping outcome. The operator fires a webhook.ping on registration and on Service-ref, audience, or token-expiration change, then records the result here so an operator can see, per webhook, whether the receiver accepted the audience-bound token. The dispatcher reads this to decide whether real traffic defers to the per-webhook failurePolicy while a probe is failing. The aggregate WebhooksReady condition summarizes these for humans. |
status.conditions[]
Conditions represent the latest available observations of the AIGateway's state.
| Field | Type | Description |
|---|---|---|
lastTransitionTimerequired | string | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format date-time |
messagerequired | string | message is a human readable message indicating details about the transition. This may be an empty string. maxLength 32768 |
observedGeneration | integer | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format int64 · min 0 |
reasonrequired | string | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. pattern ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ · minLength 1 · maxLength 1024 |
statusrequired | string | status of the condition, one of True, False, Unknown. enum: True | False | Unknown |
typerequired | string | type of condition in CamelCase or in foo.example.com/CamelCase. pattern ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ · maxLength 316 |
status.webhooks[]
Webhooks reports the per-webhook provisioning-ping outcome. The operator fires a webhook.ping on registration and on Service-ref, audience, or token-expiration change, then records the result here so an operator can see, per webhook, whether the receiver accepted the audience-bound token. The dispatcher reads this to decide whether real traffic defers to the per-webhook failurePolicy while a probe is failing. The aggregate WebhooksReady condition summarizes these for humans.
| Field | Type | Description |
|---|---|---|
lastProbeTime | string | LastProbeTime is when the operator last fired a ping for this webhook. format date-time |
message | string | Message is a human-readable explanation of the last ping outcome. |
namerequired | string | Name is the webhook this status entry describes (matches spec.webhooks[].name). |
observedAudience | string | ObservedAudience is the auth.serviceAccountToken.audience the last ping was bound to, so an audience change is visible as a status diff. |
probeSucceededrequired | boolean | ProbeSucceeded is true when the most recent webhook.ping returned a 2xx. False means the last ping returned non-2xx, a transport error, or a token the receiver rejected; while false, real traffic defers to failurePolicy. |
reason | string | Reason is a one-word CamelCase summary (ProbeSucceeded / ProbeFailed). |
Related resources
Referenced by:
- AIPolicy - via
spec.gatewayRef