Gateway
Scope: namespaced · Workload: LiteLLM via the litellm-operator, or Wäg via the waeg-operator
The AI gateway — the front door for model traffic, with models, teams, organizations, budgets, and optional Langfuse trace export.
spec.type picks the implementation. It defaults to litellm, so the rest of this page describes the LiteLLM gateway unless stated otherwise; see Wäg gateway (type: waeg) for the differences.
Spec
| Field | Type | Description |
|---|---|---|
type | litellm | waeg | Gateway implementation. Default litellm. |
waeg | object | Wäg-only wiring (topology, ClickHouse/Redis planes, OpenFGA). Required when type: waeg |
secretsRef | LocalRef (optional) | Same-namespace SecretsManagement to wait for. Leave it out to use plain Kubernetes Secrets you manage yourself — see secretsRef |
database | object (required) | Where LiteLLM stores its state |
instance | object | Image/tag, replicas, resources, master/salt keys, SSO |
organization | object | LiteLLM organization + budgets |
teams[] | list | Teams with per-team budgets |
models[] | list | The model catalog |
guardrailRefs | []ObjectRef | Guardrail CRs to wire into this gateway (licensed guardrail; emitted as LiteLLMGuardrail via each guardrail's license-gate proxy) |
observabilityRef | ObjectRef | An Observability for trace export (auto-wired if licensed) |
observability | object | Manual Langfuse callback fallback |
sso | object | OIDC/SSO login for the LiteLLM Admin UI (see SSO) |
enableEntraSSO | bool | Deprecated — use sso with provider: azure-entra |
database
| Field | Description |
|---|---|
mode | postgresCluster (reference a PostgresCluster) or external |
postgresClusterRef | The cluster to use (mode postgresCluster) |
databaseName | Database inside the shared cluster |
connectionSecretRef | DATABASE_URL Secret (mode external) |
models[]
| Field | Description |
|---|---|
name / modelName / model | Display name, LiteLLM model name, and provider model id |
refSecretKey | Key in the model-credentials Secret holding the API key |
credentials.apiBase | Provider endpoint, for any provider that needs an explicit one (Azure OpenAI / AI Foundry, self-hosted, a proxy) |
credentials.apiVersion | Provider API version, when the provider requires one (e.g. Azure OpenAI / AI Foundry) |
rpm / tpm / timeout / maxTokens | Per-model limits (LiteLLM only) |
provider / providerName / modelId / fallbacks / weight | Wäg-only catalog fields — see Wäg gateway |
When a model needs a custom endpoint and/or API version, set them on credentials — the operator passes them through to the gateway alongside the API key from refSecretKey. Keep apiBase the bare endpoint and put the version in apiVersion (don't bake ?api-version= into the URL). This is provider-agnostic: the operator emits whatever you supply and never special-cases a provider.
instance.healthCheck
Tunes LiteLLM's background model health checks. Disabled by default: GET /health probes the models on demand instead of running a background loop. Background checks add periodic upstream traffic and can incur cost or hit rate-limits on some providers, so opt in only when you want /health to serve cached results.
| Field | Type | Description |
|---|---|---|
enabled | bool | Turn on background health checks. Default false (disabled). |
intervalSeconds | int | Seconds between checks (LiteLLM default 300). Only applies when enabled: true. |
instance:
healthCheck:
enabled: true
intervalSeconds: 300Maps to the LiteLLMInstance generalSettings.backgroundHealthChecks / healthCheckInterval. When disabled the operator explicitly sets backgroundHealthChecks: false.
What it emits
The controller resolves the database, ensures the litellm-operator (waiting for its CRDs to be Established), then emits, in order:
LiteLLMInstance → LiteLLMOrganization → LiteLLMTeam(s)
→ LiteLLMCredential(s) → LiteLLMModel(s)(All in API group litellm.palena.ai/v1alpha1.)
Database wiring
postgresCluster— the operator resolves the referenced cluster, provisions thelitellmdatabase and role, generates the password, and injects the credentials Secret. No manual secret needed.external— supply aconnectionSecretRefholding aDATABASE_URL.
Langfuse trace export
- With the
auto-wiringfeature and anobservabilityRef, the operator wires LiteLLM's Langfuse callback (host + public/secret keys) from the referencedObservabilityso traces export automatically. - Without it (or against a community Langfuse that cannot mint project keys), create the project + key in the Langfuse UI and set
spec.observability.{host, callbackSecretRef}(keyspublicKey/secretKey).
See Auto-Wiring for the two gates involved.
SSO / OIDC login
spec.sso enables single sign-on for the LiteLLM Admin UI, translated to the LiteLLMInstance.spec.sso block. The OAuth client credentials come from a same-namespace Secret (via SecretsManagement — never inline).
| Field | Description |
|---|---|
issuerURL | OIDC issuer / discovery base URL |
clientSecretRef.name | Secret holding the OAuth client (keys default to client-id / client-secret, overridable with clientIDKey / clientSecretKey) |
provider | generic-oidc (default), azure-entra, google, or okta |
tenantID | Directory/tenant ID (for azure-entra) |
authorizationEndpoint / tokenEndpoint / userinfoEndpoint | Explicit endpoints — required for generic-oidc (LiteLLM does not perform discovery) |
scopes | Requested scopes (default openid, profile, email) |
providerName | Display label |
spec:
sso:
provider: generic-oidc
issuerURL: https://idp.example.com
authorizationEndpoint: https://idp.example.com/authorize
tokenEndpoint: https://idp.example.com/token
userinfoEndpoint: https://idp.example.com/userinfo
clientSecretRef: { name: gateway-oidc }Licensing
LiteLLM SSO is free for up to 5 users; full/unlimited SSO requires a LiteLLM Enterprise license. The Navique operator does not gate the field itself.
Redirect URL behind public access
LiteLLM builds the OAuth redirect_uri from PROXY_BASE_URL, which is derived from the Gateway's ingress. For SSO over public access, expose the Gateway on its public host with TLS — spec.ingress.enabled: true, spec.ingress.host: <public-host>, and spec.ingress.tls: true — so the callback resolves to https://<public-host>/sso/callback. Register exactly that URL with your IdP. Without ingress.tls: true the redirect falls back to a plain-http/in-cluster address and the OAuth callback fails. (status.endpoint on the underlying LiteLLMInstance always reports the in-cluster .svc URL — that is the operator's own admin-API address, not the SSO redirect base.)
The deprecated enableEntraSSO: true boolean still works when sso is unset — it synthesizes an azure-entra config reading the legacy entra-sso-credentials Secret. Prefer sso with provider: azure-entra.
JWT API authentication & RBAC
spec.instance.jwtAuth turns on JWT-based API authentication: LiteLLM validates a bearer JWT from your IdP on every request and maps its claims to roles/teams (distinct from sso, which is the browser login for the Admin UI). Pair it with spec.instance.rolePermissions to restrict which models each role may call.
| Field | Maps to (litellm_jwtauth) |
|---|---|
jwtAuth.enabled | general_settings.enable_jwt_auth |
jwtAuth.publicKeyURL | JWKS endpoint (JWT_PUBLIC_KEY_URL) — required: without it LiteLLM can't validate any token |
jwtAuth.issuer | expected token issuer iss (JWT_ISSUER) |
jwtAuth.audience | expected token audience aud (JWT_AUDIENCE) |
jwtAuth.userRolesJWTField | user_roles_jwt_field — JWT claim holding the list of roles |
jwtAuth.userAllowedRoles | user_allowed_roles — roles that map to an internal_user |
jwtAuth.enforceRBAC | enforce_rbac — deny callers whose roles aren't allowed |
jwtAuth.userRoleJWTField | user_role_jwt_field (single role) |
jwtAuth.userIDJWTField | user_id_jwt_field — claim used as the user id (sub / oid / preferred_username) |
jwtAuth.userIDUpsert | user_id_upsert — auto-create the LiteLLM user on first login |
jwtAuth.teamIDsJWTField | team_ids_jwt_field |
jwtAuth.adminJWTScope | admin_jwt_scope |
rolePermissions.<role>.models / .routes | general_settings.role_permissions |
spec:
instance:
licenseSecretRef: { name: litellm-enterprise-license, key: license }
jwtAuth:
enabled: true
publicKeyURL: https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys # REQUIRED (JWKS)
issuer: https://login.microsoftonline.com/<tenant>/v2.0
audience: <client-id>
userIDJWTField: sub # claim identifying the user (sub / oid / preferred_username)
userIDUpsert: true # auto-create the LiteLLM user on first login
userRolesJWTField: roles
userAllowedRoles: ["basic_user"]
enforceRBAC: true
rolePermissions:
internal_user:
models: ["anthropic-claude"]On a Wäg gateway the same jwtAuth block turns on Wäg's own data-plane JWT module, with the Wäg-specific knobs under spec.waeg.jwt — see Data-plane JWT authentication.
Enterprise only
enable_jwt_auth, enforce_rbac, and role_permissions are LiteLLM Enterprise features — set instance.licenseSecretRef to a valid LiteLLM Enterprise license or they have no effect. Setting any rolePermissions entry turns on general_settings.enforce_rbac so the restrictions are actually enforced.
Example
apiVersion: core.navique.com/v1alpha1
kind: Gateway
metadata:
name: gateway
namespace: forge-gateway
spec:
secretsRef: { name: forge-secrets }
database:
mode: postgresCluster
postgresClusterRef: { name: forge-pg, namespace: forge-data }
databaseName: litellm
instance:
image: { repository: ghcr.io/berriai/litellm, tag: v1.86.1 }
replicas: 1
masterKey: { autoGenerate: true }
saltKey: { autoGenerate: true }
observabilityRef: { name: observability, namespace: forge-langfuse }
organization:
name: navique-ag
maxBudget: 2000
budgetDuration: 30d
rpmLimit: 1000
tpmLimit: 200000
teams:
- { name: data-engineering, maxBudgetMonthly: 1000, budgetDuration: 30d }
models:
- name: gpt-5.4
modelName: gpt-5.4
model: azure/gpt-5.4 # provider/deployment id (here: an Azure OpenAI deployment)
refSecretKey: OPENAI_API_KEY
rpm: 300
tpm: 80000
timeout: 120
credentials:
apiBase: "https://forge-foundry.cognitiveservices.azure.com"
apiVersion: "2024-10-21"Wäg gateway (type: waeg)
Wäg is an alternative AI gateway, driven by the bundled waeg-operator (gateway.waeg.ai). It is a different product, not a LiteLLM clone, and this CRD does not pretend otherwise: everything below is a real difference, and anything Wäg cannot do is reported, never silently dropped.
What changes
| Concern | LiteLLM | Wäg |
|---|---|---|
| Storage | one Postgres (+ optional Redis) | dual-plane by default (storageMode: split): Postgres and ClickHouse. storageMode: single keeps analytics in the control-plane Postgres and needs no ClickHouse |
| Redis | optional | required once the API serves more than one replica (HA quotas) |
| Authorization | proxy roles / virtual keys | OpenFGA — external, or a Postgres-backed one the operator deploys |
| Process model | one proxy Deployment | AllInOne, or Split (api + optional worker) |
| Catalog | one model object | provider (connection + credential) and model alias pointing at it |
| Budgets | on the org/team objects | separate org-rooted budget policies |
spec.waeg
| Field | Description |
|---|---|
topology | AllInOne (default) or Split (*-api plus an optional *-worker Deployment) |
jobWorkers | In-process durable-job workers (AllInOne only) |
worker | The dedicated worker Deployment (enabled, replicas, jobWorkers, resources) — requires topology: Split |
storageMode | Where analytics live: split (default — ClickHouse) or single (the control-plane Postgres, no ClickHouse at all) — see Analytics storage mode |
clickhouse | The analytics plane — required for storageMode: split, ignored and not required for single: mode: ref to a ClickHouseCluster, or mode: external with a connection Secret |
clickhouseDatabase | The ClickHouse database analytics are written to in split mode. Default waeg; the operator creates it on a managed or adopt cluster |
redis | The HA-quota store: mode: ref to a RedisInstance, or external. Required for >1 API replica |
openfga | External apiUrl / apiUrlSecretRef, or omit it and the operator deploys a Postgres-backed OpenFGA (image, replicas, resources, storeId, modelId) |
autoscaling | API HPA on CPU/memory (enabled, minReplicas, maxReplicas, targets). Queue-depth scaling needs KEDA and is not wired |
artifacts | Job/media volume: emptyDir (default), pvc (an existing claim), or none |
bootstrapAdmin | Seeds the first console admin from one Secret (secretRef, emailKey, passwordKey) |
dataEncryptionKeySecretRef | The key Wäg seals config revisions with (Wäg's counterpart to LiteLLM's salt key) |
configYAML | Overrides the waeg.yaml bootstrap seed. Non-secret only — it is rendered into a ConfigMap |
brandingConfigMapRef | Your own Enterprise theme pack (JSON) the operator POSTs to the Admin API once Ready. Replaces the built-in Navique pack — licensed feature custom-branding; see Console branding |
defaultBranding | Apply the built-in Navique theme pack when the gateway is Enterprise-licensed and no brandingConfigMapRef is set. Default true |
modelAccess | The org-rooted model ACL (openByDefault, fallbackMode, grants[]). Requires spec.organization |
scim | SCIM v2 user provisioning (enabled, tokenSecretRef, defaultRole, defaultOrgID, orgSource, roleMap, orgMap) — see SCIM provisioning |
jwt | The Wäg-only data-plane JWT knobs (appClaim, appClaimFallbacks, tenantClaim, requireRegisteredApplication, allowMasterKey, allowVirtualKeys, insecureSkipVerify) — see Data-plane JWT authentication |
Postgres still comes from spec.database — the same field, resolved the same way. The operator assembles every connection string into one owned <gateway>-waeg-storage Secret and references it by key, so no credential-bearing URL ever lands in the CR.
Analytics storage mode
Wäg keeps its control plane (config, keys, orgs, teams, applications) in Postgres and its analytics plane (request logs, usage, spend) in a store selected by spec.waeg.storageMode:
| Mode | Analytics live in | ClickHouse needed |
|---|---|---|
split (default) | ClickHouse — spec.waeg.clickhouse must point at it | yes |
single | the same Postgres as the control plane (spec.database) | no |
split is this operator's default: it is what the Gateway has always emitted, and it is what holds up at volume. Upstream Wäg's own default is single, and that is its recommended starting point — which is exactly why this field exists. Requiring a ClickHouse for every gateway forced a dependency upstream had already dropped, and for a small or evaluation deployment it doubles the datastores you run, size and back up for no benefit.
With single the emitted WaegInstance carries no clickhouse block at all; spec.waeg.clickhouse is ignored and is not required.
Switching modes does not migrate analytics history
The two planes are separate stores. Changing storageMode on a running gateway points it at the other one — the analytics already written stay where they were and are no longer visible in the console. Neither the operator nor Wäg copies them across. Choose a mode before you collect data you care about, or export it first.
single — Postgres only
apiVersion: core.navique.com/v1alpha1
kind: Gateway
metadata:
name: waeg-gateway
namespace: forge-gateway
spec:
type: waeg
secretsRef: { name: forge-secrets }
database:
mode: postgresCluster
postgresClusterRef: { name: forge-pg, namespace: forge-data }
databaseName: waeg
instance:
replicas: 1
masterKey: { autoGenerate: true }
waeg:
storageMode: single # analytics land in forge-pg, next to the control plane
# no clickhouse block — none is needed, and one here would be ignoredsplit — ClickHouse analytics
spec:
type: waeg
secretsRef: { name: forge-secrets }
database:
mode: postgresCluster
postgresClusterRef: { name: forge-pg, namespace: forge-data }
databaseName: waeg
instance:
replicas: 2
masterKey: { autoGenerate: true }
waeg:
storageMode: split # the default; may be omitted
clickhouse: { mode: ref, ref: { name: forge-ch, namespace: forge-data } }
clickhouseDatabase: waeg # the default
redis: { mode: ref, ref: { name: forge-redis, namespace: forge-data } }Wäg creates only its tables, never its database. On a managed or adoptClickHouseCluster the operator declares that database under spec.databases and creates it for you; on an external cluster you must create it yourself, or the gateway exits at start-up with Database waeg does not exist.
Models: provider + alias
Wäg's catalog separates the upstream connection from the client-facing alias, so each entry in spec.models emits a WaegProvider and a WaegModel. Models sharing a driver share one provider.
| Field | Description |
|---|---|
provider | Provider driver (openai, azure, anthropic, …). Defaults to the prefix of model (azure/gpt-4o → azure), else openai |
providerName | Names the provider catalog entry, so several models can share one connection. Defaults to the driver |
modelId | Provider-side model id. Defaults to model with its prefix stripped (azure/gpt-4o → gpt-4o) |
fallbacks | Model aliases to try when this one fails |
weight | Balances this deployment against others on the same alias |
What it emits
WaegInstance → WaegOrganization → WaegBudget (org caps) → WaegTeam(s)
→ WaegProvider(s) → WaegModel(s) → WaegModelAccess(All in API group gateway.waeg.ai/v1alpha1.)
Enterprise licence
spec.instance.licenseSecretRef carries the gateway's Wäg Enterprise licence. It reaches the instance as WaegInstance.spec.secrets.licenseKey; the Secret key defaults to license.
Every Wäg Enterprise module is gated on it — SSO, SCIM, audit, CMEK, FIPS and branding alike — and enforcement is unconditional: an EE-linked gateway image with no licence answers 402 license_required. Running the Enterprise build is not on its own enough, so if a module reports LicenseRequired, this field is what is missing.
spec:
instance:
licenseSecretRef: { name: waeg-enterprise-license, key: license }The licence is referenced, never inlined: let your SecretsManagement backend materialize the Secret (ESO from Key Vault, or a SealedSecret) like every other credential. In a Stack it is declared once as spec.gateway.licenseSecretRef and passed through to the Gateway.
SSO and trace export
Both work, through paths that differ from LiteLLM's.
SSO (spec.sso) is applied through a WaegEnterpriseConfig CR, which configures Wäg's Enterprise modules via its admin API rather than through instance env. The operator reads the client id from your Secret and sets it as a plain field (a client id is public by construction — it travels in the browser's authorize URL), while the client secret stays a Secret reference, projected onto the pod as WAEG_EE_OIDC_CLIENT_SECRET. That is deliberate: the CR could also write the secret into the gateway's sealed config store, which would put it in the gateway's config revisions instead of leaving it in Kubernetes.
The redirect URI is derived as <public origin>/waeg/ui/v1/ee/sso/callback — the gateway rejects any value that does not contain that path. So SSO needs spec.ingress with a host; without one the operator warns rather than emit a callback that can never resolve. Register exactly that URL with your IdP.
Requires the Enterprise gateway build and a licence; the CR reports EnterpriseNotLinked or LicenseRequired per module if either is missing.
Trace export (observabilityRef / observability) maps onto WaegInstance.spec.observability.langfuse, with the project keys supplied as a Secret reference. Environment is the only declarative path the gateway has — it seeds its config file on first boot only and the console wins afterwards — so the sink is built once at start-up and a key change rolls the pods. The resolution order is the same as for LiteLLM: manual keys first, then licensed auto-wiring from a referenced Observability.
Needs a recent gateway
The WAEG_LANGFUSE_* variables arrived after gateway 1.0.0-rc.7. An older image reports LangfuseRequiresNewerGateway rather than accepting a setting that would never take effect.
SCIM provisioning
spec.waeg.scim turns on Wäg's SCIM v2 endpoints, so your IdP creates, updates and deprovisions console users directly instead of someone doing it by hand. SCIM is independent of spec.sso: it works on a gateway with no interactive login configured at all, which is the usual shape for a headless provisioning integration.
| Field | Description |
|---|---|
enabled | Turns the endpoints on. Default true; while off they answer 404 |
tokenSecretRef | Required. The bearer token your IdP presents. Key defaults to scim-token |
defaultRole | Console role a newly provisioned user receives: viewer, operator or admin |
defaultOrgID | Wäg organization provisioned users land in. Defaults to the Gateway's own organization when spec.organization is set |
orgSource | Where a provisioned user's organization comes from: waeg (the gateway decides), enterprise (the IdP payload) or none |
roleMap | IdP group name → Wäg console role. Sent as a whole-map replacement |
orgMap | IdP value → Wäg organization id. Whole-map replacement |
roleMap and orgMap are replaced wholesale on every apply rather than merged, so the gateway always holds exactly what is declared here — deleting an entry deletes the mapping.
Leaving defaultOrgID unset is fine when the Gateway has an organization: users land there rather than org-less, which matters because a user with no organization misses every org-rooted model-access grant.
The endpoints live under /waeg/admin/v1/ee/scim/v2:
/waeg/admin/v1/ee/scim/v2/Users
/waeg/admin/v1/ee/scim/v2/Groups
/waeg/admin/v1/ee/scim/v2/ServiceProviderConfigPoint your IdP's SCIM connector at that base URL with the bearer token from tokenSecretRef. Without the token every call answers 401.
The token is an env var, not a stored config value
The operator projects the token onto the gateway pods as WAEG_EE_SCIM_TOKEN rather than writing it into the gateway's sealed config store. That store can never be read back, so a copy written there would silently go stale the moment you rotate the Secret. As an env var, rotating the Secret and letting the pods roll is the whole rotation procedure.
Requires the gateway's Enterprise licence — without it the module answers 402 — and nothing else.
Not the platform's sso-scim feature
The AI Core licence has its own sso-scim feature. That is a separate, not-yet-implemented platform capability and has no bearing on the gateway module described here.
spec:
organization: { name: navique-ag }
instance:
licenseSecretRef: { name: waeg-enterprise-license, key: license }
waeg:
scim:
enabled: true
tokenSecretRef: { name: waeg-scim-token, key: scim-token }
defaultRole: viewer
orgSource: waeg
roleMap:
"AI Platform Admins": admin
"AI Platform Users": viewerData-plane JWT authentication
Wäg can validate a bearer JWT from your IdP on data-plane requests, so callers present a token instead of — or alongside — a key. It is switched on by the shared spec.instance.jwtAuth block (the same field LiteLLM uses) and tuned by the Wäg-only spec.waeg.jwt.
spec.instance.jwtAuth | Wäg's EE jwt module |
|---|---|
enabled | enabled |
publicKeyURL | jwksUrl — the JWKS endpoint token-signing keys are fetched from |
issuer | issuer — the expected iss |
audience | audience — the expected aud |
userIDJWTField | subjectClaim — which claim identifies the caller |
spec.waeg.jwt | Description |
|---|---|
appClaim | Claim identifying the calling application. Default azp |
appClaimFallbacks | Claims tried in order when appClaim is absent. Whole-list replacement — an empty list clears Wäg's own defaults (appid, client_id) |
tenantClaim | Claim carrying the org/tenant (Wäg otherwise falls back to tenant_id, then tid) |
requireRegisteredApplication | Reject tokens whose application is not a registered WaegApplication |
allowMasterKey | Keep the master key working on the data plane. Default true |
allowVirtualKeys | Keep virtual keys working on the data plane. Default true |
insecureSkipVerify | Accept unverified token signatures. Development only — Wäg refuses it in a production environment and alongside any hardening flag |
The hardening switches can stop the gateway booting
allowMasterKey: false and allowVirtualKeys: false lock the data plane to JWTs only, and Wäg refuses to start unless the rest is coherent: jwtAuth.enabled: true, a publicKeyURL (JWKS), an audience, and insecureSkipVerify: false. A half-configured hardening flag takes the gateway down rather than degrading — set both halves in the same apply.
allowVirtualKeys: false also stops every application key, the ChatUI's included. Change it deliberately.
The LiteLLM-only claim mappings are not applied. userRolesJWTField, userRoleJWTField, userAllowedRoles, enforceRBAC, userIDUpsert, teamIDsJWTField and adminJWTScope have no Wäg counterpart: Wäg validates the token but never projects roles or teams out of it — authorization comes from OpenFGA and registered applications. Setting them is reported on the FeaturesSupported condition and in status.unsupportedFeatures rather than silently dropped. Reach for spec.waeg.jwt.requireRegisteredApplication and spec.waeg.modelAccess instead.
Requires the gateway's Enterprise licence (entitlement jwt_api); without it the module answers 402.
spec:
instance:
licenseSecretRef: { name: waeg-enterprise-license, key: license }
jwtAuth:
enabled: true
publicKeyURL: https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys
issuer: https://login.microsoftonline.com/<tenant>/v2.0
audience: <client-id>
userIDJWTField: sub # -> Wäg's subjectClaim
waeg:
jwt:
appClaim: azp
tenantClaim: tid
requireRegisteredApplication: true
# allowMasterKey: false # only together with the four settings aboveConsole branding
An Enterprise-licensed Wäg console carries the Navique theme pack by default. The operator materializes the built-in pack into a ConfigMap it owns — <gateway-name>-branding, key theme-pack.json — and points the instance at it; the waeg-operator then POSTs it to the branding API.
| Situation | Result |
|---|---|
waeg.brandingConfigMapRef is set, licence has custom-branding | Your pack wins. The operator creates nothing of its own; CustomBranding=True |
waeg.brandingConfigMapRef is set, licence lacks custom-branding | The Navique pack is applied and CustomBranding=False (CustomBrandingUnlicensed) says why. The reference stays, so your pack applies as soon as the licence allows |
waeg.defaultBranding: false | Opt out — the console keeps Wäg's own livery |
No instance.licenseSecretRef | Nothing is created at all. Wäg refuses branding without a licence, so a ConfigMap here would only advertise styling the console can never show |
| Otherwise (the default) | The Navique pack is applied |
The ConfigMap is rewritten on every reconcile. The pack ships inside the operator, so an operator upgrade rolls the console forward instead of pinning whichever pack happened to be installed first — which also means hand-editing it is pointless, the next reconcile reverts it. To carry your own styling (white-label, licensed feature custom-branding), publish a ConfigMap and point brandingConfigMapRef at it — or use Gateway branding on the gateway's page in the management console, which validates the pack, previews it, and connects it (on the gateway's Stack when one manages it). A Stack passes spec.gateway.waeg.brandingConfigMapRef / defaultBranding through.
spec:
instance:
licenseSecretRef: { name: waeg-enterprise-license, key: license }
waeg:
defaultBranding: true # the default; false keeps Wäg's own livery
# brandingConfigMapRef: { name: my-theme-pack, key: theme-pack.json }ChatUI connects as an application
A ChatUI with gatewayRef pointing at a Wäg gateway is auto-wired, but through a different object than LiteLLM uses.
Wäg has no standalone virtual key: a key belongs to an application — an org-rooted tenancy object with its own model access, budgets and audit trail. So the operator registers the ChatUI as a WaegApplication and mints a WaegVirtualKey against it:
WaegApplication (the chat UI's identity) → WaegVirtualKey (its credential)The practical difference is attribution: spend, rate limits and audit entries land against "the chat UI" instead of an anonymous key, and the application appears in Wäg's EU AI Act deployer inventory with a declared purpose.
This requires spec.organization on the Gateway, because applications are org-rooted. Without it the ChatUI is refused with that reason rather than left waiting on a key that can never be minted — set spec.gateway.{url, apiKeySecretRef} instead to use a key you minted yourself.
Credential rotation works as it does for LiteLLM: advancing the rotation index mints a new key into a fresh Secret rather than overwriting in place. The application is stable across rotations — it is the identity, not the credential.
Encryption in transit
When the platform CA has issued a certificate, the operator turns on WaegInstance.spec.tls so the gateway serves HTTPS in-pod — traffic is encrypted to the pod, not only to the edge. The gateway has a single listener, so this switches everything together and the waeg-operator follows: probes, the Service's appProtocol, status.endpoint, the ServiceMonitor and the KEDA scale URL.
The platform CA bundle goes with it. The operator's own Admin API calls ride that connection, so without the CA they would fail x509 verification against the in-cluster CA and take branding and every product CR with them.
Needs gateway 1.0.0-rc.9 or newer
In-pod TLS only works from gateway 1.0.0-rc.9. Earlier images panicked at start-up whenever spec.tls was enabled (rustls found two crypto providers linked and none installed). The waeg-operator refuses to enable it on an older image and reports TLSUnsupportedGateway rather than handing you a crash loop, so pinning an older instance.image leaves the gateway on plaintext instead of breaking it. waeg-operator 1.3.0 defaults to rc.9, so the default path is fine.
Guardrails
spec.guardrailRefs works on a Wäg gateway, but the shape differs. LiteLLM takes one object per execution mode; Wäg takes three:
WaegGuardrailItem (one per Guardrail) → WaegGuardrailChain (orders them)
→ WaegGuardrailBinding (applies the chain)The operator emits all three at scope: platform, so no organization is required. The binding uses mode: floor, meaning it always runs — an org- or team-level binding added later in the Wäg console cannot switch off a guardrail the platform attached.
Three item settings are load-bearing, and the operator picks them for you:
| Setting | Why |
|---|---|
role | transform engines rewrite content. Configured as a policy item, Wäg treats the rewrite as a redact decision and mid-stream substitution silently does not happen. Catalog engines declare their own role |
onError | Taken from unreachableFallback (default fail_closed). Wäg defaults to fail-open, so an engine that deliberately answers 502 would otherwise have the original prompt forwarded to the provider |
mode | Wäg has a during_call phase LiteLLM lacks. A transform engine needs it, or streamed responses go un-rewritten. The bundled pseudonymizer runs all three phases on Wäg |
External guardrails need spec.waeg.path. Wäg POSTs to a URL given in full, whereas LiteLLM appends /beta/litellm_basic_guardrail_api — so the operator cannot guess where a user-managed endpoint serves the Wäg protocol. Without it the guardrail is skipped and an Event says so, rather than the operator guessing a path and making every scan fail while the gateway looks healthy. Catalog engines supply their own path (the pseudonymizer serves /v1/waeg/check).
spec.waeg on a Guardrail also carries optional role, timeoutMs, forwardIdentity and forwardSessionId. The last two matter for engines that keep per-session state: the pseudonymizer's name mappings stay consistent across turns only when Wäg forwards an identity, otherwise it falls back to the request id, which stitches a single request together but not a conversation.
What is not applied
A few fields on this CRD are still not applied to a Wäg gateway. Setting one does not break anything — it is listed in status.unsupportedFeatures, the FeaturesSupported condition goes False with reason UnsupportedByBackend, and an Event fires.
| Field | Why |
|---|---|
instance.saltKey | Wäg has no salt key — use waeg.dataEncryptionKeySecretRef |
instance.storePromptsInLogs | No equivalent runtime setting |
instance.healthCheck | Wäg exposes fixed /healthz and /readyz probes and no configurable background loop |
instance.jwtAuth — the claim mappings only | JWT auth itself is applied (see Data-plane JWT authentication). Only userRolesJWTField, userRoleJWTField, userAllowedRoles, enforceRBAC, userIDUpsert, teamIDsJWTField and adminJWTScope are refused: Wäg authorizes through OpenFGA and registered applications rather than by projecting roles out of the token |
instance.rolePermissions | Wäg has no LiteLLM role_permissions — use waeg.modelAccess for org-rooted model ACLs |
models[].rpm / tpm / timeout / maxTokens | The Wäg model object carries no rate or token caps, and the gateway has no per-model request timeout at all. A per-model limit is a WaegBudget with scope: model (an org-wide pool for the alias), which this operator does not emit |
organization.budgetDuration | Wäg budget policies have no period field |
Outside the Gateway spec, the identity resources are fully supported on Wäg: Identity, Organization and Team emit WaegUser, WaegOrganization, WaegTeam and the org- and team-scoped WaegBudget policies. Point them at this Gateway with type: gateway and they follow its spec.type — see Choosing the backend.
Two identity fields are not applied on Wäg: an Identity's teamRefs and budget. Wäg keeps membership on the team (list the address in the Team's members), and a per-user cap is a WaegBudget with scope: user. A Wäg Identity also gets a generated console password Secret and reports Ready only once the WaegUser is Synced — see Wäg accounts.
Rejected up front
Rather than emitting a resource that would wedge, the operator refuses these with Ready=False, reason InvalidTarget:
type: waegwith nospec.waegor nospec.waeg.clickhousewaeg.worker.enabledontopology: AllInOne- more than one API replica with no
waeg.redis waeg.modelAccesswith nospec.organization
Example
apiVersion: core.navique.com/v1alpha1
kind: Gateway
metadata:
name: waeg-gateway
namespace: forge-gateway
spec:
type: waeg
secretsRef: { name: forge-secrets }
database:
mode: postgresCluster
postgresClusterRef: { name: forge-pg, namespace: forge-data }
databaseName: waeg # never share a schema with a LiteLLM gateway
instance:
replicas: 2
masterKey: { autoGenerate: true }
licenseSecretRef: { name: waeg-enterprise-license, key: license } # unlocks every EE module
jwtAuth:
enabled: true
publicKeyURL: https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys
issuer: https://login.microsoftonline.com/<tenant>/v2.0
audience: <client-id>
userIDJWTField: sub
waeg:
topology: Split
worker: { enabled: true, replicas: 2, jobWorkers: 4 }
storageMode: split # the default; `single` drops ClickHouse entirely
clickhouse: { mode: ref, ref: { name: forge-ch, namespace: forge-data } }
redis: { mode: ref, ref: { name: forge-redis, namespace: forge-data } }
# openfga omitted -> the operator deploys a Postgres-backed OpenFGA
# defaultBranding defaults to true -> the Navique theme pack is applied
scim:
enabled: true
tokenSecretRef: { name: waeg-scim-token, key: scim-token }
defaultRole: viewer
roleMap: { "AI Platform Admins": admin }
jwt:
appClaim: azp
requireRegisteredApplication: true
modelAccess:
openByDefault: false
fallbackMode: deny
grants:
- models: [premium, economy]
organization: { name: navique-ag, maxBudget: 2000, rpmLimit: 1000 }
teams:
- { name: data-engineering }
models:
- name: gpt-5-4
modelName: premium
model: azure/gpt-5.4 # -> provider "azure", modelId "gpt-5.4"
refSecretKey: OPENAI_API_KEY
credentials: { apiBase: "https://forge-foundry.openai.azure.com/openai/v1" }
fallbacks: [economy]Check what was skipped:
kubectl -n forge-gateway get gateway waeg-gateway \
-o jsonpath='{.status.conditions[?(@.type=="FeaturesSupported")].message}'In a Stack
Stack.spec.gateway.type: waeg selects it for a whole Stack. The Stack then requires datastores.clickhouse.enabled (admission rejects it otherwise), auto-wires its own ClickHouse and Redis into the second plane, and names the gateway's logical database waeg so it never lands on a LiteLLM schema. Stack.spec.gateway.waeg passes through the topology/worker/OpenFGA/autoscaling knobs, plus scim and jwt; the Enterprise licence is declared once as Stack.spec.gateway.licenseSecretRef, and Stack.spec.gateway.jwtAuth turns data-plane JWT auth on.
Status
status.url exposes the gateway endpoint (public when ingress is enabled, else in-cluster), plus instance readiness, model/team counts, and database readiness. status.unsupportedFeatures lists any configured field the selected gateway type cannot honour (always empty for type: litellm); the FeaturesSupported condition mirrors it.
License notes
Teams, organizations, and budgets are part of the multi-tenancy feature; more than one Gateway requires a gateways limit above the Community default of 1. See Editions & Licensing.