Skip to content

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 ​

FieldTypeDescription
typelitellm | waegGateway implementation. Default litellm.
waegobjectWäg-only wiring (topology, ClickHouse/Redis planes, OpenFGA). Required when type: waeg
secretsRefLocalRef (optional)Same-namespace SecretsManagement to wait for. Leave it out to use plain Kubernetes Secrets you manage yourself — see secretsRef
databaseobject (required)Where LiteLLM stores its state
instanceobjectImage/tag, replicas, resources, master/salt keys, SSO
organizationobjectLiteLLM organization + budgets
teams[]listTeams with per-team budgets
models[]listThe model catalog
guardrailRefs[]ObjectRefGuardrail CRs to wire into this gateway (licensed guardrail; emitted as LiteLLMGuardrail via each guardrail's license-gate proxy)
observabilityRefObjectRefAn Observability for trace export (auto-wired if licensed)
observabilityobjectManual Langfuse callback fallback
ssoobjectOIDC/SSO login for the LiteLLM Admin UI (see SSO)
enableEntraSSOboolDeprecated — use sso with provider: azure-entra

database ​

FieldDescription
modepostgresCluster (reference a PostgresCluster) or external
postgresClusterRefThe cluster to use (mode postgresCluster)
databaseNameDatabase inside the shared cluster
connectionSecretRefDATABASE_URL Secret (mode external)

models[] ​

FieldDescription
name / modelName / modelDisplay name, LiteLLM model name, and provider model id
refSecretKeyKey in the model-credentials Secret holding the API key
credentials.apiBaseProvider endpoint, for any provider that needs an explicit one (Azure OpenAI / AI Foundry, self-hosted, a proxy)
credentials.apiVersionProvider API version, when the provider requires one (e.g. Azure OpenAI / AI Foundry)
rpm / tpm / timeout / maxTokensPer-model limits (LiteLLM only)
provider / providerName / modelId / fallbacks / weightWä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.

FieldTypeDescription
enabledboolTurn on background health checks. Default false (disabled).
intervalSecondsintSeconds between checks (LiteLLM default 300). Only applies when enabled: true.
yaml
instance:
  healthCheck:
    enabled: true
    intervalSeconds: 300

Maps 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 the litellm database and role, generates the password, and injects the credentials Secret. No manual secret needed.
  • external — supply a connectionSecretRef holding a DATABASE_URL.

Langfuse trace export ​

  • With the auto-wiring feature and an observabilityRef, the operator wires LiteLLM's Langfuse callback (host + public/secret keys) from the referenced Observability so 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} (keys publicKey / 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).

FieldDescription
issuerURLOIDC issuer / discovery base URL
clientSecretRef.nameSecret holding the OAuth client (keys default to client-id / client-secret, overridable with clientIDKey / clientSecretKey)
providergeneric-oidc (default), azure-entra, google, or okta
tenantIDDirectory/tenant ID (for azure-entra)
authorizationEndpoint / tokenEndpoint / userinfoEndpointExplicit endpoints — required for generic-oidc (LiteLLM does not perform discovery)
scopesRequested scopes (default openid, profile, email)
providerNameDisplay label
yaml
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.

FieldMaps to (litellm_jwtauth)
jwtAuth.enabledgeneral_settings.enable_jwt_auth
jwtAuth.publicKeyURLJWKS endpoint (JWT_PUBLIC_KEY_URL) — required: without it LiteLLM can't validate any token
jwtAuth.issuerexpected token issuer iss (JWT_ISSUER)
jwtAuth.audienceexpected token audience aud (JWT_AUDIENCE)
jwtAuth.userRolesJWTFielduser_roles_jwt_field — JWT claim holding the list of roles
jwtAuth.userAllowedRolesuser_allowed_roles — roles that map to an internal_user
jwtAuth.enforceRBACenforce_rbac — deny callers whose roles aren't allowed
jwtAuth.userRoleJWTFielduser_role_jwt_field (single role)
jwtAuth.userIDJWTFielduser_id_jwt_field — claim used as the user id (sub / oid / preferred_username)
jwtAuth.userIDUpsertuser_id_upsert — auto-create the LiteLLM user on first login
jwtAuth.teamIDsJWTFieldteam_ids_jwt_field
jwtAuth.adminJWTScopeadmin_jwt_scope
rolePermissions.<role>.models / .routesgeneral_settings.role_permissions
yaml
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 ​

yaml
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 ​

ConcernLiteLLMWäg
Storageone 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
Redisoptionalrequired once the API serves more than one replica (HA quotas)
Authorizationproxy roles / virtual keysOpenFGA — external, or a Postgres-backed one the operator deploys
Process modelone proxy DeploymentAllInOne, or Split (api + optional worker)
Catalogone model objectprovider (connection + credential) and model alias pointing at it
Budgetson the org/team objectsseparate org-rooted budget policies

spec.waeg ​

FieldDescription
topologyAllInOne (default) or Split (*-api plus an optional *-worker Deployment)
jobWorkersIn-process durable-job workers (AllInOne only)
workerThe dedicated worker Deployment (enabled, replicas, jobWorkers, resources) — requires topology: Split
storageModeWhere analytics live: split (default — ClickHouse) or single (the control-plane Postgres, no ClickHouse at all) — see Analytics storage mode
clickhouseThe analytics plane — required for storageMode: split, ignored and not required for single: mode: ref to a ClickHouseCluster, or mode: external with a connection Secret
clickhouseDatabaseThe ClickHouse database analytics are written to in split mode. Default waeg; the operator creates it on a managed or adopt cluster
redisThe HA-quota store: mode: ref to a RedisInstance, or external. Required for >1 API replica
openfgaExternal apiUrl / apiUrlSecretRef, or omit it and the operator deploys a Postgres-backed OpenFGA (image, replicas, resources, storeId, modelId)
autoscalingAPI HPA on CPU/memory (enabled, minReplicas, maxReplicas, targets). Queue-depth scaling needs KEDA and is not wired
artifactsJob/media volume: emptyDir (default), pvc (an existing claim), or none
bootstrapAdminSeeds the first console admin from one Secret (secretRef, emailKey, passwordKey)
dataEncryptionKeySecretRefThe key Wäg seals config revisions with (Wäg's counterpart to LiteLLM's salt key)
configYAMLOverrides the waeg.yaml bootstrap seed. Non-secret only — it is rendered into a ConfigMap
brandingConfigMapRefYour 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
defaultBrandingApply the built-in Navique theme pack when the gateway is Enterprise-licensed and no brandingConfigMapRef is set. Default true
modelAccessThe org-rooted model ACL (openByDefault, fallbackMode, grants[]). Requires spec.organization
scimSCIM v2 user provisioning (enabled, tokenSecretRef, defaultRole, defaultOrgID, orgSource, roleMap, orgMap) — see SCIM provisioning
jwtThe 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:

ModeAnalytics live inClickHouse needed
split (default)ClickHouse — spec.waeg.clickhouse must point at ityes
singlethe 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 ​

yaml
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 ignored

split — ClickHouse analytics ​

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

FieldDescription
providerProvider driver (openai, azure, anthropic, …). Defaults to the prefix of model (azure/gpt-4o → azure), else openai
providerNameNames the provider catalog entry, so several models can share one connection. Defaults to the driver
modelIdProvider-side model id. Defaults to model with its prefix stripped (azure/gpt-4o → gpt-4o)
fallbacksModel aliases to try when this one fails
weightBalances 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.

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

FieldDescription
enabledTurns the endpoints on. Default true; while off they answer 404
tokenSecretRefRequired. The bearer token your IdP presents. Key defaults to scim-token
defaultRoleConsole role a newly provisioned user receives: viewer, operator or admin
defaultOrgIDWäg organization provisioned users land in. Defaults to the Gateway's own organization when spec.organization is set
orgSourceWhere a provisioned user's organization comes from: waeg (the gateway decides), enterprise (the IdP payload) or none
roleMapIdP group name → Wäg console role. Sent as a whole-map replacement
orgMapIdP 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/ServiceProviderConfig

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

yaml
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": viewer

Data-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.jwtAuthWäg's EE jwt module
enabledenabled
publicKeyURLjwksUrl — the JWKS endpoint token-signing keys are fetched from
issuerissuer — the expected iss
audienceaudience — the expected aud
userIDJWTFieldsubjectClaim — which claim identifies the caller
spec.waeg.jwtDescription
appClaimClaim identifying the calling application. Default azp
appClaimFallbacksClaims tried in order when appClaim is absent. Whole-list replacement — an empty list clears Wäg's own defaults (appid, client_id)
tenantClaimClaim carrying the org/tenant (Wäg otherwise falls back to tenant_id, then tid)
requireRegisteredApplicationReject tokens whose application is not a registered WaegApplication
allowMasterKeyKeep the master key working on the data plane. Default true
allowVirtualKeysKeep virtual keys working on the data plane. Default true
insecureSkipVerifyAccept 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.

yaml
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 above

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

SituationResult
waeg.brandingConfigMapRef is set, licence has custom-brandingYour pack wins. The operator creates nothing of its own; CustomBranding=True
waeg.brandingConfigMapRef is set, licence lacks custom-brandingThe 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: falseOpt out — the console keeps Wäg's own livery
No instance.licenseSecretRefNothing 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.

yaml
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:

SettingWhy
roletransform 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
onErrorTaken 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
modeWä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.

FieldWhy
instance.saltKeyWäg has no salt key — use waeg.dataEncryptionKeySecretRef
instance.storePromptsInLogsNo equivalent runtime setting
instance.healthCheckWäg exposes fixed /healthz and /readyz probes and no configurable background loop
instance.jwtAuth — the claim mappings onlyJWT 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.rolePermissionsWäg has no LiteLLM role_permissions — use waeg.modelAccess for org-rooted model ACLs
models[].rpm / tpm / timeout / maxTokensThe 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.budgetDurationWä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: waeg with no spec.waeg or no spec.waeg.clickhouse
  • waeg.worker.enabled on topology: AllInOne
  • more than one API replica with no waeg.redis
  • waeg.modelAccess with no spec.organization

Example ​

yaml
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:

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

Open core under AGPL-3.0. Enterprise components are proprietary and license-gated.