diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index bf1aa7a4..49f62ce5 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -64,7 +64,7 @@ projects: - id: toolhive repo: stacklok/toolhive - version: v0.55.2 + version: v0.56.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/concepts/embedded-auth-server.mdx b/docs/toolhive/concepts/embedded-auth-server.mdx index c1c0409e..16245ef4 100644 --- a/docs/toolhive/concepts/embedded-auth-server.mdx +++ b/docs/toolhive/concepts/embedded-auth-server.mdx @@ -225,7 +225,8 @@ the OAuth flow. support CIMD (such as VS Code) use it automatically. - **Dynamic Client Registration (DCR):** Supports OAuth 2.0 DCR (RFC 7591) as a fallback for clients that do not support CIMD. MCP clients register - automatically with no manual configuration in ToolHive. + automatically with no manual configuration in ToolHive. A client cap and a + rate limit on `/oauth/register` bound how many clients can register. - **Direct upstream redirect:** Redirects clients directly to the upstream provider for authentication (for example, GitHub or Atlassian). - **Configurable signing keys:** JWTs are signed with keys you provide, diff --git a/docs/toolhive/guides-cli/client-configuration.mdx b/docs/toolhive/guides-cli/client-configuration.mdx index 8093b59e..6725cbd1 100644 --- a/docs/toolhive/guides-cli/client-configuration.mdx +++ b/docs/toolhive/guides-cli/client-configuration.mdx @@ -127,9 +127,12 @@ thv client connector rm --client claude-desktop github-tools Re-running `thv llm setup --client claude-desktop` keeps your connectors, and `thv llm teardown` removes them along with the rest of the gateway configuration. `thv client connector add` logs a warning when Claude Desktop -won't load the connector, for example when a managed-preferences (MDM) profile -overrides the local configuration or a different saved configuration is applied -in the app. +won't load the connector, for example when a managed configuration overrides the +local configuration or a different saved configuration is applied in the app. A +managed configuration is an MDM profile on macOS, or +`/etc/claude-desktop/managed-settings.json` on Linux. A managed file that Claude +Desktop rejects as invalid also disables the local configuration and triggers +the same warning. See the [`thv client connector` reference](../reference/cli/thv_client_connector.md) for diff --git a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx index dc0379f1..39ef7c48 100644 --- a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx +++ b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx @@ -397,11 +397,11 @@ For a complete walkthrough including deploying Redis Sentinel from scratch, see ## Configure MCP client registration MCP clients must identify themselves to the embedded authorization server. The -server always accepts Dynamic Client Registration (DCR) requests from MCP -clients; no configuration is required. Enable CIMD for clients that support the -MCP specification's preferred registration mechanism. You can also set baseline -scopes for clients that need to request scopes beyond those in their -registration metadata. +server accepts Dynamic Client Registration (DCR) requests from MCP clients with +no configuration required, subject to configurable registration limits. Enable +CIMD for clients that support the MCP specification's preferred registration +mechanism. You can also set baseline scopes for clients that need to request +scopes beyond those in their registration metadata. ### Enable CIMD for zero-registration clients @@ -461,6 +461,56 @@ For the two-layer trust model behind CIMD (why the upstream IdP never sees the client's CIMD URL), see [Client ID Metadata Document (CIMD)](../concepts/embedded-auth-server.mdx#client-id-metadata-document-cimd). +### Limit Dynamic Client Registration + +Because `/oauth/register` is unauthenticated, the embedded authorization server +bounds how many clients it registers and how many it keeps: + +- **Expiry:** a client that registers but never completes a token exchange + expires after 24 hours. Its first successful token exchange extends the + registration to 30 days, and a registration idle for more than 30 days + expires. An expired client must register again. +- **Client cap:** `storage.maxDCRClients` caps the number of live DCR-registered + clients (default `10000`). At the cap, `/oauth/register` returns HTTP `503` + with a `Retry-After` header until a client expires; no existing client is + evicted. Operator-configured clients and CIMD clients don't count toward the + cap. With Redis storage, every replica shares the count; with in-memory + storage, each replica keeps its own. +- **Rate limit:** `registrationRateLimit` is a token bucket that limits + registrations per replica (default 1 per second with a burst of 5). Requests + over the limit get HTTP `429` with a `Retry-After` header. Set `maxTokens` to + the burst size and `refillPeriod` to the time it takes to refill the bucket + from empty. For example, 10 registrations per minute with a burst of 3 is + `maxTokens: 3` and `refillPeriod: 18s`. + +The defaults suit most deployments. Raise the limits if many legitimate clients +register in a short window, such as during a large client rollout: + +```yaml title="MCPExternalAuthConfig: DCR limits" +spec: + embeddedAuthServer: + registrationRateLimit: + maxTokens: 10 + refillPeriod: 10s + storage: + type: redis + maxDCRClients: 25000 + redis: + # ... +``` + +When you size the cap, keep in mind that unused clients live for 24 hours. A +caller registering at the full rate can keep up to the per-second rate, +multiplied by the replica count and by 86,400 (the seconds in a day), unused +clients alive. At the default rate, one replica fills the default cap in about +2.8 hours, and new registrations then fail with `503` until those clients +expire. + +Setting `maxDCRClients: 0` removes the cap, leaving the rate limit as the only +bound on stored clients. Use it only temporarily, for example while an existing +client population above the cap drains. A large finite value, such as two to +three times your observed live client count, keeps some protection. + ### Allow confidential DCR clients By default, DCR only issues public clients: registrations must use @@ -477,9 +527,9 @@ spec: `/oauth/register` is unauthenticated, so enabling this lets any caller who can reach the endpoint mint a client credential. Confidential registrations are -restricted to `https` non-loopback redirect URIs, and (with the Redis storage -backend) any DCR registration idle for more than 30 days is evicted and must -re-register. +restricted to `https` non-loopback redirect URIs, and they expire like any other +DCR registration (see +[Limit Dynamic Client Registration](#limit-dynamic-client-registration)). To let an agent register a keypair for RFC 8693 token exchange instead of receiving a shared secret, set `allowPrivateKeyJWTRegistration: true`: diff --git a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx index 92bed3c0..6e2bd7e3 100644 --- a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx +++ b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx @@ -398,6 +398,31 @@ For an explanation of how ToolHive resolves CIMD documents and why the OAuth provider never sees the MCP client's CIMD URL, see [Client ID Metadata Document (CIMD)](../concepts/embedded-auth-server.mdx#client-id-metadata-document-cimd). +### Limit Dynamic Client Registration + +The embedded authorization server caps the number of live DCR clients (default +`10000`) and rate-limits `/oauth/register` (default 1 registration per second +with a burst of 5, per replica). To change these limits, set +`storage.maxDCRClients` and `registrationRateLimit` under +`spec.authServerConfig`: + +```yaml title="VirtualMCPServer: DCR limits" +spec: + authServerConfig: + registrationRateLimit: + maxTokens: 10 + refillPeriod: 10s + storage: + type: redis + maxDCRClients: 25000 + redis: + # ... +``` + +For client expiry rules, sizing guidance, and the HTTP responses at each limit, +see +[Limit Dynamic Client Registration](../guides-k8s/embedded-auth-server-k8s.mdx#limit-dynamic-client-registration). + ### Allow confidential DCR clients To configure a VirtualMCPServer for confidential DCR, set diff --git a/docs/toolhive/reference/cli/thv_client_connector_add.md b/docs/toolhive/reference/cli/thv_client_connector_add.md index 4cd95559..d42d00a4 100644 --- a/docs/toolhive/reference/cli/thv_client_connector_add.md +++ b/docs/toolhive/reference/cli/thv_client_connector_add.md @@ -21,8 +21,11 @@ must be the server's final address: Claude Desktop does not follow HTTP redirects, even one that only adds a trailing slash. A warning is logged when Claude Desktop will not load the connector: when a -managed-preferences (MDM) profile overrides its local configuration, or when a -different saved configuration is applied in the app. +managed configuration (an MDM profile on macOS, or +/etc/claude-desktop/managed-settings.json on Linux) overrides its local +configuration, or when a different saved configuration is applied in the app. +A managed file that Claude Desktop rejects as invalid also disables the local +configuration, so it triggers the same warning. Example: thv client connector add --client claude-desktop stacklok-connectors https://connector-gateway.example.com/gw/mcp diff --git a/docs/toolhive/reference/cli/thv_llm_config_set.md b/docs/toolhive/reference/cli/thv_llm_config_set.md index 67c2df59..8fba1571 100644 --- a/docs/toolhive/reference/cli/thv_llm_config_set.md +++ b/docs/toolhive/reference/cli/thv_llm_config_set.md @@ -31,14 +31,14 @@ thv llm config set [flags] ``` --audience string OIDC audience (optional) - --bedrock-compat Persist Bedrock compatibility for Claude Code (CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 + per-tier Bedrock model IDs). Applied by "thv llm setup". Use --bedrock-compat=false to clear. + --bedrock-compat Persist Bedrock compatibility for Claude Code (CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 + per-tier Bedrock model IDs discovered from the gateway). Applied by "thv llm setup". Use --bedrock-compat=false to clear. --callback-port int OIDC callback port (omit to keep current; default: ephemeral) --client-id string OIDC client ID --enable-1m With Bedrock compat, opt into the 1M context window by appending [1m] to opus/sonnet model IDs. --gateway-url string LLM gateway base URL (must use HTTPS) -h, --help help for set --issuer string OIDC issuer URL - --models strings Model IDs to persist and apply during "thv llm setup", comma-separated or by repeating the flag, e.g. --models=us.anthropic.claude-opus-4-8,us.anthropic.claude-sonnet-5. Credential-helper clients (Claude Desktop) write these as inferenceModels; with Bedrock compat, each ID is also mapped to a Claude Code tier by matching 'haiku', 'opus', or 'sonnet' in the ID. + --models strings Model IDs to persist and apply during "thv llm setup", comma-separated or by repeating the flag, e.g. --models=us.anthropic.claude-opus-4-8,us.anthropic.claude-sonnet-5. Credential-helper clients (Claude Desktop) write these as inferenceModels; with Bedrock compat, each ID overrides a Claude Code tier by matching 'haiku', 'opus', 'sonnet', or 'fable' in the ID. --proxy-port int Localhost proxy listen port (omit to keep current; default: 14000) --short-prompt-cache Use Claude Code's five-minute prompt cache instead of ToolHive's one-hour default. Applied by "thv llm setup"; use --short-prompt-cache=false to restore one-hour caching. --tls-skip-verify Skip TLS certificate verification for the upstream gateway (local dev only; use --tls-skip-verify=false to clear) diff --git a/docs/toolhive/reference/cli/thv_llm_setup.md b/docs/toolhive/reference/cli/thv_llm_setup.md index 3d9c68d3..99fb9bb4 100644 --- a/docs/toolhive/reference/cli/thv_llm_setup.md +++ b/docs/toolhive/reference/cli/thv_llm_setup.md @@ -60,10 +60,17 @@ run and persisted to config only after login and tool patching succeed. This lets you combine "config set" and "setup" into a single command. For a gateway that forwards to AWS Bedrock, add --bedrock-compat to configure -Claude Code with CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 and per-tier Bedrock -model IDs (override with --models; add --enable-1m for the 1M context window on -opus/sonnet). The setting is persisted and only affects Claude Code. To add it to -an already-configured Claude Code, re-run: +Claude Code with CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 and pin each model tier +to the newest Bedrock model the gateway advertises (override a tier with --models; +add --enable-1m for the 1M context window on opus/sonnet). The pins repoint Claude +Code's built-in Opus/Sonnet/Haiku/Fable rows in the /model picker (via +ANTHROPIC_DEFAULT_*_MODEL), so selecting a built-in row routes to Bedrock. Setup +also enables Claude Code's gateway model discovery, which adds any further models +the gateway serves as separate picker rows without re-running setup. If the +gateway's model list cannot be fetched, or has no Bedrock model IDs, setup does +not configure Claude Code (unless --models pins every tier). The setting is +persisted and only affects Claude Code. +To add it to an already-configured Claude Code, re-run: thv llm setup --client claude-code --bedrock-compat @@ -80,7 +87,7 @@ thv llm setup [flags] ``` --anthropic-path-prefix string Path prefix appended to the gateway URL for Anthropic API tools (e.g. /anthropic). Also used for Pi model discovery. When omitted, the gateway is probed automatically. --audience string OIDC audience (optional) - --bedrock-compat Configure Claude Code for a gateway that forwards to AWS Bedrock: write CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 (Bedrock rejects the experimental anthropic-beta headers) and pin per-tier Bedrock model IDs (override with --models). Persisted, so a later plain "thv llm setup" keeps it; clear with --bedrock-compat=false. Only affects Claude Code. + --bedrock-compat Configure Claude Code for a gateway that forwards to AWS Bedrock: write CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 (Bedrock rejects the experimental anthropic-beta headers) and pin each tier to the newest Bedrock model the gateway advertises (override with --models). Persisted, so a later plain "thv llm setup" keeps it; clear with --bedrock-compat=false. Only affects Claude Code. --callback-port int OIDC callback port (omit to keep current; default: ephemeral) --client string Configure only this AI tool by name (e.g. claude-code, cursor, codex). Omit to configure all detected tools. --client-id string OIDC client ID @@ -89,7 +96,7 @@ thv llm setup [flags] -h, --help help for setup --issuer string OIDC issuer URL --lazy Skip the interactive OIDC login and defer it until the first time a configured tool accesses the gateway. Tool config and persisted settings are written normally. Useful for unattended provisioning (e.g. an MDM profile). - --models strings Model IDs to configure, comma-separated or by repeating the flag, e.g. --models=us.anthropic.claude-opus-4-8,us.anthropic.claude-sonnet-5. For Pi these filter its discovered models (Pi does not support --lazy). For credential-helper clients (Claude Desktop) these become inferenceModels. With --bedrock-compat, each ID is also mapped to a Claude Code tier by matching 'haiku', 'opus', or 'sonnet' in the ID (IDs matching no tier are ignored with a warning). Omit to use the built-in Bedrock defaults / gateway model discovery. + --models strings Model IDs to configure, comma-separated or by repeating the flag, e.g. --models=us.anthropic.claude-opus-4-8,us.anthropic.claude-sonnet-5. For Pi these filter its discovered models (Pi does not support --lazy). For credential-helper clients (Claude Desktop) these become inferenceModels. With --bedrock-compat, each ID overrides a Claude Code tier by matching 'haiku', 'opus', 'sonnet', or 'fable' in the ID (IDs matching no tier are ignored with a warning). Omit to pin each tier to the newest Bedrock model discovered from the gateway. --proxy-port int Localhost proxy listen port (omit to keep current; default: 14000) --short-prompt-cache Use Claude Code's five-minute prompt cache instead of ToolHive's one-hour default. Persisted, so a later plain "thv llm setup" keeps it; use --short-prompt-cache=false to restore one-hour caching. --skip-browser Print the OIDC authorization URL instead of opening a browser, then wait for the callback. Use in headless/SSH/CI environments where no system browser is available. diff --git a/docs/toolhive/reference/helm/operator-crds.mdx b/docs/toolhive/reference/helm/operator-crds.mdx index 0e07cdad..472c497f 100644 --- a/docs/toolhive/reference/helm/operator-crds.mdx +++ b/docs/toolhive/reference/helm/operator-crds.mdx @@ -11,9 +11,9 @@ Control CRD installation and preservation with these Helm values. This reference lists values declared in the chart's default `values.yaml`. Templates can also accept optional settings and inherited `global` values. -Chart: `toolhive-operator-crds`. Chart version: `0.55.2`. Source: -[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.55.2/deploy/charts/operator-crds/values.yaml) -at `v0.55.2`. +Chart: `toolhive-operator-crds`. Chart version: `0.56.0`. Source: +[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.56.0/deploy/charts/operator-crds/values.yaml) +at `v0.56.0`. ## Values diff --git a/docs/toolhive/reference/helm/operator.mdx b/docs/toolhive/reference/helm/operator.mdx index 8f4f1445..032d83db 100644 --- a/docs/toolhive/reference/helm/operator.mdx +++ b/docs/toolhive/reference/helm/operator.mdx @@ -10,9 +10,9 @@ Configure the ToolHive operator with these Helm values. This reference lists values declared in the chart's default `values.yaml`. Templates can also accept optional settings and inherited `global` values. -Chart: `toolhive-operator`. Chart version: `0.55.2`. Source: -[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.55.2/deploy/charts/operator/values.yaml) -at `v0.55.2`. +Chart: `toolhive-operator`. Chart version: `0.56.0`. Source: +[`values.yaml`](https://github.com/stacklok/toolhive/blob/v0.56.0/deploy/charts/operator/values.yaml) +at `v0.56.0`. ## Values @@ -68,7 +68,7 @@ features: gc: gogc: 75 gomemlimit: 110MiB -image: ghcr.io/stacklok/toolhive/operator:v0.55.2 +image: ghcr.io/stacklok/toolhive/operator:v0.56.0 imageDiscovery: enabled: false resources: {} @@ -153,8 +153,8 @@ serviceAccount: labels: {} name: toolhive-operator tolerations: [] -toolhiveRunnerImage: ghcr.io/stacklok/toolhive/proxyrunner:v0.55.2 -vmcpImage: ghcr.io/stacklok/toolhive/vmcp:v0.55.2 +toolhiveRunnerImage: ghcr.io/stacklok/toolhive/proxyrunner:v0.56.0 +vmcpImage: ghcr.io/stacklok/toolhive/vmcp:v0.56.0 volumeMounts: [] volumes: [] ``` @@ -255,7 +255,7 @@ Key containing the password in `existingSecret`. When empty, uses **Type:** `string` -**Default:** `"ghcr.io/stacklok/toolhive/operator:v0.55.2"` +**Default:** `"ghcr.io/stacklok/toolhive/operator:v0.56.0"` Container image for the operator. @@ -271,7 +271,7 @@ Image pull policy for the operator container and image-discovery containers. **Type:** `string` -**Default:** `"ghcr.io/stacklok/toolhive/proxyrunner:v0.55.2"` +**Default:** `"ghcr.io/stacklok/toolhive/proxyrunner:v0.56.0"` Container image for ToolHive proxy runners created by the operator. @@ -279,7 +279,7 @@ Container image for ToolHive proxy runners created by the operator. **Type:** `string` -**Default:** `"ghcr.io/stacklok/toolhive/vmcp:v0.55.2"` +**Default:** `"ghcr.io/stacklok/toolhive/vmcp:v0.56.0"` Container image for Virtual MCP Server (vMCP) deployments created by the operator. diff --git a/static/api-specs/toolhive-api.yaml b/static/api-specs/toolhive-api.yaml index 60fcce77..e8efa275 100644 --- a/static/api-specs/toolhive-api.yaml +++ b/static/api-specs/toolhive-api.yaml @@ -539,6 +539,28 @@ components: userinfo_override: $ref: '#/components/schemas/authserver.UserInfoRunConfig' type: object + authserver.RegistrationRateLimitRunConfig: + description: |- + RegistrationRateLimit bounds the unauthenticated /oauth/register + endpoint. If omitted, defaults to 1 registration per second with a + burst of 5. The limiter is per process, so the effective rate is the + configured rate multiplied by the replica count. The cap on how many + registered clients may exist at once is Storage.MaxDCRClients; unused + clients live 24 hours, so a caller at the full rate keeps up to + rate x replicas x 86400 of them alive, which at the defaults exceeds the + default cap within about 2.8 hours. The operator sets it from the + embedded auth server's registrationRateLimit. + properties: + burst: + description: |- + Burst is the number of registrations admitted at once before the rate + applies. Must be positive. + type: integer + requests_per_second: + description: RequestsPerSecond is the sustained registration rate. Must + be positive. + type: number + type: object authserver.RunConfig: description: |- EmbeddedAuthServerConfig contains configuration for the embedded OAuth2/OIDC authorization server. @@ -553,8 +575,9 @@ components: client_secret_post in addition to "none" (still the default on omission) and mints a client_secret returned exactly once. Confidential clients are restricted to https non-loopback redirect URIs, and - registrations idle for more than DefaultDCRClientTTL (30 days) are - evicted and must re-register. This gates registration only: disabling + registrations expire and must re-register when never used within + DefaultUnusedDCRClientTTL (24 hours) or idle for more than + DefaultDCRClientTTL (30 days). This gates registration only: disabling it does not revoke or reject already-minted secrets at the token endpoint. @@ -732,6 +755,8 @@ components: This will be included in the "iss" claim of issued tokens. Must be a valid HTTPS URL (or HTTP for localhost) without query, fragment, or trailing slash. type: string + registration_rate_limit: + $ref: '#/components/schemas/authserver.RegistrationRateLimitRunConfig' schema_version: description: SchemaVersion is the version of the RunConfig schema. type: string @@ -934,8 +959,8 @@ components: type: object authserver.TLSListenerRunConfig: description: |- - TLSListener configures auth server routes to be served over TLS on port 8443. - It is currently rejected because the runner does not start this listener. + TLSListener serves the auth server routes over TLS on port 8443, in + addition to the MCP port. Required for spiffe_x509 client authentication. properties: cert_file: type: string @@ -5118,6 +5143,22 @@ components: Storage configures the storage backend for the auth server. If nil, defaults to in-memory storage. properties: + max_dcr_clients: + description: |- + MaxDCRClients caps the number of live clients created by + /oauth/register. At the cap, registration fails with HTTP 503 until a + client expires; no existing client is evicted. If omitted, defaults to + 10000; 0 means unlimited, and a negative value is rejected. On Redis the + count is shared by every replica. + + Sizing: a client never used for a token exchange lives 24 hours, so a + caller registering at the full rate keeps up to rate x replicas x 86400 + unused clients alive. At the default rate of 1/s that is about 86400 per + replica: one replica fills the default cap in about 2.8 hours, and new + registrations then get 503 for up to 24 hours after the traffic stops. + A client promoted by a token exchange holds its slot for 30 days. The + operator sets it from the embedded auth server's storage.maxDCRClients. + type: integer redis_config: $ref: '#/components/schemas/storage.RedisRunConfig' type: diff --git a/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json index 5517a064..7c8ab58f 100644 --- a/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json +++ b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json @@ -656,6 +656,32 @@ "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", "type": "string" }, + "registrationRateLimit": { + "description": "RegistrationRateLimit bounds the unauthenticated /oauth/register\nendpoint with a token bucket: up to maxTokens registrations at once,\nrefilled at maxTokens per refillPeriod. For a sustained rate of R per\nsecond with a burst of B, set maxTokens to B and refillPeriod to B/R;\nfor example, 10 per minute with a burst of 3 is maxTokens 3,\nrefillPeriod 18s. If omitted, defaults to 1 registration per second\nwith a burst of 5 (maxTokens 5, refillPeriod 5s). The limit is per\nreplica, so the effective rate is the configured rate multiplied by the\nreplica count. Every successful registration stores a client, so a\nhigher rate fills storage.maxDCRClients sooner, and with maxDCRClients\n0 this limit is the only bound on stored clients.", + "properties": { + "maxTokens": { + "description": "MaxTokens is the maximum number of tokens (bucket capacity).\nThis is also the burst size: the maximum number of requests that can be served\ninstantaneously before the bucket is depleted.", + "format": "int32", + "minimum": 1, + "type": "integer" + }, + "refillPeriod": { + "description": "RefillPeriod is the duration to fully refill the bucket from zero to maxTokens.\nThe effective refill rate is maxTokens / refillPeriod tokens per second.\nFormat: Go duration string (e.g., \"1m0s\", \"30s\", \"1h0m0s\").", + "type": "string" + } + }, + "required": [ + "maxTokens", + "refillPeriod" + ], + "type": "object", + "x-kubernetes-validations": [ + { + "message": "registrationRateLimit.refillPeriod must be positive", + "rule": "duration(self.refillPeriod) > duration('0s')" + } + ] + }, "signingKeySecretRefs": { "description": "SigningKeySecretRefs references Kubernetes Secrets containing signing keys for JWT operations.\nSupports key rotation by allowing multiple keys (oldest keys are used for verification only).\nIf not specified, an ephemeral signing key will be auto-generated (development only -\nJWTs will be invalid after restart).", "items": { @@ -819,6 +845,12 @@ "storage": { "description": "Storage configures the storage backend for the embedded auth server.\nIf not specified, defaults to in-memory storage.", "properties": { + "maxDCRClients": { + "description": "MaxDCRClients caps the number of live clients created by Dynamic Client\nRegistration (/oauth/register). At the cap, registration fails with HTTP\n503 until a client expires; no existing client is evicted. If omitted,\ndefaults to 10000. Operator-configured and CIMD clients are not counted.\nOn Redis the count is shared by every replica; with memory storage each\nreplica keeps its own count.\n\nA client never used for a token exchange lives 24 hours, so a caller\nregistering at the full registrationRateLimit keeps up to\nrate x replicas x 86400 unused clients alive; at the default rate one\nreplica fills the default cap in about 2.8 hours.\n\n0 means unlimited. The endpoint is unauthenticated, so 0 leaves stored\nclients bounded only by the per-replica registrationRateLimit, about\n86400 per replica per day at the default rate. Use 0 only temporarily,\nfor example while an existing client population above the cap drains;\na large finite value, such as two to three times the observed live\nclient count, keeps some protection.", + "format": "int32", + "minimum": 0, + "type": "integer" + }, "redis": { "description": "Redis configures the Redis storage backend.\nRequired when type is \"redis\".", "properties": { diff --git a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json index f6ad7ff9..5b83d720 100644 --- a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json +++ b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json @@ -547,6 +547,32 @@ "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", "type": "string" }, + "registrationRateLimit": { + "description": "RegistrationRateLimit bounds the unauthenticated /oauth/register\nendpoint with a token bucket: up to maxTokens registrations at once,\nrefilled at maxTokens per refillPeriod. For a sustained rate of R per\nsecond with a burst of B, set maxTokens to B and refillPeriod to B/R;\nfor example, 10 per minute with a burst of 3 is maxTokens 3,\nrefillPeriod 18s. If omitted, defaults to 1 registration per second\nwith a burst of 5 (maxTokens 5, refillPeriod 5s). The limit is per\nreplica, so the effective rate is the configured rate multiplied by the\nreplica count. Every successful registration stores a client, so a\nhigher rate fills storage.maxDCRClients sooner, and with maxDCRClients\n0 this limit is the only bound on stored clients.", + "properties": { + "maxTokens": { + "description": "MaxTokens is the maximum number of tokens (bucket capacity).\nThis is also the burst size: the maximum number of requests that can be served\ninstantaneously before the bucket is depleted.", + "format": "int32", + "minimum": 1, + "type": "integer" + }, + "refillPeriod": { + "description": "RefillPeriod is the duration to fully refill the bucket from zero to maxTokens.\nThe effective refill rate is maxTokens / refillPeriod tokens per second.\nFormat: Go duration string (e.g., \"1m0s\", \"30s\", \"1h0m0s\").", + "type": "string" + } + }, + "required": [ + "maxTokens", + "refillPeriod" + ], + "type": "object", + "x-kubernetes-validations": [ + { + "message": "registrationRateLimit.refillPeriod must be positive", + "rule": "duration(self.refillPeriod) > duration('0s')" + } + ] + }, "signingKeySecretRefs": { "description": "SigningKeySecretRefs references Kubernetes Secrets containing signing keys for JWT operations.\nSupports key rotation by allowing multiple keys (oldest keys are used for verification only).\nIf not specified, an ephemeral signing key will be auto-generated (development only -\nJWTs will be invalid after restart).", "items": { @@ -710,6 +736,12 @@ "storage": { "description": "Storage configures the storage backend for the embedded auth server.\nIf not specified, defaults to in-memory storage.", "properties": { + "maxDCRClients": { + "description": "MaxDCRClients caps the number of live clients created by Dynamic Client\nRegistration (/oauth/register). At the cap, registration fails with HTTP\n503 until a client expires; no existing client is evicted. If omitted,\ndefaults to 10000. Operator-configured and CIMD clients are not counted.\nOn Redis the count is shared by every replica; with memory storage each\nreplica keeps its own count.\n\nA client never used for a token exchange lives 24 hours, so a caller\nregistering at the full registrationRateLimit keeps up to\nrate x replicas x 86400 unused clients alive; at the default rate one\nreplica fills the default cap in about 2.8 hours.\n\n0 means unlimited. The endpoint is unauthenticated, so 0 leaves stored\nclients bounded only by the per-replica registrationRateLimit, about\n86400 per replica per day at the default rate. Use 0 only temporarily,\nfor example while an existing client population above the cap drains;\na large finite value, such as two to three times the observed live\nclient count, keeps some protection.", + "format": "int32", + "minimum": 0, + "type": "integer" + }, "redis": { "description": "Redis configures the Redis storage backend.\nRequired when type is \"redis\".", "properties": {