diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index 4780f275..282f009e 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -44,7 +44,7 @@ projects: - id: toolhive repo: stacklok/toolhive - version: v0.31.0 + version: v0.32.0 # toolhive is a monorepo covering the CLI, the Kubernetes # operator, and the vMCP gateway. It also introduces cross- # cutting features that land in concepts/, integrations/, diff --git a/docs/toolhive/guides-cli/run-mcp-servers.mdx b/docs/toolhive/guides-cli/run-mcp-servers.mdx index 0ac0fd54..bb36a6b0 100644 --- a/docs/toolhive/guides-cli/run-mcp-servers.mdx +++ b/docs/toolhive/guides-cli/run-mcp-servers.mdx @@ -349,6 +349,42 @@ thv run --transport streamable-http --target-port -- http Check your MCP server's documentation for the required transport and port configuration. +### Restrict browser Origin headers + +ToolHive validates the HTTP `Origin` header on inbound proxy requests to protect +browser-based clients against DNS-rebinding attacks, per the +[MCP 2025-11-25 specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#security-warning). +Requests without an `Origin` header (such as IDE clients, CLI bridges, and +SDK-based MCP clients) pass through unchanged; only browser cross-origin +requests are subject to the check. + +When ToolHive binds to a loopback address (the default is `127.0.0.1`, but +`localhost` and `[::1]` also count), it derives a loopback-only allowlist +automatically and no configuration is required. + +When you bind to a non-loopback address with `--host` and a browser client +connects from a different origin, pass `--allowed-origins` to enable the check. +The flag is repeatable, matches each value exactly on scheme, host, and port, +and is also accepted by `thv proxy`. Without it, ToolHive logs a warning and +disables the check entirely, which isn't recommended for public binds: + +```bash +thv run --transport streamable-http \ + --host 0.0.0.0 \ + --allowed-origins https://my-web-app.example.com \ + +``` + +:::info[Legacy SSE transport CORS] + +As of v0.32.0, the legacy SSE transport no longer sends a wildcard +`Access-Control-Allow-Origin: *` header. Browser clients on a non-loopback +origin that used to rely on the wildcard must now be added to the +`--allowed-origins` allowlist, or migrated off SSE to the streamable-HTTP +transport. + +::: + ### Add a custom CA certificate In corporate environments with TLS inspection or custom certificate authorities, diff --git a/docs/toolhive/guides-vmcp/authentication.mdx b/docs/toolhive/guides-vmcp/authentication.mdx index 8aa1fe17..27aa49e2 100644 --- a/docs/toolhive/guides-vmcp/authentication.mdx +++ b/docs/toolhive/guides-vmcp/authentication.mdx @@ -513,6 +513,16 @@ for key generation steps. ::: +The issuer URL must use the `https://` scheme. The single exception is +`localhost`, which can use `http://` for local development. For in-cluster +deployments where traffic between the embedded auth server and other pods stays +on a trusted network (for example, an in-cluster service mesh), you can opt in +to an `http://` issuer on a non-localhost host by setting +`insecureAllowHTTP: true`. The VirtualMCPServer controller rejects this +combination at reconcile time with `AuthServerConfigValidated=False` if the flag +is unset, so misconfiguration surfaces on the resource rather than crashing the +pod at startup. Never set this for issuers reachable outside the cluster. + If the browser-facing authorization endpoint needs to be on a different host than the issuer (for example, behind an ingress that rewrites paths), set `authorizationEndpointBaseUrl` to override the `authorization_endpoint` in the diff --git a/docs/toolhive/guides-vmcp/local-cli.mdx b/docs/toolhive/guides-vmcp/local-cli.mdx index 05b3b50c..6d952e13 100644 --- a/docs/toolhive/guides-vmcp/local-cli.mdx +++ b/docs/toolhive/guides-vmcp/local-cli.mdx @@ -240,6 +240,13 @@ For Tier 2, ToolHive starts and stops a HuggingFace Text Embeddings Inference (TEI) container named `thv-embedding-` automatically. Customize the model and image with `--embedding-model` and `--embedding-image`. +For Tier 3, you can point at any HuggingFace TEI server or at an +OpenAI-compatible `/embeddings` endpoint (OpenAI, Azure OpenAI, or another +compatible gateway). Set `embeddingProvider: openai` and `embeddingModel` +alongside `embeddingService`, and supply the API key via the `OPENAI_API_KEY` +environment variable (omit it for keyless gateways). The default is `tei`, so +existing Tier 3 configs continue to work unchanged. + For the conceptual background and tuning parameters, see [Optimize tool discovery](./optimizer.mdx) and [Tool optimization](../concepts/tool-optimization.mdx). diff --git a/docs/toolhive/guides-vmcp/optimizer.mdx b/docs/toolhive/guides-vmcp/optimizer.mdx index dfd513e7..4cfbcd61 100644 --- a/docs/toolhive/guides-vmcp/optimizer.mdx +++ b/docs/toolhive/guides-vmcp/optimizer.mdx @@ -80,9 +80,14 @@ toolset. ## EmbeddingServer resource -The EmbeddingServer CRD manages the lifecycle of a TEI server. An empty -`spec: {}` uses all defaults. The two most important fields you can customize -are: +The EmbeddingServer CRD manages the lifecycle of a managed TEI server, which is +the default embedding backend. If you'd rather point the optimizer at an +external OpenAI-compatible embedding service instead, see +[Use an OpenAI-compatible embedding service](#use-an-openai-compatible-embedding-service) +below. + +An empty `spec: {}` uses all defaults. The two most important fields you can +customize are: - **`model`**: The Hugging Face embedding model to use. The default (`BAAI/bge-small-en-v1.5`) is the tested and recommended model. You can @@ -115,6 +120,77 @@ spec: ::: +## Use an OpenAI-compatible embedding service + +Instead of running a managed TEI EmbeddingServer, you can point the optimizer at +an external service that speaks the OpenAI `/embeddings` API, such as OpenAI +itself, Azure OpenAI, or another OpenAI-compatible gateway. Use this when you +already operate a centralized embedding service and don't want a second copy +running per vMCP, or when you need a hosted model. + +Set `embeddingProvider: openai` under `spec.config.optimizer` and configure +`embeddingService` and `embeddingModel` directly. Do **not** set +`embeddingServerRef`; the operator rejects combining the two at admission. + +```yaml title="VirtualMCPServer resource" +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: VirtualMCPServer +metadata: + name: optimizer-vmcp + namespace: toolhive-system +spec: + groupRef: + name: my-group + config: + optimizer: + # highlight-start + embeddingProvider: openai + embeddingService: http://llm-gateway.default.svc.cluster.local:8080/v1 + embeddingModel: text-embedding-3-small + # highlight-end + embeddingServiceTimeout: 15s + incomingAuth: + type: anonymous +``` + +`embeddingService` is the base URL of the OpenAI-compatible endpoint; +`/embeddings` is appended automatically. `embeddingModel` is the model name +passed in each request and is required for the `openai` provider (the `tei` +provider ignores it, because the model is fixed by the TEI container). + +The API key for the embedding service is read from the `OPENAI_API_KEY` +environment variable on the vmcp container, never from the CRD spec or +ConfigMap. Inject it from a Secret via `podTemplateSpec`: + +```yaml title="VirtualMCPServer resource (excerpt)" +spec: + podTemplateSpec: + spec: + containers: + - name: vmcp + env: + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: embedding-api-key + key: apiKey +``` + +Omit the env var entirely if your gateway is keyless (for example, an in-cluster +LLM gateway that authenticates by network position). An empty key omits the +`Authorization` header. + +:::warning[Inputs are not truncated] + +Unlike the TEI backend, the OpenAI API does not silently truncate over-long +inputs. A tool description that exceeds the model's context window causes the +request to fail with an error rather than being truncated. + +::: + +When `embeddingProvider` is omitted, the optimizer defaults to `tei` and your +existing TEI-based configuration continues to work unchanged. + ## Local mode (CLI) You can enable the optimizer directly from the `thv vmcp` CLI without a @@ -256,16 +332,23 @@ spec: exclude={['embeddingService']} /> -:::info[Kubernetes: EmbeddingServer is always required] +:::info[Kubernetes: EmbeddingServer is required for the default TEI provider] + +When using the Kubernetes operator with the default `tei` embedding provider, +even if you set `hybridSearchSemanticRatio` to `"0.0"` (all keyword search), the +optimizer still requires a configured `EmbeddingServer`. The EmbeddingServer +won't be used at runtime when the semantic ratio is `0.0`, but the configuration +must be present due to how the operator wires the resources internally. -When using the Kubernetes operator, even if you set `hybridSearchSemanticRatio` -to `"0.0"` (all keyword search), the optimizer still requires a configured -`EmbeddingServer`. The EmbeddingServer won't be used at runtime when the -semantic ratio is `0.0`, but the configuration must be present due to how the -operator wires the resources internally. +This restriction doesn't apply when you set `optimizer.embeddingService` +directly, such as with the +[OpenAI-compatible provider](#use-an-openai-compatible-embedding-service); the +operator only requires `embeddingServerRef` when no manual embedding service is +configured. -This restriction does not apply to local CLI mode. `thv vmcp serve --optimizer` -runs keyword-only search with no EmbeddingServer and no container. +This restriction also does not apply to local CLI mode. +`thv vmcp serve --optimizer` runs keyword-only search with no EmbeddingServer +and no container. ::: diff --git a/docs/toolhive/reference/cli/thv_proxy.md b/docs/toolhive/reference/cli/thv_proxy.md index be2e8d92..6cbc09c2 100644 --- a/docs/toolhive/reference/cli/thv_proxy.md +++ b/docs/toolhive/reference/cli/thv_proxy.md @@ -97,6 +97,7 @@ thv proxy [flags] SERVER_NAME ### Options ``` + --allowed-origins stringArray Exact-match allowlist for the HTTP Origin header (repeatable). Recommended when binding publicly; loopback binds derive a default allowlist automatically, non-loopback binds log a warning when no value is supplied. Example: https://my-mcp.example.com -h, --help help for proxy --host string Host for the HTTP proxy to listen on (IP or hostname) (default "127.0.0.1") --oidc-audience string Expected audience for the token diff --git a/docs/toolhive/reference/cli/thv_run.md b/docs/toolhive/reference/cli/thv_run.md index 4c0d1100..b3f5d7aa 100644 --- a/docs/toolhive/reference/cli/thv_run.md +++ b/docs/toolhive/reference/cli/thv_run.md @@ -112,6 +112,7 @@ thv run [flags] SERVER_OR_IMAGE_OR_PROTOCOL [-- ARGS...] ``` --allow-docker-gateway Allow outbound connections to Docker gateway addresses (host.docker.internal, gateway.docker.internal, 172.17.0.1). Only applies when --isolate-network is set. These are blocked by default even when insecure_allow_all is enabled. + --allowed-origins stringArray Exact-match allowlist for the HTTP Origin header (repeatable). Recommended when binding publicly; loopback binds derive a default allowlist automatically, non-loopback binds log a warning when no value is supplied. Example: https://my-mcp.example.com --audit-config string Path to the audit configuration file --authz-config string Path to the authorization configuration file --ca-cert string Path to a custom CA certificate file to use for container builds diff --git a/docs/toolhive/tutorials/mcp-optimizer.mdx b/docs/toolhive/tutorials/mcp-optimizer.mdx index a1fe2453..2129365a 100644 --- a/docs/toolhive/tutorials/mcp-optimizer.mdx +++ b/docs/toolhive/tutorials/mcp-optimizer.mdx @@ -181,8 +181,11 @@ Then apply the YAML above, which creates a new `fetch` server with the correct ## Step 2: Deploy an EmbeddingServer -The optimizer uses semantic search to find relevant tools. This requires an -EmbeddingServer, which runs a text embeddings inference (TEI) server. +The optimizer uses semantic search to find relevant tools, which means it needs +to talk to an embedding service. This tutorial deploys a managed EmbeddingServer +that runs a HuggingFace Text Embeddings Inference (TEI) container. If you'd +rather point at an existing OpenAI-compatible embedding service, see +[Use an OpenAI-compatible embedding service](../guides-vmcp/optimizer.mdx#use-an-openai-compatible-embedding-service). Create an EmbeddingServer with default settings. This deploys the `BAAI/bge-small-en-v1.5` model. If you are running on ARM64 nodes (for example, diff --git a/static/api-specs/toolhive-api.yaml b/static/api-specs/toolhive-api.yaml index 63d3dd71..b0c70cfa 100644 --- a/static/api-specs/toolhive-api.yaml +++ b/static/api-specs/toolhive-api.yaml @@ -640,6 +640,12 @@ components: type: string type: array uniqueItems: false + insecure_allow_http: + description: |- + InsecureAllowHTTP permits an http:// issuer URL for non-localhost hosts. + Only set this for in-cluster Kubernetes deployments on a trusted network. + Production deployments reachable outside the cluster MUST use https://. + type: boolean issuer: description: |- Issuer is the issuer identifier for this authorization server. @@ -1309,6 +1315,18 @@ components: blocked by default in the egress proxy even when InsecureAllowAll is set. Only applicable to Docker deployments with network isolation enabled. type: boolean + allowed_origins: + description: |- + AllowedOrigins is the allowlist of values accepted on the HTTP Origin header, + used for DNS-rebinding protection per MCP 2025-11-25 §"Security Warning". + When empty and Host is loopback (127.0.0.1 / localhost / [::1]), a default + loopback-only allowlist is derived at middleware-wiring time. + When empty and Host is non-loopback, the middleware is disabled — operators + exposing the proxy publicly must configure an explicit allowlist. + items: + type: string + type: array + uniqueItems: false audit_config: $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_audit.Config' audit_config_path: diff --git a/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json index d109cf30..6b2153fe 100644 --- a/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json +++ b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json @@ -128,7 +128,7 @@ "description": "EmbeddedAuthServer configures an embedded OAuth2/OIDC authorization server\nOnly used when Type is \"embeddedAuthServer\"", "properties": { "authorizationEndpointBaseUrl": { - "description": "AuthorizationEndpointBaseURL overrides the base URL used for the authorization_endpoint\nin the OAuth discovery document. When set, the discovery document will advertise\n`{authorizationEndpointBaseUrl}/oauth/authorize` instead of `{issuer}/oauth/authorize`.\nAll other endpoints (token, registration, JWKS) remain derived from the issuer.\nThis is useful when the browser-facing authorization endpoint needs to be on a\ndifferent host than the issuer used for backend-to-backend calls.\nMust be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.", + "description": "AuthorizationEndpointBaseURL overrides the base URL used for the authorization_endpoint\nin the OAuth discovery document. When set, the discovery document will advertise\n`{authorizationEndpointBaseUrl}/oauth/authorize` instead of `{issuer}/oauth/authorize`.\nAll other endpoints (token, registration, JWKS) remain derived from the issuer.\nThis is useful when the browser-facing authorization endpoint needs to be on a\ndifferent host than the issuer used for backend-to-backend calls.\nMust be a valid HTTPS URL (or HTTP for localhost, or HTTP for trusted in-cluster hosts\nwhen insecureAllowHTTP is true) without query, fragment, or trailing slash.", "pattern": "^https?://[^\\s?#]+[^/\\s?#]$", "type": "string" }, @@ -195,8 +195,13 @@ "type": "array", "x-kubernetes-list-type": "atomic" }, + "insecureAllowHTTP": { + "default": false, + "description": "InsecureAllowHTTP permits an http:// issuer URL for non-localhost hosts.\nOnly set this for in-cluster Kubernetes deployments where traffic between\npods traverses a trusted network (e.g. the in-cluster service mesh).\nProduction deployments reachable outside the cluster MUST use https://.\n\nOn VirtualMCPServer: when false (the default), http:// issuers for non-localhost\nhosts are rejected at reconcile time with an AuthServerConfigValidated=False condition.\n\nOn MCPServer and MCPRemoteProxy (via MCPExternalAuthConfig): this field is\nstructurally present but enforcement is deferred to pod startup via Config.Validate();\na misconfigured issuer will cause the pod to crash at startup rather than surface\nas an operator condition.", + "type": "boolean" + }, "issuer": { - "description": "Issuer is the issuer identifier for this authorization server.\nThis will be included in the \"iss\" claim of issued tokens.\nMust be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash (per RFC 8414).", + "description": "Issuer is the issuer identifier for this authorization server.\nThis will be included in the \"iss\" claim of issued tokens.\nMust be a valid HTTPS URL (or HTTP for localhost, or HTTP for trusted in-cluster hosts when\ninsecureAllowHTTP is true) without query, fragment, or trailing slash (per RFC 8414).", "pattern": "^https?://[^\\s?#]+[^/\\s?#]$", "type": "string" }, diff --git a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json index b57e129e..eac2abc4 100644 --- a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json +++ b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json @@ -19,7 +19,7 @@ "description": "AuthServerConfig configures an embedded OAuth authorization server.\nWhen set, the vMCP server acts as an OIDC issuer, drives users through\nupstream IDPs, and issues ToolHive JWTs. The embedded AS becomes the\nIncomingAuth OIDC provider — its issuer must match IncomingAuth.OIDCConfigRef\nso that tokens it issues are accepted by the vMCP's incoming auth middleware.\nWhen nil, IncomingAuth uses an external IDP and behavior is unchanged.", "properties": { "authorizationEndpointBaseUrl": { - "description": "AuthorizationEndpointBaseURL overrides the base URL used for the authorization_endpoint\nin the OAuth discovery document. When set, the discovery document will advertise\n`{authorizationEndpointBaseUrl}/oauth/authorize` instead of `{issuer}/oauth/authorize`.\nAll other endpoints (token, registration, JWKS) remain derived from the issuer.\nThis is useful when the browser-facing authorization endpoint needs to be on a\ndifferent host than the issuer used for backend-to-backend calls.\nMust be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash.", + "description": "AuthorizationEndpointBaseURL overrides the base URL used for the authorization_endpoint\nin the OAuth discovery document. When set, the discovery document will advertise\n`{authorizationEndpointBaseUrl}/oauth/authorize` instead of `{issuer}/oauth/authorize`.\nAll other endpoints (token, registration, JWKS) remain derived from the issuer.\nThis is useful when the browser-facing authorization endpoint needs to be on a\ndifferent host than the issuer used for backend-to-backend calls.\nMust be a valid HTTPS URL (or HTTP for localhost, or HTTP for trusted in-cluster hosts\nwhen insecureAllowHTTP is true) without query, fragment, or trailing slash.", "pattern": "^https?://[^\\s?#]+[^/\\s?#]$", "type": "string" }, @@ -86,8 +86,13 @@ "type": "array", "x-kubernetes-list-type": "atomic" }, + "insecureAllowHTTP": { + "default": false, + "description": "InsecureAllowHTTP permits an http:// issuer URL for non-localhost hosts.\nOnly set this for in-cluster Kubernetes deployments where traffic between\npods traverses a trusted network (e.g. the in-cluster service mesh).\nProduction deployments reachable outside the cluster MUST use https://.\n\nOn VirtualMCPServer: when false (the default), http:// issuers for non-localhost\nhosts are rejected at reconcile time with an AuthServerConfigValidated=False condition.\n\nOn MCPServer and MCPRemoteProxy (via MCPExternalAuthConfig): this field is\nstructurally present but enforcement is deferred to pod startup via Config.Validate();\na misconfigured issuer will cause the pod to crash at startup rather than surface\nas an operator condition.", + "type": "boolean" + }, "issuer": { - "description": "Issuer is the issuer identifier for this authorization server.\nThis will be included in the \"iss\" claim of issued tokens.\nMust be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash (per RFC 8414).", + "description": "Issuer is the issuer identifier for this authorization server.\nThis will be included in the \"iss\" claim of issued tokens.\nMust be a valid HTTPS URL (or HTTP for localhost, or HTTP for trusted in-cluster hosts when\ninsecureAllowHTTP is true) without query, fragment, or trailing slash (per RFC 8414).", "pattern": "^https?://[^\\s?#]+[^/\\s?#]$", "type": "string" }, @@ -1498,6 +1503,19 @@ "optimizer": { "description": "Optimizer configures the MCP optimizer for context optimization on large toolsets.\nWhen enabled, vMCP exposes only find_tool and call_tool operations to clients\ninstead of all backend tools directly. This reduces token usage by allowing\nLLMs to discover relevant tools on demand rather than receiving all tool definitions.", "properties": { + "embeddingModel": { + "description": "EmbeddingModel is the model name requested from the embedding service\n(e.g. \"text-embedding-3-small\"). Required when EmbeddingProvider is\n\"openai\". Ignored for the \"tei\" provider, where the model is fixed by the\nrunning TEI container.\n\nThe API key for an OpenAI-compatible service is not configured here: it is\nread from the OPENAI_API_KEY environment variable so the secret never\nlands in a CRD spec or ConfigMap. An empty key omits the Authorization\nheader, which supports keyless in-cluster gateways.", + "type": "string" + }, + "embeddingProvider": { + "default": "tei", + "description": "EmbeddingProvider selects the wire protocol used to talk to the embedding\nservice. \"tei\" speaks the HuggingFace Text Embeddings Inference API;\n\"openai\" speaks the OpenAI-compatible /embeddings API, which lets the\noptimizer use OpenAI, Azure OpenAI, or another OpenAI-compatible gateway.\nDefaults to \"tei\" when empty.\n\nThe \"openai\" provider reads EmbeddingService directly and cannot be combined\nwith EmbeddingServerRef, which provisions a managed TEI server; the operator\nrejects that combination at admission.", + "enum": [ + "tei", + "openai" + ], + "type": "string" + }, "embeddingService": { "description": "EmbeddingService is the full base URL of the embedding service endpoint\n(e.g., http://my-embedding.default.svc.cluster.local:8080) for semantic\ntool discovery.\n\nIn a Kubernetes environment, it is more convenient to use the\nVirtualMCPServerSpec.EmbeddingServerRef field instead of setting this\ndirectly. EmbeddingServerRef references an EmbeddingServer CRD by name,\nand the operator automatically resolves the referenced resource's\nStatus.URL to populate this field. This provides managed lifecycle\n(the operator watches the EmbeddingServer for readiness and URL changes)\nand avoids hardcoding service URLs in the config. If both\nEmbeddingServerRef and this field are set, EmbeddingServerRef takes\nprecedence and this value is overridden with a warning.", "type": "string" @@ -1623,6 +1641,52 @@ ], "type": "object" }, + "obo": { + "description": "OBO contains configuration for on-behalf-of (OBO) auth strategy.\nUsed when Type = \"obo\". The default upstream build returns ErrEnterpriseRequired;\nan out-of-tree build registers a real strategy via auth.RegisterOBOStrategy.", + "properties": { + "audience": { + "description": "Audience is the target audience (resource URI) for the exchanged token.", + "type": "string" + }, + "cacheSkewSeconds": { + "description": "CacheSkewSeconds is the number of seconds to subtract from a cached token's\nexpiry when deciding whether to refresh it. Defaults to zero (no skew).\nThe operator CRD stores this as CacheSkew *metav1.Duration and converts it\nto an integer-seconds value for the vMCP runtime contract.", + "format": "int32", + "type": "integer" + }, + "clientId": { + "description": "ClientID is the OAuth client ID for the OBO request.", + "type": "string" + }, + "clientSecret": { + "description": "ClientSecret is the OAuth client secret (use ClientSecretEnv for security).", + "type": "string" + }, + "clientSecretEnv": { + "description": "ClientSecretEnv is the environment variable name containing the client secret.\nThe value will be resolved at runtime from this environment variable.", + "type": "string" + }, + "scopes": { + "description": "Scopes are the requested scopes for the exchanged token.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "subjectTokenProviderName": { + "description": "SubjectTokenProviderName selects which upstream provider's token to use as the\nsubject (assertion) token for the OBO exchange. When set, the token is looked\nup from Identity.UpstreamTokens[SubjectTokenProviderName]; when omitted, the\ninbound end-user token (Identity.Token) is used directly.\nMatches the operator CRD's SubjectTokenProviderName field; the enterprise OBO\nconverter maps both to the runtime contract without renaming.", + "type": "string" + }, + "tokenUrl": { + "description": "TokenURL is the Entra token endpoint URL for the OBO exchange.", + "type": "string" + } + }, + "required": [ + "tokenUrl" + ], + "type": "object" + }, "tokenExchange": { "description": "TokenExchange contains configuration for token exchange auth strategy.\nUsed when Type = \"token_exchange\".", "properties": { @@ -1647,7 +1711,8 @@ "items": { "type": "string" }, - "type": "array" + "type": "array", + "x-kubernetes-list-type": "atomic" }, "subjectProviderName": { "description": "SubjectProviderName selects which upstream provider's token to use as the\nsubject token. When set, the token is looked up from Identity.UpstreamTokens\ninstead of using Identity.Token.\nWhen left empty and an embedded authorization server is configured, the system\nautomatically populates this field with the first configured upstream provider name.\nSet it explicitly to override that default or to select a specific provider when\nmultiple upstreams are configured.", @@ -1668,7 +1733,7 @@ "type": "object" }, "type": { - "description": "Type is the auth strategy: \"unauthenticated\", \"header_injection\", \"token_exchange\", \"upstream_inject\", \"aws_sts\", \"obo\"", + "description": "Type is the auth strategy: \"unauthenticated\", \"header_injection\", \"token_exchange\", \"upstream_inject\", \"aws_sts\", \"obo\", \"xaa\"", "type": "string" }, "upstreamInject": { @@ -1683,6 +1748,73 @@ "providerName" ], "type": "object" + }, + "xaa": { + "description": "XAA contains configuration for XAA (Cross-Application Access) auth strategy.\nUsed when Type = \"xaa\".", + "properties": { + "idpClientId": { + "description": "IDPClientID is the OAuth client ID at the IdP for Step A.", + "type": "string" + }, + "idpClientSecret": { + "description": "IDPClientSecret is the client secret at the IdP for Step A.", + "type": "string" + }, + "idpClientSecretEnv": { + "description": "IDPClientSecretEnv is the env var containing the IdP client secret.", + "type": "string" + }, + "idpTokenUrl": { + "description": "IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange).", + "type": "string" + }, + "insecureTargetTokenUrl": { + "description": "InsecureTargetTokenURL allows plain HTTP for TargetTokenURL.\nWARNING: this is insecure and must only be set for in-cluster or\ndevelopment/testing endpoints — never in production.", + "type": "boolean" + }, + "scopes": { + "description": "Scopes are the requested scopes for Steps A and B.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "subjectProviderName": { + "description": "SubjectProviderName selects which upstream provider's ID token to use.\nAuto-populated when embedded AS is active.", + "type": "string" + }, + "targetAudience": { + "description": "TargetAudience is the resource AS URL for the ID-JAG audience claim (required).", + "type": "string" + }, + "targetClientId": { + "description": "TargetClientID is the OAuth client ID at the target AS for Step B.", + "type": "string" + }, + "targetClientSecret": { + "description": "TargetClientSecret is the client secret at the target AS for Step B.", + "type": "string" + }, + "targetClientSecretEnv": { + "description": "TargetClientSecretEnv is the env var containing the target AS client secret.", + "type": "string" + }, + "targetResource": { + "description": "TargetResource is the RFC 8707 resource indicator sent as the `resource`\nparameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It\nidentifies the target resource server — not the access-token audience, which\nis governed by TargetAudience. For MCP backends, set to the MCP server URL.", + "type": "string" + }, + "targetTokenUrl": { + "description": "TargetTokenURL is the target AS token endpoint for Step B (JWT Bearer grant).", + "type": "string" + } + }, + "required": [ + "idpTokenUrl", + "targetAudience", + "targetTokenUrl" + ], + "type": "object" } }, "required": [ @@ -1785,6 +1917,52 @@ ], "type": "object" }, + "obo": { + "description": "OBO contains configuration for on-behalf-of (OBO) auth strategy.\nUsed when Type = \"obo\". The default upstream build returns ErrEnterpriseRequired;\nan out-of-tree build registers a real strategy via auth.RegisterOBOStrategy.", + "properties": { + "audience": { + "description": "Audience is the target audience (resource URI) for the exchanged token.", + "type": "string" + }, + "cacheSkewSeconds": { + "description": "CacheSkewSeconds is the number of seconds to subtract from a cached token's\nexpiry when deciding whether to refresh it. Defaults to zero (no skew).\nThe operator CRD stores this as CacheSkew *metav1.Duration and converts it\nto an integer-seconds value for the vMCP runtime contract.", + "format": "int32", + "type": "integer" + }, + "clientId": { + "description": "ClientID is the OAuth client ID for the OBO request.", + "type": "string" + }, + "clientSecret": { + "description": "ClientSecret is the OAuth client secret (use ClientSecretEnv for security).", + "type": "string" + }, + "clientSecretEnv": { + "description": "ClientSecretEnv is the environment variable name containing the client secret.\nThe value will be resolved at runtime from this environment variable.", + "type": "string" + }, + "scopes": { + "description": "Scopes are the requested scopes for the exchanged token.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "subjectTokenProviderName": { + "description": "SubjectTokenProviderName selects which upstream provider's token to use as the\nsubject (assertion) token for the OBO exchange. When set, the token is looked\nup from Identity.UpstreamTokens[SubjectTokenProviderName]; when omitted, the\ninbound end-user token (Identity.Token) is used directly.\nMatches the operator CRD's SubjectTokenProviderName field; the enterprise OBO\nconverter maps both to the runtime contract without renaming.", + "type": "string" + }, + "tokenUrl": { + "description": "TokenURL is the Entra token endpoint URL for the OBO exchange.", + "type": "string" + } + }, + "required": [ + "tokenUrl" + ], + "type": "object" + }, "tokenExchange": { "description": "TokenExchange contains configuration for token exchange auth strategy.\nUsed when Type = \"token_exchange\".", "properties": { @@ -1809,7 +1987,8 @@ "items": { "type": "string" }, - "type": "array" + "type": "array", + "x-kubernetes-list-type": "atomic" }, "subjectProviderName": { "description": "SubjectProviderName selects which upstream provider's token to use as the\nsubject token. When set, the token is looked up from Identity.UpstreamTokens\ninstead of using Identity.Token.\nWhen left empty and an embedded authorization server is configured, the system\nautomatically populates this field with the first configured upstream provider name.\nSet it explicitly to override that default or to select a specific provider when\nmultiple upstreams are configured.", @@ -1830,7 +2009,7 @@ "type": "object" }, "type": { - "description": "Type is the auth strategy: \"unauthenticated\", \"header_injection\", \"token_exchange\", \"upstream_inject\", \"aws_sts\", \"obo\"", + "description": "Type is the auth strategy: \"unauthenticated\", \"header_injection\", \"token_exchange\", \"upstream_inject\", \"aws_sts\", \"obo\", \"xaa\"", "type": "string" }, "upstreamInject": { @@ -1845,6 +2024,73 @@ "providerName" ], "type": "object" + }, + "xaa": { + "description": "XAA contains configuration for XAA (Cross-Application Access) auth strategy.\nUsed when Type = \"xaa\".", + "properties": { + "idpClientId": { + "description": "IDPClientID is the OAuth client ID at the IdP for Step A.", + "type": "string" + }, + "idpClientSecret": { + "description": "IDPClientSecret is the client secret at the IdP for Step A.", + "type": "string" + }, + "idpClientSecretEnv": { + "description": "IDPClientSecretEnv is the env var containing the IdP client secret.", + "type": "string" + }, + "idpTokenUrl": { + "description": "IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange).", + "type": "string" + }, + "insecureTargetTokenUrl": { + "description": "InsecureTargetTokenURL allows plain HTTP for TargetTokenURL.\nWARNING: this is insecure and must only be set for in-cluster or\ndevelopment/testing endpoints — never in production.", + "type": "boolean" + }, + "scopes": { + "description": "Scopes are the requested scopes for Steps A and B.", + "items": { + "type": "string" + }, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "subjectProviderName": { + "description": "SubjectProviderName selects which upstream provider's ID token to use.\nAuto-populated when embedded AS is active.", + "type": "string" + }, + "targetAudience": { + "description": "TargetAudience is the resource AS URL for the ID-JAG audience claim (required).", + "type": "string" + }, + "targetClientId": { + "description": "TargetClientID is the OAuth client ID at the target AS for Step B.", + "type": "string" + }, + "targetClientSecret": { + "description": "TargetClientSecret is the client secret at the target AS for Step B.", + "type": "string" + }, + "targetClientSecretEnv": { + "description": "TargetClientSecretEnv is the env var containing the target AS client secret.", + "type": "string" + }, + "targetResource": { + "description": "TargetResource is the RFC 8707 resource indicator sent as the `resource`\nparameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It\nidentifies the target resource server — not the access-token audience, which\nis governed by TargetAudience. For MCP backends, set to the MCP server URL.", + "type": "string" + }, + "targetTokenUrl": { + "description": "TargetTokenURL is the target AS token endpoint for Step B (JWT Bearer grant).", + "type": "string" + } + }, + "required": [ + "idpTokenUrl", + "targetAudience", + "targetTokenUrl" + ], + "type": "object" } }, "required": [ @@ -2522,6 +2768,10 @@ { "message": "per-tool perUser rate limiting requires incomingAuth.type oidc", "rule": "!has(self.config) || !has(self.config.rateLimiting) || !has(self.config.rateLimiting.tools) || self.config.rateLimiting.tools.all(t, !has(t.perUser)) || (has(self.incomingAuth) && self.incomingAuth.type == 'oidc')" + }, + { + "message": "embeddingServerRef provisions a managed TEI server and cannot be combined with optimizer.embeddingProvider 'openai'; openai mode uses embeddingService directly", + "rule": "!(has(self.embeddingServerRef) && has(self.config) && has(self.config.optimizer) && has(self.config.optimizer.embeddingProvider) && self.config.optimizer.embeddingProvider == 'openai')" } ] },