diff --git a/docs/ai-gateway/budgets-and-pricing.mdx b/docs/ai-gateway/budgets-and-pricing.mdx index 2f89ee9e..7308cafd 100644 --- a/docs/ai-gateway/budgets-and-pricing.mdx +++ b/docs/ai-gateway/budgets-and-pricing.mdx @@ -6,87 +6,60 @@ description: understand why an unpriced model is refused at the gateway. --- -A _budget_ caps what one user or one group is allowed to spend on model calls, -denominated in USD. Every request is checked against the budget store before any -tokens are spent, and every response is priced from the provider's reported -usage and charged against that same store. +A _budget_ caps model spend in USD for a directory user or group. The AI Gateway +checks available budget before sending a request and charges provider-reported +usage after the response. -You manage budgets in the console, under [Budgets](manage-budgets.mdx). Each one -is independent: creating, editing, or deleting a budget has no effect on any -other budget's counters. - -A REST API covers the same ground for automation, bulk import, and the few -things the console does not expose yet. This page uses it where it is the -clearer way to show a rule, and names the console screen wherever there is one. +Use [Budgets](manage-budgets.mdx) in the console for routine administration. Use +the REST API for automation, bulk import, and pricing. :::danger[Cover every caller before you turn enforcement on] -A caller that no budget covers is **denied**. Pointing the gateway at the budget -service before every caller is covered is a hard outage, not a degraded mode. -See [Cover every caller first](#cover-every-caller-first) for the two ways to do -it. +Budget enforcement denies callers without an applicable budget. Cover every +caller before enabling enforcement. ::: ## Prerequisites -Budget enforcement is only injected into a gateway that has at least one -`AIPolicy` targeting it. The policy's contents are not consulted for budgets, so -an `AIPolicy` that exists purely to satisfy this requirement is a normal and -supported shape. Without one, budgets are silently not enforced. +Budget enforcement requires an `AIPolicy` that targets the gateway. The policy +can omit screening controls when you only need budgets. -Confirm enforcement is actually live before you rely on it: +Confirm that both budget entries report `probeSucceeded: true`: ```bash kubectl get aigw -n \ -o jsonpath='{.status.webhooks}' | jq . ``` -Both budget entries should report `probeSucceeded: true`. A `false` almost -always means the audience configured on the gateway and on the budget service do -not match exactly. +A `false` value usually indicates mismatched audiences on the gateway and budget +service. ## Cover every caller first -Enforcement is fail-closed: there is no implicit allow, so a caller no budget -covers cannot make requests at all. Before pointing the gateway at the budget -service in an environment that already has traffic, cover everyone by one of two -routes. +Before enabling budget enforcement for an environment with traffic, choose one +of these approaches: -**Set the organization default.** One value covers every user who has no budget -of their own. This is the simpler route and usually the right one. Set it on the -**Defaults** tab of the console's [Budgets](manage-budgets.mdx) screen, or see -[Set an organization default](#set-an-organization-default) for the API. +**Set an organization default.** This covers directory users without an explicit +budget. Set it on the **Defaults** tab or through the API. -**Or create budgets individually**, for every existing user and group. For a -large existing estate, the [bulk import](#import-in-bulk) below is faster than -doing it one at a time. +**Create budgets individually.** Use the [bulk import](#import-in-bulk) for an +existing user and group inventory. -The default covers **users** only. There is no group default, so a caller who -resolves to no directory user at all stays uncovered whichever route you take. +The organization default applies only to resolved directory users. ## Create budgets -Every budget belongs to one directory user or one directory group, and carries a -limit in USD and a period of daily, monthly, or yearly. Periods are -calendar-aligned rather than rolling windows from the first request, so a -monthly budget resets on the first of the month. - -The console's [Budgets](manage-budgets.mdx) screen is where you normally do -this: one tab per group budgets, user budgets, and the organization default. The -rest of this section covers the same operations through the API, for automation -and for bulk changes. +Each budget has a USD limit and a daily, monthly, or yearly calendar period. -Over the API a budget identifies its principal by scope and subject. `scope` is -`user` or `group`; `subject_id` is the OIDC subject for a user, or the directory -group's UUID for a group. `limit_usd` is a decimal string and `period` is `day`, -`month`, or `year`. +The API identifies a budget by scope and subject. `scope` is `user` or `group`; +`subject_id` is the OIDC subject for a user, or the directory group's UUID for a +group. `limit_usd` is a decimal string and `period` is `day`, `month`, or +`year`. ### Set an organization default -One org-wide default covers every user who has no budget of their own, so you do -not have to create a row per person. In the console this is the **Defaults** -tab. Over the API: +Set the organization default on the **Defaults** tab or through the API: ```bash curl -X PUT https:///v1/budgets/default \ @@ -95,21 +68,13 @@ curl -X PUT https:///v1/budgets/default \ -d '{"limit_usd": "350.000000000", "period": "day"}' ``` -The default is a singleton: setting it replaces the previous value, and deleting -it leaves every user who has no explicit budget uncovered again. - -At admission the default is materialized against the caller's own subject, using -the same key an explicit budget would, so spend counters carry over unchanged -when you later give that person a budget of their own. Promoting someone from -the default to an exception does not reset their usage. +Setting the default replaces its previous value. Deleting it leaves users +without explicit budgets uncovered. Moving a user between the default and an +explicit budget preserves their current-period usage. -The default is user-scope only. There is no group default. +### Update one budget -### One budget at a time - -These create or update exactly one budget and cannot delete another by omission, -which is what the console's per-row editing uses. Prefer them for anything -scripted that touches a single principal: +Use the user or group endpoint to create or update one budget: ```bash curl -X PUT https:///v1/budgets/users/ \ @@ -123,17 +88,14 @@ curl -X PUT https:///v1/budgets/groups/ \ -d '{"limit_usd": "500.000000000", "period": "month", "precedence": 1}' ``` -Group budgets take a 1-based `precedence`. User budgets do not, because a user's -own budget is always charged first. +Group budgets use 1-based `precedence`. The gateway charges an explicit user +budget first. ### Import in bulk -This is the one operation with no console equivalent, and the reason to reach -for the API when you are covering an existing estate. - `PUT /v1/budgets` replaces the **entire** collection: budgets in the request are created or updated, and **budgets absent from the request are deleted**. Use it -for import, never for edits. +only for complete imports. ```bash curl -X PUT https:///v1/budgets \ @@ -147,9 +109,7 @@ curl -X PUT https:///v1/budgets \ }' ``` -Spend counters survive a collection replace, because they are keyed on the -principal rather than on the budget row, so a bulk import does not reset -anyone's usage. +Bulk replacement preserves spend counters. `GET /v1/budgets` returns the collection with an `ETag`. Send it back as `If-Match` on your next write to detect a concurrent change; a `412` response @@ -157,33 +117,28 @@ means re-read and retry. ## Which budget gets charged -A user's own budget is tried first, whatever their group membership, and the -organization default stands in for it when they have none. If that budget has no -headroom left, the request charges the first group budget in ascending -`precedence` order that still has headroom. A caller that no budget covers at -all is the fail-closed case: refused. +The gateway charges budgets in this order: -## Publish a pricing catalog +1. The user's explicit budget, or the organization default. +2. The first group budget with remaining capacity, ordered by ascending + `precedence`. -Pricing has no console screen today, so this is API-only. +The gateway denies the request when no applicable budget has capacity. -Pricing is one organization-wide catalog, versioned by `effective_from`. A -published version takes effect immediately for interactions after that -timestamp, with no restart. +## Publish a pricing catalog + +Publish the organization-wide pricing catalog through the API. Catalog versions +use `effective_from` and take effect without a restart. -The gateway ships a baseline catalog, regenerated weekly from public model -pricing data, so a fresh install prices new models without any action from you. -The baseline is dated far in the past, which means **any** version you publish -outranks it permanently. Once you publish, the weekly baseline refresh never -reaches your deployment again: to pick up a model the refreshed baseline added, -add it to your own catalog. +The gateway ships a baseline catalog generated weekly from public model pricing. +After publishing a custom version, maintain the custom catalog to add models or +update rates. :::warning[`PUT` replaces the whole catalog] -`PUT /v1/budgets/pricing` publishes a complete replacement, not a partial merge. -Every entry you omit becomes unpriced, and unpriced models are refused. Always -`GET` the active catalog, edit the returned matrix, then `PUT` the whole thing -back. +`PUT /v1/budgets/pricing` replaces the catalog. Retrieve the active catalog, +edit it, and publish the complete result. The gateway denies requests for models +omitted from the active catalog. ::: @@ -197,19 +152,14 @@ curl -X PUT https:///v1/budgets/pricing \ -d @catalog.json ``` -If your active catalog omits a model the baseline prices, the service logs a -warning at startup naming the active version and the exact provider and model -pairs it does not cover. Read that as a standing statement that those models are -unpriced, not as a transient condition. - -A binary rollback does not roll pricing back. Whatever version was active stays -active, so recovery is a `PUT` of the rates you want. +At startup, the service logs provider and model pairs omitted from a custom +catalog. Application rollback preserves the active catalog. Publish a previous +catalog version to restore earlier rates. ### Rate fields by provider -Set the optional sub-counts when a model in your workload reports them. -Otherwise the cost falls back to the nearest required rate, which can materially -under-bill audio and reasoning workloads. +Set provider-specific rates for cache creation, reasoning, audio, prompt-size +tiers, and server-side tools when the model reports those usage categories. Anthropic reports cache-creation tokens split by time to live, so declare each rate separately: @@ -286,23 +236,11 @@ Server-side tools that bill per thousand calls go in a `tools` block alongside } ``` -## An unpriced model is refused - -If a request names a provider and model with no entry in the active catalog, the -gateway refuses it. The platform will not serve a request it cannot account for, -so keep the catalog complete for every model you route to. - -This is deliberately the same treatment as a caller whose identity cannot be -resolved: both are cases where the platform cannot govern the request, rather -than cases where the caller is out of money. - ## Monitor spend -Use the console for this. [Budgets](manage-budgets.mdx) shows each budget's -usage against its limit. Developers see their own figures in the console. +Use [Budgets](manage-budgets.mdx) to compare usage with each limit. -The API returns the same figures when you need them somewhere else, such as a -finance report or your own dashboard: +Use the API for finance reports and external dashboards: | Endpoint | Returns | | --------------------------- | ---------------------------------------------------------------- | @@ -322,19 +260,15 @@ separate budgets role. ## When a budget runs out -An exhausted budget refuses further requests with the same response as any other -policy denial. The response deliberately does not reveal which condition -matched, so to tell exhaustion apart from a missing budget, check the caller in -the console: a budget at full utilization is exhaustion, and no budget listed at -all is the other case. +An exhausted budget returns a policy denial. Check the caller in the console to +distinguish an exhausted budget from missing coverage. -A budget recovers on its own when the calendar period rolls over. To restore -access immediately, raise the limit on the affected budget; it applies to the -next request, with no restart and no counter reset. +Access resumes when the calendar period resets. To restore access immediately, +raise the limit. The change applies on the next request and preserves the usage +counter. ## Next steps -- [Budgets in the console](manage-budgets.mdx) to do all of this without the - API. -- [Connect model providers](./providers-and-models.mdx) if a model you need to - price is not routed yet. +- [Manage budgets](manage-budgets.mdx) for console-based administration. +- [Connect model providers](./providers-and-models.mdx) to add models to the + gateway. diff --git a/docs/ai-gateway/forward-audit-logs.mdx b/docs/ai-gateway/forward-audit-logs.mdx index b85e41fa..2a0a6d39 100644 --- a/docs/ai-gateway/forward-audit-logs.mdx +++ b/docs/ai-gateway/forward-audit-logs.mdx @@ -6,9 +6,8 @@ description: lifecycle, then ship them to Splunk, Elastic, Kafka, or Loki. --- -The gateway emits structured JSON audit events to the standard output of each of -its pods. Events cover data-plane and application-layer actions: request -admission, detection decisions, virtual key lifecycle, and authentication +The AI Gateway emits structured JSON audit events to pod standard output for +request admission, detection decisions, virtual key changes, and authentication outcomes. Every event carries the fields required by NIST SP 800-53 controls AU-2, AU-3, @@ -29,9 +28,8 @@ spec: enabled: true ``` -When enabled, every audit event fires. There are no per-component toggles and no -sampling, on the principle that routing and volume control belong to your ingest -layer rather than to the thing being audited. +Enabling audit events captures every supported event without sampling. Filter +and route events in your log pipeline. ## What gets captured @@ -53,8 +51,7 @@ layer rather than to the thing being audited. | `aigw.apikey.validation.failed` | An invalid or unknown virtual key was presented | failure | | `aigw.apikey.ratelimit.exceeded` | A per-key request rate limit was exceeded | denied | -Event types follow the namespace `aigw...`, so a SIEM -rule can match a whole component or a single verb. +Event types use `aigw...`. ## Event schema @@ -87,27 +84,23 @@ rule can match a whole component or a single verb. } ``` -**Subjects** are never taken from attacker-controllable headers. `user` comes -from the authenticated caller's token on API events, or from a header set by a -trusted upstream filter on request-path events. `virtual_key_id` is stamped only -after a key has been validated. +**Subjects** come from authenticated tokens on API events or a trusted upstream +filter on request-path events. The gateway adds `virtual_key_id` after key +validation. -**Targets** are `{type: llm_request, model, provider, route}` on request-path -events and `{type: virtual_api_key, key_id, prefix}` on key events. Secret key -material is never emitted. A SIEM rule keying on `target.provider` should match -the lowercase provider set: `openai`, `anthropic`, `awsbedrock`, `azureopenai`, -`gcpvertexai`, `geminiaistudio`. +**Targets** use `{type: llm_request, model, provider, route}` on request events +and `{type: virtual_api_key, key_id, prefix}` on key events. Key events omit +secret material. Match `target.provider` against these lowercase values: +`openai`, `anthropic`, `awsbedrock`, `azureopenai`, `gcpvertexai`, and +`geminiaistudio`. **Common extras** are `request_id`, `trace_id` and `span_id` for OpenTelemetry correlation, `client_ip`, `user_agent`, `pod`, and the gateway's name and namespace. -Detection events additionally carry `detection_types`, the entity names that -fired, and `action`, one of `block`, `redact`, or `log-only`. Passive detections -split their attribution into `shadow_types`, for detectors configured but not -acting, and `log_only_types`, for detectors whose policy is deliberately -passive. Keeping those distinct lets a compliance team tell "we saw this and -chose not to act" apart from "policy here is passive by design". +Detection events add `detection_types` and `action`. `action` is `block`, +`redact`, or `log-only`. Passive detections use `shadow_types` for detectors in +shadow mode and `log_only_types` for log-only policy. Request-path events carry `body_bytes`; response-path events carry `chunk_bytes`, which is per-chunk for streaming responses and must not be summed @@ -260,29 +253,21 @@ if $msg contains '"logger":"audit"' and $msg contains '"msg":"audit_event"' then ## Retention, tamper evidence, and GDPR -The gateway emits events and takes no position on how long you keep them, -whether storage is tamper-evident (NIST SP 800-53 AU-9), or which fields are in -scope for GDPR. Those are decisions for your ingest layer. Index retention -policies, index lifecycle management, log-group retention, and write-once -archival with object lock all work here. +Configure retention, tamper evidence, and privacy controls in your ingest and +storage systems. Use lifecycle policies and write-once storage where your +compliance requirements call for them. -Field-level redaction, for example stripping a caller's email address for EU -data subjects, is best enforced as an ingest-time transform before the event -reaches long-term storage. The gateway never reads back or replays events after -emitting them. +Apply field-level redaction before events reach long-term storage. The gateway +does not retain or replay emitted audit events. -## What these events are not - -Two adjacent streams are out of scope. +## Related logs The **Kubernetes API audit log** covers control-plane activity such as `kubectl` -calls and operator reconciles. Enable it in your API server audit policy and -forward it separately. +calls and operator reconciles. Configure it through the API server audit policy. -**Interaction journaling** captures full prompt and response content for every -request, configured under `spec.journaling`. Audit events record _that_ a -request was admitted, denied, or masked and _why_; journaling records _what_ the -prompt and response contained. +**Interaction journaling**, configured under `spec.journaling`, captures prompt +and response content. Audit events capture decisions and reasons without the +full interaction content. ## Next steps diff --git a/docs/ai-gateway/index.mdx b/docs/ai-gateway/index.mdx index 2be19941..f9c98411 100644 --- a/docs/ai-gateway/index.mdx +++ b/docs/ai-gateway/index.mdx @@ -7,52 +7,47 @@ description: import DocCardList from '@theme/DocCardList'; -The Stacklok AI Gateway is a self-hosted enterprise gateway that sits between -your AI tools and large language model (LLM) providers. Deployed in your -environment, every AI request hits your policy, identity, and audit controls -before it reaches a provider. It gives platform and security teams a single -control point for cost, compliance, and access to models like those from OpenAI, -Anthropic, AWS Bedrock, Azure OpenAI, and Google Vertex AI. +:::enterprise -The AI Gateway is part of Stacklok Enterprise. It complements ToolHive: ToolHive -governs the **tools** your agents can use, while the AI Gateway governs the -**models** they can call. +The AI Gateway is a component of Stacklok Enterprise. + +[Learn more about Stacklok Enterprise](../platform/index.mdx). + +::: + +The AI Gateway is a self-hosted control point between AI clients and large +language model (LLM) providers. Platform and security teams use it to enforce +identity, budget, routing, data protection, and audit policies for model +traffic. + +The AI Gateway governs model access. The +[Connector Gateway](../connector-gateway/index.mdx) governs MCP tool access. ## What you can do -- Cap AI spend in real time with token and cost budgets per user, team, agent, - or org, reconciled to your finance team's reporting cycle -- Block PII, financial data, regulated identifiers, and your own custom patterns - at the gateway, with fail-safe denial when policy can't be enforced -- Capture every LLM request in an audit trail and stream it to your security - information and event management (SIEM) system -- Route logical model names across multiple providers with failover -- Tie every request to an identity from your IdP, with provider keys locked in - the gateway and access revoked on the next request when a user or agent is - offboarded -- Govern people and agents as first-class peers under the same policies, - budgets, and audit trail +- Set cost budgets for users and groups. +- Route model names across providers with weighting and failover. +- Screen requests and responses for prompt injection, payment card data, and + personally identifiable information (PII). +- Record structured audit events and forward them to a security information and + event management (SIEM) system. +- Associate requests with identities while keeping provider credentials in the + gateway. ## How configuration works -Gateway configuration is declarative. The operator reconciles the running -infrastructure to match the configuration you apply, so you change the gateway -by changing its configuration rather than by operating it directly. +Apply `AIGateway` and `AIPolicy` custom resources to configure providers, +routing, resilience, and screening. The AI Gateway operator reconciles those +resources into running infrastructure. -Budgets are the exception. They are managed in the console, under -[Budgets](manage-budgets.mdx), with an API for automation and bulk import. Each -budget is independent of every other one. See +Manage budgets in the console or use the management API for automation and bulk +import. See [Manage budgets](manage-budgets.mdx) and [Budgets and pricing](./budgets-and-pricing.mdx). -The pages in this section cover the tasks rather than enumerating every field. - ## Before you start -The AI Gateway must already be installed. See -[Configure the AI Gateway](../platform/enterprise-platform/configure-ai-gateway.mdx) -for the install-time settings, and -[Deploy the platform](../platform/enterprise-platform/deployment.mdx) for the -full sequence. +Install the AI Gateway before applying the examples in this section. See +[Configure the AI Gateway](../platform/enterprise-platform/configure-ai-gateway.mdx). Examples on these pages use `-n `, where `` is the namespace your AI Gateway is installed into. diff --git a/docs/ai-gateway/manage-budgets.mdx b/docs/ai-gateway/manage-budgets.mdx index 5d750859..858f8052 100644 --- a/docs/ai-gateway/manage-budgets.mdx +++ b/docs/ai-gateway/manage-budgets.mdx @@ -6,49 +6,39 @@ description: defaults, and control which group budget is charged first. --- -The **Budgets** screen is where you set what people may spend. It has three -tabs, which correspond to the three decisions involved: **Group Budgets**, -**User Budgets**, and the **Defaults** that apply to everyone without one. +Use the **Budgets** screen to set organization defaults and limits for users and +groups. -For how enforcement behaves, including the fact that a caller with no budget is -refused rather than allowed, read -[Budgets and pricing](./budgets-and-pricing.mdx) first. This page covers doing -it in the console. +Budget enforcement denies model requests from callers without an applicable +budget. Configure an organization default before enabling traffic. See +[Budgets and pricing](./budgets-and-pricing.mdx) for enforcement and API +details. ## Set the organization default -Start on the **Defaults** tab on a new deployment. A default gives every user a -budget without you creating one per person, which matters because enforcement is -fail-closed: a user no budget covers cannot make requests at all. - -Set a limit and a period, and every user who has no budget of their own inherits -it. +On the **Defaults** tab, set the limit and period applied to users without an +explicit budget. ## Group budgets -The **Group Budgets** tab lists each group's budget with its **Period**, -**Budget**, **Used**, and **Utilization**. Use **Create Group Budget** to add -one, choosing a group, a period of daily, monthly, or yearly, and a limit. -Periods are calendar-aligned rather than rolling from first use, so a monthly -budget resets on the first of the month. +The **Group Budgets** tab lists each group's **Period**, **Budget**, **Used**, +and **Utilization**. Select **Create Group Budget**, then choose a group, daily, +monthly, or yearly period, and limit. Budget periods align with the calendar. -Each group budget also carries a **Priority**, which the API calls `precedence`. -It is what you use to express "charge the team budget before the department -budget". A user's own budget is always tried first, whatever the priorities say, -so a per-user exception wins over the group's cap. For the full order, see +Use **Priority**, called `precedence` in the API, to order applicable group +budgets. The AI Gateway charges a user's explicit budget first. For the full +order, see [Which budget gets charged](./budgets-and-pricing.mdx#which-budget-gets-charged). ## User budgets -The **User Budgets** tab lists every user with their **Budget source**, -**Period**, and **Limit**. The source reads **Default** for a user on the -organization default and **Custom** for one with a limit of their own, which is -the quickest way to see who has been given an exception. **Edit Budgets** lets -you change several at once. +The **User Budgets** tab lists each user's **Budget source**, **Period**, and +**Limit**. **Default** identifies the organization default, and **Custom** +identifies an explicit user budget. Select **Edit Budgets** to update multiple +users. -Editing a user's limit takes effect on the next request. There is no restart and -no counter reset, so raising a limit is the way to restore access to someone who -has run out mid-period without waiting for the period to roll over. +Changes take effect on the next request and preserve current-period usage. Raise +an exhausted user's limit to restore access before the period resets. ## Next steps diff --git a/docs/ai-gateway/model-routing.mdx b/docs/ai-gateway/model-routing.mdx index cd58e36b..0f0ea2c6 100644 --- a/docs/ai-gateway/model-routing.mdx +++ b/docs/ai-gateway/model-routing.mdx @@ -6,11 +6,8 @@ description: priority failover, retries, and timeouts on the AI Gateway. --- -A _route_ maps what a client asks for onto the provider that serves it. Clients -send a normal `model` value in their request; the gateway matches it against -`spec.routes` and forwards to the referenced provider. Because the mapping lives -on the gateway, you can move a logical model name to a different provider -without touching a single client. +A _route_ maps a requested model name to one or more providers. Configure routes +under `spec.routes` to change model providers without updating clients. ## Route a specific model @@ -42,9 +39,8 @@ spec: - provider: openai ``` -Without a default route, a request for an unmatched model is refused. That is -often what you want: it means users can only reach models you have deliberately -routed. +If you omit a default route, the gateway denies requests for unmatched models. +Use this behavior to allow only explicitly routed models. ## Spread traffic across providers @@ -82,18 +78,14 @@ spec: :::warning[Failover needs retries configured] -`priority` sets the failover order, but the gateway only walks that order when -`spec.resilience.retry` is set. With no retry configuration, a failure at the -first priority is returned to the client and the lower-priority provider is -never tried. Configuring priorities alone gives you no failover. +Configure `spec.resilience.retry` with `priority`. A failed request reaches the +next priority only during a retry. ::: ## Configure retries, health checks, and timeouts -`spec.resilience` is opt-in and applies to the whole gateway. Every sub-block is -optional, and omitting one leaves the underlying default in place, so a gateway -that does not set `spec.resilience` behaves exactly as it does today. +`spec.resilience` applies to the whole gateway. Each sub-block is optional. ```yaml title="aigateway.yaml" spec: @@ -135,21 +127,16 @@ retries, so a retry storm cannot exhaust upstream capacity. ## How retries interact with budgets -A request denied for budget reasons is refused before any retry happens, and -retried attempts against a provider error do not charge the budget again. So -retries cannot inflate a user's spend, and a budget denial is never retried into -an accidental success. +The gateway evaluates budgets before retries and charges the request once. +Provider retries do not add charges, and budget denials are not retried. ## Limits worth knowing - A gateway supports up to 20 providers and 120 routes. -- Routes that share the same backends and timeout are combined internally, so - the practical ceiling is roughly 15 distinct backend and timeout combinations - rather than 15 routes. Forty routes that all point at the same provider count - as one combination. -- If a change would exceed that ceiling, the gateway keeps the last working - configuration and reports `RoutesValid=False` with reason `TooManyRouteRules` - rather than applying it. +- The gateway supports approximately 15 distinct combinations of backends and + timeouts. Routes that share a combination are consolidated. +- A configuration that exceeds this limit reports `RoutesValid=False` with + reason `TooManyRouteRules` and preserves the previous working routes. ## Next steps diff --git a/docs/ai-gateway/pci-pii-controls.mdx b/docs/ai-gateway/pci-pii-controls.mdx index 734b79ee..969550c3 100644 --- a/docs/ai-gateway/pci-pii-controls.mdx +++ b/docs/ai-gateway/pci-pii-controls.mdx @@ -6,23 +6,10 @@ description: sensitive data, and block, redact, or log what is found. --- -The gateway can scan request and response bodies for personally identifiable -information (PII) and payment data, then block the request, redact the match, or -record it. Scanning runs in your cluster and never calls out to a cloud service. -It is configured under `spec.processor` on the `AIGateway` resource, and changes -are picked up without a restart. - -Detection itself is performed by a named-entity-recognition service. Microsoft -Presidio is the provider wired today. - -:::note - -This is a different surface from -[prompt injection screening](./prompt-injection-screening.mdx), which calls a -cloud service to look for adversarial prompts. The two are configured -independently and can be used together. - -::: +The AI Gateway uses Microsoft Presidio to scan requests and responses for +personally identifiable information (PII) and payment data. Configure in-cluster +scanning under `spec.processor` on the `AIGateway` resource. You can combine it +with [prompt injection screening](./prompt-injection-screening.mdx). ## Choose what happens on a match @@ -57,9 +44,8 @@ spec: ## Choose which entities to look for -`entities` is the authoritative list of what the scanner looks for. The default -covers common structured identifiers plus model-derived entities such as -`PERSON` and `LOCATION`. Tune it to your data-handling posture. +`entities` controls what the scanner detects. The default includes structured +identifiers and model-derived entities such as `PERSON` and `LOCATION`. | Entity | Matches | | -------------------- | ------------------------------------------- | @@ -85,26 +71,17 @@ here verbatim. ## Tune the confidence threshold -`scoreThreshold` is the minimum confidence, from 0 to 100, for a detection to be -acted on. It defaults to `50`. - -- **50** suits high-sensitivity environments where a false negative is worse - than a false positive. Model-derived entities such as `PERSON` typically score - in this range, so this threshold catches most of them. -- **70 to 80** is a reasonable start for coding assistants and chat, where a - false-positive block is disruptive. -- **85 and above** is conservative, and in practice surfaces only - high-confidence structured matches such as card numbers. - -A common approach is to keep the threshold at 50 but narrow `entities` to the -structured identifiers you actually care about, dropping the model-derived ones -so they cannot drive enforcement. +`scoreThreshold` sets the minimum detection confidence from 0 to 100 and +defaults to `50`. Test representative traffic in `LogOnly` mode, then adjust the +threshold and entity list to balance false positives and false negatives. For +predictable enforcement, select the structured identifiers required by your +data-handling policy. ## Decide how failures behave -Three independent dials cover the three ways scanning can fail. All three should -stay closed in production. The open settings exist for the rollout window, or -for incident response when the detection backend is unhealthy. +Configure parsing, redaction, and detection backend failures independently. Use +the closed settings in production and open settings only during rollout or +incident response. ```yaml spec: @@ -121,8 +98,8 @@ spec: | `mutationFailureAction` | Refuse if redaction fails | Forward unmodified | | `nerProvider.failureAction` | Refuse on backend error, timeout, or open circuit | Log a warning and forward | -A circuit breaker sits in front of the detection backend. While it is open, -every request short-circuits straight to `failureAction` without a backend call: +A circuit breaker applies `failureAction` without calling an unhealthy detection +backend: ```yaml spec: @@ -133,17 +110,14 @@ spec: resetTimeout: '30s' ``` -This is a different breaker from the one in -[`spec.resilience`](./model-routing.mdx#configure-retries-health-checks-and-timeouts), -which protects your model providers. +The +[`spec.resilience`](./model-routing.mdx#configure-retries-health-checks-and-timeouts) +circuit breaker protects model providers separately. ## Size the scanner -`timeout` caps each individual detection call and defaults to `5s`. That sits -comfortably above a warm backend, and well above the large-body case that agent -command-line tools routinely produce: their request bodies run to hundreds of -kilobytes and push inference past a shorter window. Lowering it tightens the -latency budget but risks tripping the breaker under load. +`timeout` limits each detection call and defaults to `5s`. Test large request +bodies before lowering it because timeouts count as detection backend failures. :::warning @@ -153,22 +127,13 @@ call returns. ::: -`concurrency` caps how many detection calls run in parallel for one request, one -per text node, defaulting to `8` with a range of 1 to 128. Changing it rolls the -processor rather than hot-reloading. - -:::tip[Concurrency does not help a single-replica backend] +`concurrency` limits parallel detection calls for one request. It defaults to +`8` and accepts values from 1 to 128. Changing it rolls the processor. -The Presidio analyzer serves one request at a time per pod, so a fan-out only -queues server-side. Raising `concurrency` against one replica measurably does -nothing. The lever for a saturated backend is `presidio.replicas`. To tell which -one you are short of, divide `stacklok_ai_gateway_presidio_analyze_inflight` by -the number of ready analyzer pods: anything above 1 means calls are queuing -inside the backend, where more client-side concurrency cannot reach them. Use -`concurrency` to keep a scaled-out backend busy, not to extract more from one -replica. - -::: +Each Presidio pod processes one request at a time. Scale `presidio.replicas` +before raising `concurrency`. If `stacklok_ai_gateway_presidio_analyze_inflight` +divided by the number of ready analyzer pods exceeds 1, calls are queuing in the +backend. ```yaml spec: @@ -188,18 +153,15 @@ spec: memory: 2Gi ``` -The language model loads at pod start and is around 800 MiB resident, so size -memory accordingly. The analyzer is CPU-bound, so replicas plus an autoscaler is -the simplest scaling lever. At two replicas or more, the gateway also creates a -PodDisruptionBudget for it. +The language model uses approximately 800 MiB of memory after startup. The +analyzer is CPU-bound, so use replicas and autoscaling for additional capacity. +At two or more replicas, the gateway creates a PodDisruptionBudget. ## Cache scan results -Agent command-line tools resend the whole conversation on every turn, so the -same text is scanned repeatedly. The result cache keys each answer by the text -plus a fingerprint of the scanning configuration and serves repeats from the -Redis or Valkey instance the platform already runs, skipping the detection call -entirely. +Enable the result cache to avoid scanning repeated text. It stores detection +results in the platform's Redis or Valkey instance, keyed by the text and +scanning configuration. ```yaml spec: @@ -210,22 +172,12 @@ spec: ttl: '24h' ``` -The cache is off by default. There is no address field: the backend comes from -the platform chart's Redis values. Enabling it with no resolvable backend runs -uncached rather than failing, and never changes a detection outcome. The -`NERResultCacheReady` status condition reports whether an address resolved, -which is not the same as whether it is reachable, so confirm a working cache -from the hit-rate metric rather than from the condition. +The cache uses the platform chart's Redis configuration. If the gateway cannot +resolve the backend, scanning continues without caching. Use the hit-rate metric +to verify that the cache is serving results. ### What read access to the cache discloses -:::danger - -Decide this before enabling the cache. It is a latency optimization, and nothing -else depends on it, so leaving it off is always a safe answer. - -::: - Cache keys are an unsalted hash of the scanned text, which makes the keyspace a **confirmation oracle**. Anyone who can read the backing Redis can hash a candidate piece of text, probe for the key, and confirm whether that exact text @@ -237,28 +189,21 @@ detection, which discloses the position and length of every detected value. If you ship custom recognizers, the rule name tells a reader which of your own detection rules fired. -Three things bound the exposure: +The cache limits exposure as follows: -- **No text is stored.** Neither the scanned text nor any matched substring is - ever written, which is what makes entries safe to keep alongside other - counters in a shared instance. -- **Keyspaces are scoped per gateway**, so the oracle never crosses a gateway - boundary. Two gateways with identical configuration deliberately do not warm - each other's entries. -- **Entries carry an integrity tag**, so someone with write access cannot plant - an empty result to suppress detection. +- Entries omit scanned text and matched substrings. +- Each gateway uses a separate keyspace. +- Integrity tags prevent a writer from inserting a result that suppresses + detection. -The practical rule: treat read access to that backend as equivalent to being -able to confirm what text passed through the gateway. If that is not acceptable -in your environment, leave the cache off. +Treat read access to the cache as permission to confirm whether specific text +passed through the gateway. Leave caching disabled if that disclosure conflicts +with your security requirements. ### The integrity secret -There is normally nothing to provision. The first time the gateway sees the -cache enabled, it creates a Secret named `-ner-cache-mac` in the -gateway's namespace, owned by the gateway resource, holding fresh random bytes. -It is only created if absent, so a steady-state reconcile never replaces a live -value. +By default, the gateway creates `-ner-cache-mac` in its namespace. +The Secret contains the integrity key for cached results. To supply your own instead, through External Secrets, sealed secrets, or out-of-band creation, name it on the operator chart: @@ -270,45 +215,19 @@ nerResultCache: key: mac-secret ``` -Two constraints on a supplied Secret. It must live in the namespace of every -gateway the operator serves. And its value must be at least 32 bytes of -**printable text**, not raw bytes, because it reaches the pod as an environment -variable and raw bytes will wedge the pod rather than degrade gracefully. -Base64-encoding your entropy satisfies both. - -A missing or undersized secret costs latency, never detection: entries that -cannot be verified are re-scanned. That failure is silent, so again, confirm -from the hit rate rather than from a status condition. +Place a supplied Secret in each gateway namespace. Its value must contain at +least 32 bytes of printable text. Base64-encoded random data satisfies this +requirement. -Rotation is the same either way: replace the value. Every existing entry then -fails verification and is re-scanned, which is a cold start rather than a -correctness event, and it is the entire incident response for a leaked secret. +The gateway re-scans entries when it cannot verify the integrity tag. Replace +the Secret value to rotate it; existing entries then miss the cache and are +re-scanned. ### Time to live and footprint -`ttl` defaults to 24 hours. Entries cannot go stale, because the key covers both -content and configuration: changing a threshold, the entity list, a recognizer, -or the analyzer image changes the key and cold-starts the affected entries on -its own. So the setting is a memory and disclosure-window knob, never a -correctness one. Shorten it to narrow the oracle window described above; there -is no correctness reason to. - -Retuning it rolls no pod, rebuilds nothing, and resets no circuit-breaker state. -That matters during a backend outage: a roll would kill in-flight requests, and -rebuilding the detection client would reopen the breaker and herd a struggling -backend back into per-request timeouts, which refuse requests under the default -closed failure action. - -For footprint, budget roughly 250 bytes per distinct conversation turn within -the window. Entry size is independent of turn size, because a value holds entity -types, rule names, and offsets rather than text, so a 200 KB agent payload and a -one-line message cost the same. And the count is bounded by distinct turns -rather than request volume, since resent turns collapse onto entries that -already exist. At 100,000 requests a day that is on the order of 25 MB at steady -state. - -That estimate is derived rather than measured. Check it against your own -workload before relying on it. +`ttl` defaults to 24 hours. Cache keys include the scanning configuration, so +changes to thresholds, entities, recognizers, or the analyzer image cause cache +misses. Shorten the TTL to reduce the disclosure window and memory use. ### Cache metrics @@ -318,11 +237,8 @@ workload before relying on it. | `stacklok_ai_gateway_ner_cache_errors_total` | Backend or codec faults, labeled by operation and error type | | `stacklok_ai_gateway_ner_cache_op_duration_seconds` | Backend round-trip per operation | -The hit rate is the only true measure of detection calls avoided. Every error is -a miss that falls through to a real detection call, so a non-zero error rate -costs latency and hit rate rather than correctness. A request that runs uncached -because no backend resolved issues no lookup at all, and so charts no series -rather than a zero percent hit rate. +Use the hit rate to measure avoided detection calls. Cache errors become misses +and trigger a normal detection call. ## Next steps diff --git a/docs/ai-gateway/prompt-injection-screening.mdx b/docs/ai-gateway/prompt-injection-screening.mdx index 127f5777..2a2cfca0 100644 --- a/docs/ai-gateway/prompt-injection-screening.mdx +++ b/docs/ai-gateway/prompt-injection-screening.mdx @@ -6,20 +6,14 @@ description: including rollout from monitor to enforce, IAM setup, and cost. --- -Prompt injection screening checks every inbound prompt against a cloud screening -service before it reaches your model provider. It is configured under -`spec.guardrails` on the `AIGateway` resource. - -Screening references a guardrail you have already created in your own AWS -account, by ID and version. The gateway does not create or manage the guardrail -itself, so the filters, thresholds, and denied topics stay under your control in -AWS. +Prompt injection screening sends inbound prompts to an AWS Bedrock Guardrail +before the model request reaches its provider. Configure the existing guardrail +ID and version under `spec.guardrails` on the `AIGateway` resource. :::note -This is a different surface from [PCI/PII controls](./pci-pii-controls.mdx), -which scan for sensitive data in-cluster and never call out to AWS. The two are -configured independently and can be used together or separately. +[PCI/PII controls](./pci-pii-controls.mdx) scan sensitive data in your cluster. +Configure either control independently or use them together. ::: @@ -42,26 +36,20 @@ configured independently and can be used together or separately. | `failurePolicy` | `Fail` | What happens if the screening call itself fails | | `unscreenableContentPolicy` | `Deny` | What happens to content that cannot be turned into text | -`failurePolicy` and `unscreenableContentPolicy` look similar and are -deliberately separate. A timeout or an AWS error is a **failure**, governed by -`failurePolicy`. An image-only message that contains no text to screen is not a -failure, it is **unscreenable**, and is governed by `unscreenableContentPolicy`. -Vision workloads usually want `Admit` for the second while keeping `Fail` for -the first. - -Two situations count as unscreenable: a message whose user content is present -but not text, such as an image, and a request body that carries no recognizable -chat container at all. The second usually means a provider was connected whose -request shape the screening layer has not been taught to read, so it is worth -alerting on separately. Either decision is recorded in the audit trail. A -genuinely empty body, or a chat request with no user turn, is not unscreenable -and is admitted without screening. +`failurePolicy` handles timeouts and AWS errors. `unscreenableContentPolicy` +handles content that the gateway cannot convert to text, such as an image-only +message. For vision workloads, consider `Admit` for unscreenable content while +retaining `Fail` for screening errors. + +The gateway also marks unrecognized provider request formats as unscreenable. +Alert on this reason because it can indicate that prompts are bypassing +screening. Empty request bodies and requests without a user message are admitted +without screening. The audit trail records each decision. ## Enable screening -Start in `Monitor` with `failurePolicy: Ignore`. Nothing can be blocked in this -posture, so you can observe verdicts and measure cost against real traffic -first. +Start in `Monitor` with `failurePolicy: Ignore` to measure verdicts, latency, +and cost before enforcing the control. ```yaml title="aigateway.yaml" apiVersion: ai-gateway.stacklok.dev/v1alpha1 @@ -84,8 +72,7 @@ spec: region: us-east-1 ``` -Once you are satisfied with the verdicts and the cost, switch both knobs and -re-apply. The gateway reconciles within a few seconds. +After validation, enforce verdicts and fail closed on screening errors: ```yaml mode: Enforce @@ -109,16 +96,14 @@ the guardrail: } ``` -`bedrock:ApplyGuardrail` is a data-plane action, and it is not implied by the -control-plane actions `bedrock:GetGuardrail` or `bedrock:ListGuardrails`. A role -that can list and describe guardrails but lacks `ApplyGuardrail` fails on every -screening call. That is treated as a call failure and resolved by -`failurePolicy`, so it does not surface as a configuration error at startup. +Grant `bedrock:ApplyGuardrail` explicitly. The control-plane actions +`bedrock:GetGuardrail` and `bedrock:ListGuardrails` do not include this +permission. Missing permission causes screening calls to follow `failurePolicy`. ### IRSA The gateway creates a ServiceAccount named `-guardrails-adapter`. -Annotating it with the IAM role is your step, not the gateway's: +Annotate it with the IAM role: ```bash kubectl annotate serviceaccount \ @@ -127,20 +112,17 @@ kubectl annotate serviceaccount \ eks.amazonaws.com/role-arn=arn:aws:iam:::role/ ``` -Annotations are merged on each reconcile rather than overwritten, so yours -survives resource updates. Restart the screening pod once after annotating, so -the new role token is projected into it. +The operator preserves the annotation during reconciliation. Restart the +screening pod to project the role token. ### EKS Pod Identity Associate the same ServiceAccount with an IAM role through an EKS Pod Identity association, and no annotation is needed. -Pod Identity resolves credentials from a node-local agent on a link-local -address over plain HTTP. The gateway's default network policy already allows -exactly that egress. If you replace or tighten that policy, keep the rule: -without it the credential fetch is dropped silently rather than refused, so the -only symptom is every screening call timing out and failing closed. +The default network policy allows access to the EKS Pod Identity node agent. If +you replace the policy, preserve this egress rule. Blocking it causes credential +requests and screening calls to time out. ### Static credentials @@ -190,9 +172,8 @@ Verdicts reach you three ways: policy type, increments once per blocked category in both `Monitor` and `Enforce` mode. `aigw.guardrail.unscreenable`, labeled by reason, increments once per request that could not be screened, under both `Deny` and `Admit`. - Alert on the unrecognized-shape reason in particular: it means a connected - provider's request shape is not being read, whether or not requests are - currently being blocked because of it. + Alert on the `unrecognized-shape` reason because it indicates that the gateway + cannot extract text from a connected provider's requests. - **Component logs**, which name the categories that fired: ```bash @@ -203,30 +184,12 @@ Verdicts reach you three ways: ## Cost -Bedrock Guardrails bills per 1,000 text units, where a text unit is up to 1,000 -characters. Blocked requests still incur the evaluation cost. These figures are -estimates; see the -[AWS Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/) for current -rates. - -| Filter | Rate per 1,000 text units | -| ---------------------------------------- | ------------------------- | -| Content filters, including prompt attack | $0.15 | -| Denied topics | $0.15 | -| Sensitive information, managed PII types | $0.10 | -| Sensitive information, regex patterns | free | -| Word filters | free | - -A prompt-attack filter alone is $0.15; with PII, $0.25; with denied topics on -top, $0.40. At $0.25 per 1,000 text units, a knowledge worker sending around 20 -prompts a day costs roughly $0.11 a month, a developer using a coding assistant -roughly $0.44, and heavy agent use around $2.20. - -Two things push that up. A threshold that fires too broadly inflates the bill -without reducing model spend, since blocked requests are still evaluated, which -is the practical reason to start in `Monitor` and measure the real block rate. -And a guardrail in a different AWS region from your cluster adds cross-region -data transfer, so colocate them where you can. +Bedrock Guardrails bills by text units and filter type. Blocked requests still +incur evaluation cost. Review current rates on the +[AWS Bedrock pricing page](https://aws.amazon.com/bedrock/pricing/). + +Use `Monitor` mode to estimate cost against representative traffic. Deploy the +guardrail in the same AWS region as the cluster to avoid cross-region transfer. The gateway emits no per-request cost telemetry for screening. Track that spend through your AWS billing tooling. @@ -240,22 +203,20 @@ through your AWS billing tooling. ## Troubleshooting -**`WebhooksReady=False` with a probe failure for guardrails.** The gateway pings -the screening component once per reconcile to set this status. Read the detail -rather than the condition alone: +
+`WebhooksReady=False` reports a guardrails probe failure + +The gateway pings the screening component once per reconcile to set this status. +Inspect the probe details: ```bash kubectl get aigw -n \ -o jsonpath='{.status.webhooks}' | jq ``` -A transport timeout with no matching entry in the component's own log means the -ping never arrived, and the usual cause is a network policy. The default policy -admits exactly two ingress peers: the gateway's main processor, which carries -live traffic, and the operator, which sends the ping. If you replace that -policy, keep both peers. A blocked peer is dropped silently, so a timeout is the -only symptom. +A transport timeout without a component log entry usually indicates a network +policy problem. Preserve ingress from the gateway's main processor and the +operator when replacing the default policy. Check component logs to confirm +whether live screening requests succeed. -This condition covers the ping alone. Screening runs over the other peer, so -screening can be working correctly on live traffic while this reads false. -Confirm from the component log rather than from the condition. +
diff --git a/docs/ai-gateway/providers-and-models.mdx b/docs/ai-gateway/providers-and-models.mdx index 85d86c91..baa39bfd 100644 --- a/docs/ai-gateway/providers-and-models.mdx +++ b/docs/ai-gateway/providers-and-models.mdx @@ -6,10 +6,9 @@ description: OpenAI-compatible providers to the AI Gateway, and rotate their credentials. --- -Every model the gateway can reach belongs to a _provider_: an upstream API, its -network endpoint, and the credential the gateway uses to authenticate to it. -Providers are declared in `spec.providers` on the `AIGateway` resource, and the -credential itself always lives in a Kubernetes Secret, never in the manifest. +A _provider_ defines an upstream API endpoint and its authentication method. +Declare providers in `spec.providers` on the `AIGateway` resource and store +credentials in Kubernetes Secrets. ## Supported providers @@ -24,22 +23,16 @@ credential itself always lives in a Kubernetes Secret, never in the manifest. | Google AI Studio | `GeminiAIStudio` | `APIKey` | `apiKey` | | Google Gemini API | `GoogleGenerativeLanguage` | `APIKey` | `apiKey` | -That is the complete set of `schema` values. `credentials.type` accepts one -further value, `AzureCredentials`, for Azure deployments that authenticate by -role rather than by key; like `AWSCredentials` and `GCPCredentials` it takes a -`region`. +For Azure deployments that authenticate by role, use +`credentials.type: AzureCredentials` with `region`. -Two of these target the same models by different routes. `AWSBedrock` uses -Bedrock's Converse API, while `AWSAnthropic` serves Claude models through -Bedrock's InvokeModel API in Anthropic's native wire format, and its traffic is -priced under the `awsanthropic` family rather than Bedrock's. +`AWSBedrock` uses the Bedrock Converse API. `AWSAnthropic` uses the Bedrock +InvokeModel API with Anthropic's native request format and the `awsanthropic` +pricing family. -The credential field names are type-dependent, and several pairings are enforced -rather than conventional: `GeminiAIStudio` and `GoogleGenerativeLanguage` both -require `APIKey`, and `GCPVertexAI` requires `GCPCredentials` together with -`region`, `projectName`, and a `secretRef`. Pairing a `schema` with the wrong -`credentials.type` fails validation when you apply the resource, naming the -combination it rejected. +`GeminiAIStudio` and `GoogleGenerativeLanguage` require `APIKey`. `GCPVertexAI` +requires `GCPCredentials`, `region`, `projectName`, and `secretRef`. Resource +validation rejects unsupported schema and credential type combinations. ## Add a provider @@ -68,9 +61,8 @@ combination it rejected. key: apiKey ``` -3. Add at least one route that references it. A provider with no route is not - reachable, and validation rejects a route that names a provider you have not - declared: +3. Add a route that references the provider. Resource validation requires every + route to reference a declared provider: ```yaml title="aigateway.yaml" spec: @@ -128,9 +120,8 @@ providers: ### AWS Bedrock -Bedrock authenticates with IAM rather than a stored key, so there is no Secret. -Give the gateway's proxy pods an IAM role, through IRSA or pod identity, that -permits the Bedrock actions you intend to use. +Assign the gateway proxy pods an IAM role through IAM roles for service accounts +(IRSA) or EKS Pod Identity. Grant the role the required Bedrock actions. ```yaml providers: @@ -146,12 +137,10 @@ providers: ### Google Vertex AI -Vertex is reached through a regional endpoint and authenticates with a Google -service-account key. The `credentials.region` value must match the region prefix -in the hostname. +Vertex AI uses a regional endpoint and Google service account key. Match +`credentials.region` to the region prefix in the hostname. -**The Secret's data key must be `service_account.json`.** That name is fixed, -and a different key is ignored on this path rather than reported as an error. +Name the Secret data key `service_account.json`. ```bash kubectl create secret generic vertex-sa \ @@ -179,10 +168,8 @@ Federation is not supported. ### Google AI Studio -Google AI Studio has no native schema of its own. Selecting `GeminiAIStudio` -makes the gateway speak OpenAI's request and response shape and rewrite the path -for AI Studio's OpenAI-compatible surface, so no extra configuration is needed -beyond the API key. +`GeminiAIStudio` uses Google AI Studio's OpenAI-compatible API and requires only +an API key. ### OpenAI-compatible providers @@ -210,18 +197,14 @@ spec: `pathPrefix` is valid only with `schema: OpenAI`. Providers that already carry their own prefix, such as `GeminiAIStudio`, must not set it. -Onboarding an OpenAI-compatible provider needs no code change, only this -resource edit. In the audit and journaling streams it is identified by its -`name`, so it stays distinguishable from a native OpenAI provider. +Audit and journaling events identify an OpenAI-compatible provider by its +configured `name`. :::warning[Name an OpenRouter provider exactly `openrouter`] -Rates for an OpenAI-compatible provider are looked up first by the provider's -`name`, then by its schema family. The pricing catalog accepts a fixed set of -provider values, so an OpenRouter provider named anything else has no rates it -can resolve, and its OpenRouter-only model slugs are refused at admission rather -than billed at the wrong rate. Running more than one OpenRouter account, or -renaming the provider, is not supported for spend tracking today. +The pricing service resolves OpenRouter rates through the provider name. Use +`openrouter` so the gateway can price and admit its model slugs. Spend tracking +supports one OpenRouter provider. ::: @@ -241,9 +224,8 @@ Send a test request afterwards to confirm the new credential is in use. ## Remove a provider -Remove the routes that reference the provider first, then the provider itself, -then apply. Validation rejects a resource whose routes name a provider that no -longer exists, so removing them in the other order fails. +Remove routes that reference the provider, remove the provider, and apply the +resource. ```bash kubectl apply -f aigateway.yaml @@ -255,7 +237,6 @@ automatically. ## Next steps -- [Route models](./model-routing.mdx) to map model names onto the providers you - just connected, with weighting and failover. -- [Budgets and pricing](./budgets-and-pricing.mdx) to price the models you route - to. A model with no price is refused, so this is not optional. +- [Route models](./model-routing.mdx) to configure weighting and failover. +- [Budgets and pricing](./budgets-and-pricing.mdx) to publish prices for the + models you route. diff --git a/docs/connector-gateway/connectors.mdx b/docs/connector-gateway/connectors.mdx index 565abb97..696c4811 100644 --- a/docs/connector-gateway/connectors.mdx +++ b/docs/connector-gateway/connectors.mdx @@ -6,10 +6,8 @@ description: and understand the draft, available, and failure states. --- -A _connector_ is an MCP server the platform makes available through the -Connector Gateway. Registering one here does not expose it to anybody: access is -granted per group, so a newly added connector is visible to nobody until you -grant it. +A _connector_ is an MCP server available through the Connector Gateway. After +registering a connector, grant directory groups access to it. ## The connector list @@ -19,50 +17,43 @@ created. ## Where connectors come from -Connectors can be created three ways: +Register connectors in one of three ways: -- **Import from registry** pulls a server from your MCP registry, so the catalog - your platform team curates is the starting point rather than hand-entry. +- **Import from registry** selects a server from your curated MCP registry. - **Add connector** registers one directly by endpoint. - **Discovery** picks up MCP servers running in your cluster automatically. Those arrive with a description noting the namespace they were found in. ## Draft, available, and failure -**Draft** is an administrator's own marker: the connector is registered but not -active, and the gateway does not serve it. Saving one as no longer a draft is -what takes it live. - -At that point the platform checks the endpoint, and the state it lands in -depends on what it finds: **available** when the endpoint behaves as an MCP -server, or **failure** when it does not. +**Draft** connectors are registered but inactive. Activating a connector +triggers an endpoint check. A valid MCP endpoint becomes **Available**; an +invalid or unreachable endpoint enters **Failure**. ## Granting access Open a connector and use **Grant access** to add one or more groups. Membership is inherited, so granting to a parent group reaches every subgroup beneath it. -Access is evaluated per request rather than cached, so a grant takes effect on -the affected user's next request. +The Connector Gateway evaluates access on every request, so changes take effect +on the next request. :::warning -Access is granted to **directory** groups, not to the OIDC claim groups that -cluster-level authorization policy matches on. See -[The two group models](../platform/concepts/two-group-models.mdx). +Connector grants use **directory groups**. See +[Directory groups and OIDC claim groups](../platform/concepts/two-group-models.mdx). ::: ## Connector authentication -A connector that needs to reach a backend on the user's behalf brokers OAuth -against an -[identity provider](../platform/enterprise-directory/identity-providers.mdx), -which you reference on the connector. +A connector can broker OAuth through an +[identity provider](../platform/enterprise-directory/identity-providers.mdx) +when it needs user authorization for its backend. -Users complete the OAuth consent themselves, either in the console or when they -connect their MCP client. The gateway holds the resulting authorization for -their subsequent tool calls, and offers **Re-authenticate** if it later expires. +Users complete consent in **Your workspace** or during client connection. The +Connector Gateway stores the authorization and prompts the user to +**Re-authenticate** after it expires. ## Next steps diff --git a/docs/connector-gateway/index.mdx b/docs/connector-gateway/index.mdx index 707e61d7..bd0df94f 100644 --- a/docs/connector-gateway/index.mdx +++ b/docs/connector-gateway/index.mdx @@ -1,66 +1,54 @@ --- title: Connector Gateway description: - Govern which MCP servers your organization exposes, who may use them, and what - their tools are actually being called for. + Govern which MCP servers your organization exposes, who can use them, and how + their tools are used. --- import DocCardList from '@theme/DocCardList'; -The Connector Gateway is the per-user entry point for tool calls. Developers -point one client at it and reach every tool they are entitled to, without -holding any backend credential themselves: the gateway resolves each call -against the caller's identity and the access their groups have been granted. +:::enterprise + +The Connector Gateway is a component of Stacklok Enterprise. + +[Learn more about Stacklok Enterprise](../platform/index.mdx). + +::: + +The Connector Gateway provides an identity-aware endpoint for MCP tool calls. It +uses directory group grants to expose the connectors available to each caller +and brokers connector credentials on their behalf. It is the counterpart to the [AI Gateway](../ai-gateway/index.mdx). The AI Gateway governs the **models** your agents can call; the Connector Gateway governs the **tools** they can use. -## It is not the only gateway you can run - -The Connector Gateway builds on the same aggregation the -[Virtual MCP Server (vMCP)](../toolhive/guides-vmcp/index.mdx) provides, but the -chart deploys and manages it for you as its own component. It is not a limit of -one: the ToolHive operator still runs as many vMCPs of your own alongside it, -and the two are for different jobs. - -Reach for the Connector Gateway when the users are people, doing open-ended -work, and you do not know their toolset in advance. It is the daily driver, and -what it gives you is administration without Kubernetes: connectors registered -and granted in the console, each user self-selecting from what their groups -allow, and OAuth or OIDC at the front door. +## Connector Gateway and vMCP -Reach for a vMCP of your own when the toolset is known up front, which is -usually an autonomous agent or a specific application rather than a person. -There you assign backends statically, filter and rename the tools they expose, -choose a front-door authentication method other than OAuth, and attach a -`ToolhiveAuthorizationPolicy` for rules finer than per-connector access. +Use the Connector Gateway to provide people with a group-scoped set of +connectors that administrators manage in the console. Use a +[Virtual MCP Server (vMCP)](../toolhive/guides-vmcp/index.mdx) for applications +and agents with a predefined toolset. A vMCP supports static backend selection, +tool filtering and renaming, multiple authentication methods, and +`ToolhiveAuthorizationPolicy` resources. -Running both is the normal case. See the -[Virtual MCP Server guides](../toolhive/guides-vmcp/index.mdx) for the ones you -build yourself. +You can run the Connector Gateway and multiple vMCP instances in the same +cluster. ## What you administer here -- **Which servers exist.** A connector is an MCP server the platform makes - available through the gateway. Registering one does not expose it to anyone. -- **Who may use each one.** Access is granted per group, so a new connector is - visible to nobody until you grant it. -- **What is actually being called.** Tool usage is the organization's picture of - traffic through the gateway, the counterpart to the AI Gateway's model spend. +- Register MCP servers as connectors. +- Grant connector access to directory groups. +- Review tool calls by connector and tool. ## Before you start -The Connector Gateway is off by default and takes two values to enable, not one. -See -[Configure the Connector Gateway](../platform/enterprise-platform/configure-connector-gateway.mdx) -for the install-time settings, and -[Deploy the platform](../platform/enterprise-platform/deployment.mdx) for the -full sequence. +Deploy and configure the Connector Gateway before administering connectors. See +[Configure the Connector Gateway](../platform/enterprise-platform/configure-connector-gateway.mdx). -Group-based access depends on directory groups rather than the claim groups used -by cluster authorization policy. The two are different systems and are easy to -confuse; see [Two group models](../platform/concepts/two-group-models.mdx). +Connector grants use directory groups. Cluster authorization policy uses OIDC +claim groups. See +[Directory groups and OIDC claim groups](../platform/concepts/two-group-models.mdx). ## Contents diff --git a/docs/connector-gateway/tool-usage.mdx b/docs/connector-gateway/tool-usage.mdx index fc5b4cbb..a927d1de 100644 --- a/docs/connector-gateway/tool-usage.mdx +++ b/docs/connector-gateway/tool-usage.mdx @@ -2,44 +2,26 @@ title: Tool usage sidebar_label: Tool usage description: - See how many tool calls went through the Connector Gateway, which connectors - and tools are being used, and how that is trending. + Review Connector Gateway traffic by time range, connector, and tool. --- -The **Tool usage** screen is the organization's picture of tool calls through -the Connector Gateway, the counterpart to its model spend. +The **Tool usage** screen summarizes calls through the Connector Gateway. ## What it shows -Across the top, three figures for the selected window: **Tool calls**, **Active -connectors**, and **Top connector**. A time-range selector controls the window, -and **Tool calls trend** shows the longer arc. +Select a time range to view **Tool calls**, **Active connectors**, **Top +connector**, and the tool call trend. -Below that, a breakdown with two tabs: **By connector**, showing calls and share -per connector, and **By tool**, showing the same per individual tool. +Use **By connector** to compare traffic across integrations. Use **By tool** to +identify the capabilities receiving traffic. -## Reading it +The screen reports tool calls only. Review model traffic in the AI Gateway. -The two tabs answer different questions. **By connector** tells you which -integrations are earning their keep, which is what you want when deciding -whether to keep maintaining one. **By tool** tells you which specific -capabilities people actually use, which is more useful when tuning what a -connector exposes. - -A connector registered but absent from this list is not being used at all. -Either nobody has been granted access, nobody has connected a client, or it is -not serving. The [Connectors](./connectors.mdx) screen's group-access column -usually distinguishes the first from the others. - -## Tool calls, not model calls - -This screen counts tool calls through the Connector Gateway. It does not count -model requests, and the two are not comparable: a single conversation might make -one model call and a dozen tool calls, or the reverse. +If a registered connector does not appear, check its state and group grants on +the [Connectors](./connectors.mdx) screen. Then confirm that clients can reach +the Connector Gateway. ## Next steps - [Connectors](./connectors.mdx) to change which connectors exist and who may reach them. -- [Forward audit logs](../ai-gateway/forward-audit-logs.mdx) if you need this - data in your own systems rather than in the console. diff --git a/docs/platform/concepts/two-group-models.mdx b/docs/platform/concepts/two-group-models.mdx index f5e3cf9d..2d8bb7fb 100644 --- a/docs/platform/concepts/two-group-models.mdx +++ b/docs/platform/concepts/two-group-models.mdx @@ -1,79 +1,49 @@ --- -title: The two group models -sidebar_label: The two group models +title: Directory groups and OIDC claim groups +sidebar_label: Group models description: - Connector access uses directory groups, cluster authorization policy uses OIDC - claim groups, and the platform does not link them. + Compare the group models used for connector access, budgets, and cluster + authorization policy. --- -Stacklok Enterprise has two things called groups, and they are not the same -thing. They are maintained separately, matched differently, and the platform -does not keep them in sync. Knowing which one a given control uses is the -difference between a policy that works and one that silently does nothing. +Stacklok Enterprise uses directory groups for connector access and budgets. It +uses OpenID Connect (OIDC) claim groups for cluster authorization policy. Choose +the group model that matches the control you are configuring. -## Directory groups +| Control | Group model | +| ----------------------------------------------------- | ---------------- | +| Connector access | Directory group | +| AI Gateway budget | Directory group | +| `PlatformRoleBinding` or `ClusterPlatformRoleBinding` | OIDC claim group | +| `ToolhiveAuthorizationPolicy` | OIDC claim group | -A directory group is a record in the directory service, with its own identifier, -a name, and an explicit membership list. You see and edit them in the console -under **User management**, and they can also be provisioned from your identity -provider over SCIM. +## Directory groups -**Directory groups decide connector access.** When a developer's MCP client asks -what tools it can reach, the Connector Gateway resolves the caller to a -directory user, reads that user's directory group memberships, and exposes only -the connectors granted to those groups. Adding someone to a group in the console -changes what their client can see. +A directory group is a record in the directory service with an identifier, name, +and membership list. Administrators can manage directory groups in the console +or provision them from an identity provider through System for Cross-domain +Identity Management (SCIM). -They also carry budgets. A group budget is addressed by the directory group's -identifier. +The Connector Gateway resolves each caller to a directory user and uses their +group memberships to determine connector access. AI Gateway group budgets also +reference the directory group's identifier. ## OIDC claim groups -An OIDC claim group is just a string that appears in the groups claim of the -caller's token. It has no record anywhere in the platform; it exists only as -text inside a token your identity provider issued. - -**Claim groups decide cluster-level authorization.** The `PlatformRoleBinding`, -`ClusterPlatformRoleBinding`, and `ToolhiveAuthorizationPolicy` resources name -groups as plain strings, and those strings are matched against the token's -claim. Nothing on that path consults the directory. - -## Why this matters - -Because the two are independent, all of the following are true at once: +An OIDC claim group is a string in the caller's token. The +`PlatformRoleBinding`, `ClusterPlatformRoleBinding`, and +`ToolhiveAuthorizationPolicy` resources match these strings when evaluating +cluster authorization policy. -- Adding a user to a directory group in the console **does** change their - connector access, and **does not** change what any `PlatformRoleBinding` - grants them. -- Adding a user to a group in your identity provider **does** change what claim - groups their next token carries, and **does not** create directory membership - unless SCIM is provisioning groups. -- Two groups with the same name in both systems are a coincidence you are - maintaining, not a link the platform enforces. - -The failure this produces is quiet. Someone adds a colleague to the Engineering -group in the console, the colleague still cannot call a tool governed by a -cluster authorization policy, and nothing anywhere reports an error, because -both systems behaved exactly as configured. - -## Which one am I using? - -| If you are configuring | You are naming | -| ----------------------------------------------------- | ------------------- | -| Connector access, in the console | A directory group | -| A budget, in the console | A directory group | -| `PlatformRoleBinding` or `ClusterPlatformRoleBinding` | An OIDC claim group | -| `ToolhiveAuthorizationPolicy` | An OIDC claim group | +These resources read group values directly from the token. Configure the +identity provider to include the expected values. ## Keeping them aligned -If you want one set of groups to govern both, provision directory groups from -the same identity provider groups you name in authorization policy, and keep the -names identical. SCIM group provisioning is what makes that maintainable, -because membership then tracks your provider on both sides. - -That alignment is a convention you choose and maintain. The platform will not -warn you when the two drift. +To use the same organizational groups for both control planes, provision +directory groups through SCIM from the identity provider that issues the OIDC +group claims. Keep the directory group names and claim values aligned. Stacklok +Enterprise evaluates the two group models independently. ## Related information diff --git a/docs/platform/concepts/what-you-can-see.mdx b/docs/platform/concepts/what-you-can-see.mdx deleted file mode 100644 index ae73cf39..00000000 --- a/docs/platform/concepts/what-you-can-see.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: What you see depends on what is installed -sidebar_label: What you can see -description: - The console shows the features whose backing services are installed and - reachable, so a missing section usually means a component is not deployed. ---- - -Stacklok Enterprise has one tier. There are no editions, no per-feature -entitlements, and no administrator-facing feature flags to switch things on. - -What any given person sees in the console is decided by two things: which -components you installed, and what that person is allowed to do. Nothing else. - -## Why a section is missing - -Each area of the console is backed by a service. The navigation only offers an -area when its service is both **installed** and **reachable**, and the route -behind it is gated the same way, so a hidden link and its page can never -disagree. Follow a stale bookmark to an area whose service is not installed and -you get a not-found response rather than a broken page. - -The practical consequence: if an administrator cannot see a section they expect, -the answer is almost never permissions. It is that the component behind it is -not enabled in the platform chart, or it is enabled but not answering. - -Which component that is will not always match how the navigation groups things. -Administering connectors, for example, reads from the directory rather than from -the Connector Gateway, because the connector records and their group grants live -there and the gateway only serves them. So when a section is missing, work -outwards from what the section actually reads rather than from the heading it -sits under. - -Both gateways are off by default, and are the most common answer. See -[Configure the AI Gateway](../enterprise-platform/configure-ai-gateway.mdx) and -[Configure the Connector Gateway](../enterprise-platform/configure-connector-gateway.mdx). - -## Why an area is visible but empty - -Installed and reachable is not the same as configured. A visible area with no -content usually means the service is running and has nothing to show yet: no -connectors registered, no budgets created, no traffic recorded. The console says -so in place rather than hiding the section, because an empty list and an -uninstalled component are different problems with different fixes. - -## What this means for rollout - -Because visibility follows installation, you can bring the platform up in -stages, and each stage is self-describing: install a component, and its areas -appear for the people entitled to them. There is no separate step to reveal a -feature after deploying it, and no flag to forget. - -## Related information - -- [The two group models](./two-group-models.mdx), the other thing that decides - what someone can reach. -- [Deploy the platform](../enterprise-platform/deployment.mdx) for the install - sequence and its toggles. diff --git a/docs/platform/enterprise-authz/index.mdx b/docs/platform/enterprise-authz/index.mdx index 5e5d4f2d..42638763 100644 --- a/docs/platform/enterprise-authz/index.mdx +++ b/docs/platform/enterprise-authz/index.mdx @@ -1,19 +1,16 @@ --- title: Enterprise authorization description: - Express MCP access control in familiar terms without learning a new policy - language. Namespace self-service for the teams that own the servers. + Define fleet-wide MCP roles and delegate server-specific access grants to + namespace administrators. --- import DocCardList from '@theme/DocCardList'; :::enterprise -Enterprise authorization lets cluster admins express MCP access control in -familiar terms, without learning a domain-specific policy language. - -Namespace owners can grant access to MCP servers they own, so the platform team -isn't in the loop on every change. +Enterprise authorization lets cluster admins define reusable MCP roles and lets +namespace admins grant those roles on the servers they manage. [Learn more about Stacklok Enterprise](../index.mdx). diff --git a/docs/platform/enterprise-authz/intro.mdx b/docs/platform/enterprise-authz/intro.mdx index a570b9c5..e313815d 100644 --- a/docs/platform/enterprise-authz/intro.mdx +++ b/docs/platform/enterprise-authz/intro.mdx @@ -13,36 +13,16 @@ comparison of ToolHive Community and Stacklok Enterprise capabilities, see ::: -At small scale, hand-writing a Cedar policy for each MCP server works fine. At -fleet scale, with many servers, many teams, and an identity-provider-driven -identity model, that approach falls apart. Enterprise authorization turns access -control into a set of declarative custom resources that the operator compiles -into the Cedar policies ToolHive enforces at runtime. - -## The problem - -Open source ToolHive supports rich Cedar policies on every MCP server, and that -flexibility is exactly what you want when you are securing one or two servers. -Once you are running a fleet, the same flexibility becomes a liability. Each -server carries its own policy file. The policies drift. There is no clean place -to express something as simple as "every engineer can read every server." - -The model also conflates two things that should be separate. A Cedar policy -defines _what_ a role can do _and_ _who_ holds that role, all in one document. -That works for a single team that owns a single server. It does not work when a -platform team needs to define a reusable role and a namespace owner needs to -grant that role to their own users without rewriting the platform team's policy. - -The CRDs introduced by enterprise authorization separate these concerns. Cluster -admins define roles once. Cluster admins or namespace owners bind those roles to -identity-provider groups and roles. A separate attachment object decides which -MCP servers a binding applies to. The operator compiles the combination into -Cedar. +Enterprise authorization represents MCP access as declarative Kubernetes custom +resources. Platform teams define reusable roles, and platform or namespace +admins bind those roles to identity-provider groups and MCP servers. The +operator compiles the resources into the Cedar policies ToolHive enforces at +runtime. ## The model -Enterprise authorization is built on a small set of custom resources. If you -have used Kubernetes RBAC, the shape will feel familiar. +Enterprise authorization uses four custom resources modeled on Kubernetes +role-based access control (RBAC). - **`ClusterPlatformRole`** ([reference](../reference/crds/clusterplatformrole.mdx)) defines _what_ a role @@ -112,10 +92,10 @@ OIDC provider, see ## Built-in roles -The operator ships two `ClusterPlatformRole` objects out of the box. +The operator includes two `ClusterPlatformRole` objects. - `reader` grants the MCP read operations and `call_tool`. The `call_tool` - surface can be narrowed at bind time with `toolHintFilter.readOnlyHint: true`, + actions can be narrowed at bind time with `toolHintFilter.readOnlyHint: true`, which restricts the binding to tools the MCP server itself annotates as read-only. - `writer` grants every action via wildcard. diff --git a/docs/platform/enterprise-authz/namespace-self-service.mdx b/docs/platform/enterprise-authz/namespace-self-service.mdx index c9b53021..61d13190 100644 --- a/docs/platform/enterprise-authz/namespace-self-service.mdx +++ b/docs/platform/enterprise-authz/namespace-self-service.mdx @@ -1,21 +1,19 @@ --- -title: Namespace self-service authorization +title: Delegate authorization by namespace description: - Hand authorization authoring to the team that owns the MCPServer, using - PlatformRoleBinding inside their own namespace. + Let namespace administrators grant approved roles on the MCP servers they + manage. --- -This guide is for a team that owns an MCPServer in its own namespace and wants -to author authorization grants without involving the cluster admin. The cluster -admin still owns the role catalog; the team owns the bindings inside its -namespace. +Use `PlatformRoleBinding` to let namespace administrators grant approved roles +on the MCP servers they manage. Cluster admins maintain the role catalog, while +namespace admins maintain bindings in their namespaces. ## When to use it -Use namespace self-service when the team owns the workload, has an IdP group of -its own, and wants access changes to land in the same manifests as the MCPServer -itself. The cluster admin maintains a `ClusterPlatformRole` catalog; teams pick -from it rather than minting new verb sets. +Use delegated authorization when a team owns the workload and has its own IdP +group. The team can manage access in the same manifests as its `MCPServer` by +selecting roles from the cluster admin's `ClusterPlatformRole` catalog. ## How the trust model works @@ -132,9 +130,9 @@ Expected outcome: tool calls that carry the `readOnlyHint` annotation return principal set was contributed entirely by the team's `PlatformRoleBinding`; no cluster-wide binding had to change. -## What namespace owners cannot do +## Delegation boundaries -Self-service is bounded. A namespace owner cannot: +Namespace admins have these restrictions: - **Subtract a cluster-wide grant.** If a `ClusterPlatformRoleBinding` grants a role to a principal that also matches a `ToolhiveAuthorizationPolicy` in the @@ -149,14 +147,13 @@ Self-service is bounded. A namespace owner cannot: `team-alpha`. Cross-namespace effects require a `ClusterPlatformRoleBinding`, which only the cluster admin can author. -## Guard rails the cluster admin can enable +## Limit the available roles The access gate on namespace self-service is namespace-level Kubernetes RBAC on -`platformrolebindings` and `toolhiveauthorizationpolicies`. A team that holds -those verbs can reference any `ClusterPlatformRole` in the cluster, including -high-power roles published for other teams. Treat the `ClusterPlatformRole` -catalog as a curated allow-list, and publish fewer, narrower roles when in -doubt. +`platformrolebindings` and `toolhiveauthorizationpolicies`. A team with those +permissions can reference any `ClusterPlatformRole` in the cluster. Publish a +curated catalog of narrowly scoped roles that are safe for namespace admins to +assign. ## Next steps diff --git a/docs/platform/enterprise-authz/quickstart-entra.mdx b/docs/platform/enterprise-authz/quickstart-entra.mdx index e4d9e2b6..3467bd99 100644 --- a/docs/platform/enterprise-authz/quickstart-entra.mdx +++ b/docs/platform/enterprise-authz/quickstart-entra.mdx @@ -5,13 +5,9 @@ description: authorization with a compiled ToolhiveAuthorizationPolicy. --- -By the end of this walkthrough, you'll stand up an Entra ID-authenticated GitHub -MCP server, attach a policy that gives one user full write access and another -user read-only access, and verify the split with live tool calls. The reader is -restricted by a tool annotation filter so the same `reader` -`ClusterPlatformRole` can be reused elsewhere with different scoping. An -optional final step demonstrates a second pattern: narrowing a broad role to a -named allow-list of tools at bind time. +Deploy an Entra ID-authenticated GitHub MCP server, give one test user write +access and another read-only access, then verify both policies with live tool +calls. An optional step restricts a third user to a named list of tools. ## What you'll learn @@ -172,8 +168,8 @@ kubectl -n authz-demo wait --for=condition=Ready \ `oidcConfigRef.audience` must match the `aud` claim Entra puts in your access tokens. The manifest above uses the client GUID, which is the v2 default. If your tenant is configured to emit the Application ID URI (`api://`) -instead, set `audience` to that string. If you hit `401` errors later, step 9 -covers how to inspect a token and fix the mismatch. +instead, set `audience` to that string. If you receive a `401` response, see +[Troubleshooting](#troubleshooting). ::: @@ -226,17 +222,16 @@ spec: kubectl apply -f 10-trb-admin-viewer.yaml ``` -This `ClusterPlatformRoleBinding` says: any caller whose JWT carries -`roles: ["mcp-admin"]` gets the `writer` role, and any caller with -`roles: ["mcp-viewer"]` gets the `reader` role. The binding is cluster-scoped -and doesn't yet apply to any server; that's the job of the next step. +This `ClusterPlatformRoleBinding` grants the `writer` role to callers whose JWT +carries `roles: ["mcp-admin"]` and the `reader` role to callers whose JWT +carries `roles: ["mcp-viewer"]`. The binding is cluster-scoped and applies after +a `ToolhiveAuthorizationPolicy` attaches it to a server. ## Step 5: Apply the per-server policy with reader and writer The `ToolhiveAuthorizationPolicy` attaches roles to a specific MCP server. This -policy binds the `writer` role unrestricted, and binds the `reader` role with a -`toolHintFilter` that limits the binding to tools that declare the MCP -`readOnlyHint` annotation. +policy binds the unrestricted `writer` role and uses a `toolHintFilter` to limit +the `reader` binding to tools that declare the MCP `readOnlyHint` annotation. ```yaml title="11-tap-reader-writer.yaml" apiVersion: toolhive.enterprise.stacklok.com/v1alpha1 @@ -258,11 +253,8 @@ spec: readOnlyHint: true ``` -`targetRef.kind` defaults to `MCPServer`, so it's omitted. `toolHintFilter` -lives on the binding rather than on the role itself: the same `reader` role can -be reused in a different policy without a filter, or with a different filter -like `destructiveHint: false`. The role describes the verbs; the binding decides -which tools at the target satisfy those verbs. +`targetRef.kind` defaults to `MCPServer`. The role defines the actions, and the +binding uses `toolHintFilter` to select the tools allowed at this target. Apply the policy and wait for the operator to compile it: @@ -281,10 +273,9 @@ into the Cedar policy bundle the proxy will enforce. If it doesn't compile, see Point an MCP client at the server using each user's token, and call a read tool (`list_issues`) and a write tool (`issue_write`) to see the policy in action. -In a real deployment, clients reach the MCP server through the ingress or -gateway your platform exposes, and you point the client at that URL. For a quick -local check against this quickstart, forward the proxy Service to your machine. -This is a validation convenience, not a production access path: +For this local check, forward the proxy Service to your machine. In production, +configure clients with the MCP server URL exposed through your ingress or +gateway. ```bash kubectl -n authz-demo port-forward svc/mcp-github-demo-proxy 18080:8080 @@ -363,16 +354,15 @@ server: | `mcp-admin` | succeeds | succeeds (issue created) | | `mcp-viewer` | succeeds | denied | -The viewer's denial surfaces as a transport error carrying the proxy's `403`: +The client reports the proxy's `403` response as a transport error: ```text Failed to call tool issue_write: Streamable HTTP error: Error POSTing to endpoint: {"Result":null,"Error":{"code":403,"message":"Unauthorized"},"ID":{}} ``` -That 403 is the policy doing its job: `issue_write` does not carry the -`readOnlyHint` annotation, so the reader binding's `toolHintFilter` excludes it, -while the read-only `list_issues` is allowed. +The proxy rejects `issue_write` because it lacks the `readOnlyHint` annotation. +The reader binding permits `list_issues`, which carries the annotation. :::note[Tool names track the server version] @@ -384,11 +374,9 @@ This guide targets `github-mcp-server:v1.0.3`, where issue writes go through the ## Step 7: (Optional) Give a reviewer access to only the tools they need -A code reviewer doesn't need every tool the GitHub MCP server exposes, just the -ones for reading pull requests, searching code, and listing issues. Grant them -exactly that set by listing tool names in `ruleRestrictions.tools` on the -binding. The underlying `code-reviewer` role can still be reused elsewhere with -a different tool list; the per-target policy decides the narrowing. +List the tools a code reviewer needs in `ruleRestrictions.tools` on the binding. +The following policy permits reading pull requests, searching code, and listing +issues. ```yaml title="20-platformrole-code-reviewer.yaml" apiVersion: platform.enterprise.stacklok.com/v1alpha1 @@ -471,9 +459,9 @@ narrows the tool list to exactly the three names in `ruleRestrictions.tools`. ## Step 8: Clean up Deleting the namespace removes the MCPServer, OIDC config, secret, and -namespaced `ToolhiveAuthorizationPolicy` in one shot. The two cluster-scoped -role bindings (and the custom `code-reviewer` role from the optional step) live -outside the namespace, so delete them explicitly: +namespaced `ToolhiveAuthorizationPolicy`. The two cluster-scoped role bindings +(and the custom `code-reviewer` role from the optional step) live outside the +namespace, so delete them explicitly: ```bash kubectl delete clusterplatformrolebinding \ @@ -484,8 +472,8 @@ kubectl delete namespace authz-demo ## Next steps -- Hand a namespace and a scoped policy surface to an application team with - [Namespace self-service](./namespace-self-service.mdx). +- Delegate access grants to an application team with + [namespace authorization](./namespace-self-service.mdx). - See every field on the policy resource in the [ToolhiveAuthorizationPolicy CRD reference](../reference/crds/toolhiveauthorizationpolicy.mdx). - Understand what the operator compiles your `ToolhiveAuthorizationPolicy` into diff --git a/docs/platform/enterprise-cli/index.mdx b/docs/platform/enterprise-cli/index.mdx index 960f4f0d..35cb3a26 100644 --- a/docs/platform/enterprise-cli/index.mdx +++ b/docs/platform/enterprise-cli/index.mdx @@ -13,15 +13,12 @@ ToolHive Community and Stacklok Enterprise capabilities, see ::: -The Stacklok CLI is the enterprise edition of the -[ToolHive CLI](../../toolhive/guides-cli/index.mdx) (`thv`). Everything in the -open source `thv` works the same way. The Stacklok CLI adds OIDC authentication -to your Stacklok Enterprise platform and enforces the policies your -administrators define in the +The Stacklok CLI extends the [ToolHive CLI](../../toolhive/guides-cli/index.mdx) +(`thv`) with OIDC authentication and policies distributed by the [Enterprise Manager](../enterprise-manager/index.mdx). -This page covers what the enterprise edition adds. For the base CLI workflows, -see the [ToolHive CLI](../../toolhive/guides-cli/index.mdx) guides. +For base CLI workflows, see the +[ToolHive CLI](../../toolhive/guides-cli/index.mdx) guides. ## How you get it @@ -38,13 +35,10 @@ The CLI needs your platform URL before it can authenticate. It reads preferences property list or the Windows registry, then falls back to the `STACKLOK_PLATFORM_URL` environment variable. -Managed preferences are the option to prefer for a fleet: setting that one value -through your device management tooling configures every installation, so nobody -has to be told an address. +For fleet deployment, set this value through device management. -From that URL, the CLI discovers the OIDC issuer, client ID, and scopes from the -platform's well-known configuration endpoint. You do not configure those by -hand. +The CLI discovers the OIDC issuer, client ID, and scopes from the platform's +well-known configuration endpoint. ## Authentication @@ -73,7 +67,7 @@ carries an `enforcement` level: `enforced` directives are mandatory, while `default` directives set a value you can still override locally. See [Enforcement levels](../enterprise-manager/policies/index.mdx#enforcement-levels). -Two directives shape what the CLI can do: +Two directives control which MCP servers the CLI can run: - **[Registry](../enterprise-manager/policies/registry.mdx).** When the registry directive is enforced, the configured registry URL is locked. Attempts to diff --git a/docs/platform/enterprise-console/index.mdx b/docs/platform/enterprise-console/index.mdx index 06e4d066..b8029f43 100644 --- a/docs/platform/enterprise-console/index.mdx +++ b/docs/platform/enterprise-console/index.mdx @@ -1,47 +1,25 @@ --- -title: The console -sidebar_label: The console +title: Console +sidebar_label: Console description: - Sign in to the Stacklok Enterprise console, and find where each of its areas - is documented. + Administer budgets, connectors, users, groups, and identity providers in the + Stacklok Enterprise console. --- -The console is the platform's web interface. It has two experiences, and which -one you land in depends on what you are allowed to do. - -**Your workspace** is about your own work: the connectors available to you, your -own model and tool usage, your own API keys, and the setup instructions for -pointing your tools at the gateways. - -**Administration** is about the organization: what it is spending, what the -budgets are, which connectors exist and who may use them, and who your users and -groups are. - -If you have administrative access, you can switch between the two from the -account menu. If you do not, you have the one, which is a permissions boundary -rather than a preference. +The console is the administrative web interface for Stacklok Enterprise. Use it +to manage budgets, connectors, directory records, identity providers, and other +organization-wide settings. ## Signing in -Open the console at the address your platform administrator gave you and sign in -with your normal corporate credentials. The console uses the identity provider -configured for the platform, so there is no separate account to create. - -## Getting administrative access +Open the console URL for your deployment and authenticate through the configured +identity provider. Administration requires the platform admin grant, which +covers budgets, connectors, and directory administration. Administrators can +switch between **Administration** and **Your workspace** from the account menu. -Administration requires the platform admin grant. It is one shared grant rather -than a role per feature, so the same grant covers budgets, connectors, and -directory administration. +## Administrative areas -## Finding your way around - -The console shows the areas whose backing services are installed and reachable. -A missing section is usually a component that is not deployed rather than a -permission you are lacking. See -[What you see depends on what is installed](../concepts/what-you-can-see.mdx). - -Each administrative area is documented with the component behind it rather than -with the console: +Each administrative area is documented with the component that provides it: - [Budgets](../../ai-gateway/manage-budgets.mdx), with the AI Gateway that enforces them. @@ -52,3 +30,22 @@ with the console: [Identity providers](../enterprise-directory/identity-providers.mdx), and [Managed secrets](../enterprise-directory/managed-secrets.mdx), with the directory that stores them. + +## Troubleshooting + +
+An administrative area is missing + +Confirm that its backing service is enabled, reachable, and ready. The console +displays only the areas provided by running services. + +
+ +
+An administrative area is empty + +The backing service is available but has no configured resources or recorded +activity. Add the relevant configuration or generate traffic, depending on the +area. + +
diff --git a/docs/platform/enterprise-directory/identity-providers.mdx b/docs/platform/enterprise-directory/identity-providers.mdx index 0b5fa5db..eac0a76a 100644 --- a/docs/platform/enterprise-directory/identity-providers.mdx +++ b/docs/platform/enterprise-directory/identity-providers.mdx @@ -6,18 +6,14 @@ description: and understand the read-only corporate provider entry. --- -An identity provider record here describes an upstream service that a -**connector** authenticates against. An administrator configures these -separately from how users sign in to the platform. - -Both concepts exist and it is easy to conflate them: +An identity provider record describes an upstream service that a connector +authenticates against. This is separate from platform sign-in: - **Your corporate identity provider** signs users in to the platform. It is set once at install time. See [Configure identity](../enterprise-platform/configure-identity.mdx). -- **Connector identity providers**, on this page, are the services a connector - brokers OAuth against so a developer's tool call can reach a backend on their - behalf. +- **Connector identity providers** let the Connector Gateway broker OAuth to + upstream services. ## Where to find them @@ -25,41 +21,24 @@ In the admin console, go to **Identity providers**. ## The corporate provider appears here, read-only -Your corporate identity provider is listed first, marked as coming from platform -configuration rather than from an administrator. It appears for a practical -reason: connectors can authenticate against it, so an administrator needs to be -able to confirm it is configured at all. - -It cannot be edited or deleted from this screen. Attempting to do so is refused -with a message naming the chart value that governs it, rather than failing -quietly. To change it, change your platform values and upgrade. +The console lists the corporate identity provider as a read-only record sourced +from platform configuration. To change it, update the platform values and +upgrade the release. -Its client secret is sealed into managed-secret storage at startup, so -connectors resolve it through the same mechanism as every other provider's -credential. Because that happens at startup, rotating the underlying secret -takes effect on the next restart rather than immediately. +At startup, the directory copies its client secret into managed-secret storage. +Restart the directory after rotating the source Secret. -If the corporate provider's secret cannot be sealed, that one record is absent -and the condition is logged. It does not stop the directory from starting, on -the grounds that one unusable provider record should not take down identity -resolution for everything else. +If the directory cannot store the secret, it logs the error and omits the +provider record while continuing to serve other directory operations. ## Register a provider -Adding a provider needs its issuer, its client identifier, and a reference to a -[managed secret](./managed-secrets.mdx) holding the client secret. Create the -managed secret first; the provider record points at it rather than holding the -credential itself. - -You do not supply a redirect URI. The platform derives every provider's callback -from the Connector Gateway's own issuer, as `{issuer}/oauth/callback`, and -ignores any redirect URI on the record. The page shows the derived callback -read-only, with a control to copy it. +Create a [managed secret](./managed-secrets.mdx) for the client secret. Then add +the provider issuer, client identifier, and managed-secret reference. -Copying it is a required step, not a convenience: **register that exact URI in -the upstream provider's OAuth application**, among its authorized redirect URIs. -Until you do, every login through the provider fails on a `redirect_uri` -mismatch. +The console displays the callback derived from the Connector Gateway issuer: +`{issuer}/oauth/callback`. Register this URI in the upstream provider's OAuth +application. A mismatch causes the upstream provider to reject authorization. Once registered, a provider becomes selectable when you configure a connector's authentication. diff --git a/docs/platform/enterprise-directory/index.mdx b/docs/platform/enterprise-directory/index.mdx index 35a30c1b..7e9ffa95 100644 --- a/docs/platform/enterprise-directory/index.mdx +++ b/docs/platform/enterprise-directory/index.mdx @@ -2,8 +2,8 @@ title: Identity and directory sidebar_label: Identity and directory description: - Manage the users, groups, identity providers, secrets, and API keys that the - rest of Stacklok Enterprise makes decisions about. + Manage the users, groups, identity providers, secrets, and API keys used by + Stacklok Enterprise controls. --- import DocCardList from '@theme/DocCardList'; @@ -16,56 +16,43 @@ comparison of ToolHive Community and Stacklok Enterprise capabilities, see ::: -The directory is the platform's record of who your people are and what they are -entitled to. Other components ask it rather than keeping their own copies: the -Connector Gateway asks which connectors a caller may reach, the AI Gateway's -budgets are addressed to directory users and groups, and the console's -administration screens are views onto it. +The directory stores identities and group memberships used by the Connector +Gateway and AI Gateway. Administrators manage these records through the console +or API. ## What it holds -| Thing | Used for | -| -------------------- | -------------------------------------------------------------- | -| Users | Resolving a token to a person, and addressing per-user budgets | -| Groups and subgroups | Granting connector access, and addressing per-group budgets | -| Identity providers | The upstream services connectors authenticate against | -| Managed secrets | Encrypted storage for the credentials those connections need | -| Virtual API keys | Long-lived keys that stand in for a person's own token | +| Record | Used for | +| -------------------- | -------------------------------------------------- | +| Users | Resolve tokens and assign user budgets | +| Groups and subgroups | Grant connector access and assign group budgets | +| Identity providers | Broker OAuth from connectors to upstream services | +| Managed secrets | Store connector and identity provider credentials | +| Virtual API keys | Authenticate automated clients as a directory user | ## How identity is resolved -A caller arrives with a token. The directory maps the token's issuer and subject -to a user record, then reads that user's group memberships, including groups -inherited through subgroups. Downstream components receive the resolved user and -group identifiers rather than raw token claims. - -A caller with no matching user record resolves to nothing. That is not an error -in itself, but controls that key on platform identity will not fire for them, -and controls that fail closed will refuse them. +The directory maps the issuer and subject in a caller's token to a user record. +It returns the user's direct and inherited group memberships to the requesting +component. Controls that require a directory identity deny callers without a +matching record. :::warning -Directory groups are **not** the same as the OIDC claim groups named in -cluster-level authorization policy, and the platform does not keep the two in -sync. See [The two group models](../concepts/two-group-models.mdx) before -configuring either. +Connector access and budgets use directory groups. Cluster authorization policy +uses OIDC claim groups. See +[Directory groups and OIDC claim groups](../concepts/two-group-models.mdx). ::: ## Where you administer it -The console is the place to do this day to day. **User management** covers -users, groups, and subgroups; **Identity providers** and **Managed secrets** -have their own screens; and every user manages their own keys under **API -keys**. - -Administration requires the shared platform admin grant. There is no separate -role per module, so the same grant covers directory administration and budget -administration. +Use **User management** for users, groups, and subgroups. Use **Identity +providers** and **Managed secrets** for connector authentication. Users manage +their virtual keys under **API keys** in **Your workspace**. -Each of those screens has an API behind it, under `/v1`, for automation and bulk -work. Every authenticated caller can also read their own records under `/v1/me` -without an administrative grant, which is what the end-user screens use. +The platform admin grant covers directory and budget administration. Use the +`/v1` API for automation and bulk changes. ## Contents diff --git a/docs/platform/enterprise-directory/managed-secrets.mdx b/docs/platform/enterprise-directory/managed-secrets.mdx index 1a00c5ae..1867aa09 100644 --- a/docs/platform/enterprise-directory/managed-secrets.mdx +++ b/docs/platform/enterprise-directory/managed-secrets.mdx @@ -6,29 +6,22 @@ description: key that protects them. --- -A managed secret is a credential the directory stores on your behalf, encrypted -at rest. Connector configuration and identity provider records reference managed -secrets rather than holding credentials themselves, so a credential is written -once and never returned in a read. +A managed secret stores an encrypted connector or identity provider credential. +The API accepts secret values on writes and omits them from reads. ## Where to find them In the console, go to **Managed secrets**. -The list shows each secret's name and when it was last updated. Values are never -displayed, and never returned by the API either. To change a credential you -replace the value; to find out what a value currently is, go to the system that -issued it. +The list shows each secret's name and last update time. Replace a secret to +change its value. Retrieve the source value from the system that issued it. ## How they are protected -Each secret's value is encrypted with its own data key, and that data key is -itself encrypted with a key-encryption key held in a Kubernetes Secret rather -than in the database. Both layers use AES-256. - -The practical consequence: a copy of the database alone does not yield the -credentials. Recovering them requires the key-encryption key as well, which -lives in a different place with different access controls. +The directory encrypts each value with a data key, then encrypts that data key +with a key-encryption key from a Kubernetes Secret. Both layers use AES-256. +Back up the Kubernetes Secret with the directory database so you can recover the +stored credentials. ## Rotating the key-encryption key @@ -43,24 +36,19 @@ encrypt new values: } ``` -To rotate, **add** a new highest-numbered entry and keep the old ones. Existing -secrets record which version protected them, so old entries are still needed to -read them. Removing a version makes every value encrypted under it unreadable. +To rotate the key, add a new highest-numbered entry and retain the earlier +versions. The directory needs each previous version to decrypt values written +with it. -Each key must be exactly 32 bytes before base64 encoding. A shorter key is -accepted by the underlying cipher at a weaker strength rather than rejected, -which is why the platform checks the length itself and refuses to start on a -mismatch instead of silently downgrading. +Each key must contain exactly 32 bytes before base64 encoding. The directory +refuses to start when the decoded length differs. -Keys are read once at startup, so a rotation takes effect when the directory -restarts, matching how the rest of its configuration reloads. +Restart the directory after adding a key version. ## Re-encrypting after rotation -Adding a new version does not rewrite existing secrets. They stay under their -original version until their values are next written, at which point they pick -up the current key. To move a specific credential onto the new key immediately, -update its value. +Existing values retain their original key version until the next write. Update a +credential to re-encrypt it immediately with the current version. ## Next steps diff --git a/docs/platform/enterprise-directory/scim-provisioning.mdx b/docs/platform/enterprise-directory/scim-provisioning.mdx index e5fa72bd..a0934ccb 100644 --- a/docs/platform/enterprise-directory/scim-provisioning.mdx +++ b/docs/platform/enterprise-directory/scim-provisioning.mdx @@ -1,30 +1,24 @@ --- -title: SCIM provisioning +title: Configure SCIM provisioning sidebar_label: SCIM provisioning description: Provision directory users and groups automatically from Okta, Entra ID, or any SCIM 2.0 identity provider. --- -SCIM lets your identity provider keep the directory current: users appear when -they join, group membership follows your provider's groups, and deactivation -propagates when someone leaves. The platform implements SCIM 2.0 +Use System for Cross-domain Identity Management (SCIM) to provision directory +users, groups, memberships, and deactivation from your identity provider. +Stacklok Enterprise implements SCIM 2.0 ([RFC 7644](https://datatracker.ietf.org/doc/html/rfc7644)) as a service -provider, so your provider pushes changes rather than the platform polling. +provider. -Provisioning is the recommended way to populate the directory. Records it -creates are marked with a SCIM source, which is how an administrator knows to -make edits in the identity provider rather than in the console. +SCIM records display their source in the console. Update these records in the +identity provider. ## Declare the issuer first -Provisioning is configured in the platform chart, not in the console. Before you -touch your identity provider, add an entry for it under `directory.issuers` and -create the Secret holding the token it will authenticate with. - -The token is **yours to generate**. Nothing issues it for you: the platform -compares what your provider sends against the value in this Secret, so create a -long random string and put it somewhere your provider can read it from too. +Add the provider under `directory.issuers` in the platform chart and create a +Secret containing a random bearer token: ```bash kubectl create secret generic directory-scim-token-okta \ @@ -32,7 +26,7 @@ kubectl create secret generic directory-scim-token-okta \ --from-literal=token="$(openssl rand -base64 32)" ``` -Then declare the issuer: +Declare the issuer: ```yaml title="values.yaml" directory: @@ -50,7 +44,7 @@ directory: key: 'token' ``` -| Field | What to put in it | +| Field | Value | | ----------------------- | ---------------------------------------------------------------------------------- | | `id` | A short name you choose. It becomes a path segment, so keep it URL-safe | | `issuer_url` | Your provider's OIDC discovery base URL, HTTPS only | @@ -64,21 +58,17 @@ provider carries that identifier somewhere else. ## The endpoint -Once the issuer is declared, its base URL is the `id` you chose: +The issuer's `id` determines its base URL: ```text https:///scim//v2 ``` -Point your identity provider's provisioning configuration at that base, and give -it the bearer token from the Secret. The `/Users` and `/Groups` collections sit -beneath it, along with the standard `/ServiceProviderConfig`, `/Schemas`, and -`/ResourceTypes` discovery endpoints. +Configure the identity provider with this base URL and the bearer token from the +Secret. Standard discovery endpoints and the `/Users` and `/Groups` collections +are available beneath the base path. -Scoping the path per issuer means two identity providers can provision into the -same directory without colliding, and a record always carries the issuer it came -from. Adding a second provider is another entry in `issuers` with its own `id` -and its own Secret. +Give each additional provider its own `issuers` entry, `id`, and Secret. ## What is supported @@ -92,18 +82,14 @@ and its own Secret. | ETags | No | | | Password change | No | Credentials stay with your identity provider | -Your provider reads these from `/ServiceProviderConfig` and adapts on its own, -so there is nothing to configure for the unsupported items. Bulk is the only one -worth knowing about in advance: a provider that would have batched will issue -individual requests instead, which is slower on a large initial sync but -otherwise equivalent. +Providers read these capabilities from `/ServiceProviderConfig`. Large initial +syncs use individual requests because the service does not support SCIM bulk +operations. ## How records are matched -A user is keyed on the external identifier your provider sends, scoped to the -issuer. That is deliberately not the email address: people change names and -addresses, and matching on a stable provider-side identifier means a rename -updates the existing record instead of creating a second one. +The directory matches a user by external identifier and issuer. Use a stable +provider identifier so email or name changes update the existing record. Group membership can reference both users and other groups, so a nested group structure in your provider arrives as @@ -122,48 +108,60 @@ structure in your provider arrives as 6. Push an initial sync, then confirm in the console under **User management** that users and groups appear with a SCIM source. -Start with a small test group rather than your whole directory. The initial sync -is the step most likely to surface a mapping problem, and it is much easier to -read the result on ten records than on ten thousand. +Start with a small test group to validate mappings before a full sync. ## After provisioning -Group names arriving from your provider are what you will grant connector access -to and address budgets against, so it is worth agreeing on them before wide -rollout. Renaming a group later is safe, but every grant and budget referencing -it needs revisiting. +Connector grants and budgets reference the provisioned directory groups. Define +a stable naming convention before rollout and update references after renaming a +group. -If you also use cluster-level authorization policy, provisioning from the same -provider groups you name in that policy is what keeps the -[two group models](../concepts/two-group-models.mdx) aligned. That alignment is -a convention you maintain; the platform does not enforce it. +If you use cluster authorization policy, align SCIM directory groups with the +OIDC group claims used in policy. See +[Directory groups and OIDC claim groups](../concepts/two-group-models.mdx). ## Next steps - [Users and groups](./users-and-groups.mdx) for what these records govern. -- [Identity providers](./identity-providers.mdx), which is a different thing: - the upstream services connectors authenticate against, not the provider that - signs your users in. +- [Identity providers](./identity-providers.mdx) to configure upstream OAuth for + connectors. ## Troubleshooting -**Your provider cannot authenticate.** The token it sends is compared directly -against the Secret named in `scim_bearer_token_ref`, so the two must match -exactly. Check for a trailing newline in the Secret value, and that the Secret -is in the namespace the reference names. +
+Your provider cannot authenticate + +Confirm that the token matches the Secret named in `scim_bearer_token_ref`. +Check the Secret namespace and remove trailing newlines from the value. + +
+ +
+The endpoint is not found + +Use the issuer `id` from `directory.issuers` in the path and confirm that the +release includes your updated values. + +
+ +
+Users are provisioned but groups are missing + +Enable group provisioning after the initial user sync. + +
+ +
+User deactivation does not take effect + +Configure the provider to send deactivation. -**The endpoint is not found.** The path segment is the issuer's `id` from -`directory.issuers`, not your provider's name. Confirm the release has been -upgraded since you added the entry. +
-**Users provisioned but no groups.** Group provisioning is usually a separate -switch from user provisioning, and providers commonly require users to sync -first. +
+Duplicate users appear -**Deactivation not taking effect.** Check that your provider is configured to -send deactivation rather than deletion. A deactivated user resolves to nothing -here, which is the intended end state. +Check whether the external identifier changed in the provider. Reconcile the +records there, then resync. -**Duplicate users.** Almost always a changed external identifier on the provider -side, which the platform reads as a new person. Reconcile in the provider, then -resync. +
diff --git a/docs/platform/enterprise-directory/users-and-groups.mdx b/docs/platform/enterprise-directory/users-and-groups.mdx index ee0c88b6..eb8ea9d6 100644 --- a/docs/platform/enterprise-directory/users-and-groups.mdx +++ b/docs/platform/enterprise-directory/users-and-groups.mdx @@ -6,24 +6,19 @@ description: how inherited membership affects connector access and budgets. --- -Users and groups are the directory's core records. A user is a person the -platform can resolve a token to. A group is a named set of users, optionally -containing other groups, that access and budgets are granted to. +The directory resolves tokens to users. Directory groups organize those users +for connector grants and AI Gateway budgets. ## Where records come from -Each user and group carries a source, which tells you how it arrived and -therefore where to change it. +The source on each record identifies where to update it: -- **SCIM.** Provisioned from your identity provider. Membership and profile - fields track the provider, so edit them there rather than here. See - [SCIM provisioning](./scim-provisioning.mdx). -- **API.** Created directly through the platform, either in the console or over - REST. These are yours to maintain. +- **SCIM.** Update provisioned profile and membership data in the identity + provider. See [SCIM provisioning](./scim-provisioning.mdx). +- **API.** Update records created in the console or REST API through either + interface. -Both kinds work identically for access decisions. The distinction matters only -for deciding where an edit belongs, which is why the console shows the source on -every row. +Both sources work identically in access decisions. ## Administer users @@ -31,23 +26,17 @@ In the console, go to **User management**. The list shows each user's name, email, and **Source**, and can be filtered by group or status. Opening a user shows the budgets that apply to them. -Membership is edited from the group rather than from the user: use **Add -member** on a group's **Members** tab. +Edit membership from a group's **Members** tab. -Deactivating a user is the operative control, not deleting them. An inactive -user resolves to nothing, so controls that key on platform identity stop firing -for them and controls that fail closed refuse them. Their recorded usage and -charges remain, because those are history rather than entitlement. +Deactivate a user to prevent the directory from resolving their identity. This +preserves their recorded usage and charges. ## Administer groups -Groups live under the **Groups** tab of the same area, where **New Group** adds -one. A group has a name, an optional description, and a membership list. Open a -group for its **Members** tab, where **Add member** puts someone in it. - -Group names are worth choosing deliberately, because they are what you will name -in budgets and connector grants, and they are what someone will compare against -your identity provider's group names when they try to reconcile the two systems. +On the **Groups** tab, select **New Group** and provide a name and optional +description. Add users from the group's **Members** tab. Use group names that +align with your identity provider when SCIM and OIDC policies refer to the same +organizational groups. ## Subgroups and inherited membership @@ -64,12 +53,8 @@ flowchart TB Eng --> BE ``` -Grant a connector to **Engineering** and members of both subgroups can reach it. -That is the mechanism to prefer over granting each leaf group separately: it -keeps a single place to change access when a team reorganizes. - -Resolution walks the whole tree, so downstream components see a caller's full -inherited group set, not just their direct memberships. +Granting a connector to **Engineering** gives members of both subgroups access. +The directory returns direct and inherited membership to downstream components. ## What groups decide @@ -82,9 +67,8 @@ Both follow inherited membership. :::warning -These are directory groups. They are **not** the OIDC claim groups that -cluster-level authorization policy matches on, and changing one does not change -the other. See [The two group models](../concepts/two-group-models.mdx). +Cluster authorization policy uses OIDC claim groups. See +[Directory groups and OIDC claim groups](../concepts/two-group-models.mdx). ::: diff --git a/docs/platform/enterprise-directory/virtual-api-keys.mdx b/docs/platform/enterprise-directory/virtual-api-keys.mdx index 9b83f637..ae4b67cc 100644 --- a/docs/platform/enterprise-directory/virtual-api-keys.mdx +++ b/docs/platform/enterprise-directory/virtual-api-keys.mdx @@ -6,36 +6,23 @@ description: and CI can reach the gateways without an interactive sign-in. --- -A virtual API key lets someone authenticate to the gateways with a long-lived -key instead of an interactive sign-in. It is the right mechanism for a script, a -continuous integration job, or a tool that cannot complete a browser flow. - -The key is not a shared service credential. Each key is bound to the identity of -the person who created it, and when a request presents one, the platform -resolves it back to that identity. Everything downstream, budgets, access -decisions, and the audit trail, sees the owner rather than an anonymous key. -Revoking a person's access revokes their keys with it. +A virtual API key authenticates scripts, continuous integration jobs, and other +clients that cannot complete an interactive sign-in. Each key is bound to its +owner's directory identity, so requests retain the owner's access, budget, and +audit attribution. ## Where to find them -Every authenticated user manages their own keys in the console under **API -keys**, no administrative grant needed. - -There is no administrator screen for other people's keys yet. Listing and -disabling a key that belongs to someone else is role-gated and available over -the API only. +Users manage their keys under **API keys** in **Your workspace**. Administrators +can list and disable other users' keys through the API. ## Prerequisites -Virtual keys require the platform's identity provider to be configured, since -key issuance binds each key to a real identity. The AI Gateway also enforces -this in its own configuration: enabling virtual keys without identity configured -is refused rather than started in a state where keys could not be attributed. +Configure the platform identity provider before enabling virtual keys. The AI +Gateway validates this dependency at startup. -Enabling virtual keys on the AI Gateway also deploys its management API, which -the console uses. There is no separate switch for the two today, so an -installation that wants the console's gateway screens needs virtual keys -enabled. +Enabling virtual keys also deploys the AI Gateway management API used by the +console. ## Issue a key @@ -44,26 +31,20 @@ enabled. 3. Copy the value immediately. It is shown once and cannot be retrieved afterwards; only its prefix is stored for identification. -The key can then be used wherever a provider API key would go. See -[Connect a client](../enterprise-platform/connect-a-client.mdx). +Users follow the instructions in the console to configure their client. See +[Roll out gateway clients](../enterprise-platform/roll-out-gateway-clients.mdx). ## Revoke a key -**Revoke** invalidates a key immediately, with no grace period. That is the -response to a key you believe has leaked. - -The key list also shows a **Disabled** state, the reversible version, useful -when you suspect misuse but are not yet certain. +Use **Revoke** to invalidate a compromised key immediately. Use **Disable** for +a reversible suspension. ## Rotate a key -Rotation issues a new value under the same key record, keeping its name and -history, and is available over the API rather than in the console today. +The API can rotate a key while preserving its name and history. -The **outgoing value keeps working for 24 hours** after a rotation, so callers -you have not updated yet do not break the moment you rotate. That grace period -is also why rotation is the wrong response to a leaked key: the leaked value -stays valid for another day. Revoke instead. +The previous value remains valid for 24 hours after rotation. Revoke a +compromised key because rotation retains this grace period. Key creation, rotation, revocation, and enable or disable changes are all recorded in the audit trail, along with the identity that made the change. See @@ -71,13 +52,13 @@ recorded in the audit trail, along with the identity that made the change. See ## Storage -Keys are stored hashed, in the directory's database, alongside the users and -groups they belong to. The plaintext value exists only in the response that -created it. A key presented on a request is validated against the stored hash, -and the gateway then acts on the owner's resolved identity. +The directory stores a hash of each key and returns plaintext only when creating +or rotating it. The gateway validates the hash and resolves the owner on each +request. ## Next steps -- [Connect a client](../enterprise-platform/connect-a-client.mdx) to use a key. +- [Roll out gateway clients](../enterprise-platform/roll-out-gateway-clients.mdx) + to configure authentication. - [Budgets and pricing](../../ai-gateway/budgets-and-pricing.mdx), which charge a key's traffic to its owner. diff --git a/docs/platform/enterprise-manager/degraded-mode.mdx b/docs/platform/enterprise-manager/degraded-mode.mdx index 93ac3bb0..1c44717c 100644 --- a/docs/platform/enterprise-manager/degraded-mode.mdx +++ b/docs/platform/enterprise-manager/degraded-mode.mdx @@ -5,14 +5,9 @@ description: unreachable. --- -Degraded mode governs how Stacklok clients behave when the Enterprise Manager is -unreachable, for example during a network partition, server maintenance, or an -outage. You can configure a stricter policy to prevent unapproved activity -during outages. - -Unlike the policy directives described in [Policies](./policies/), degraded mode -does not carry an `enforcement` field. It controls client fallback behavior: -what happens when the server cannot be reached to enforce anything at all. +Degraded mode controls Stacklok client behavior when the Enterprise Manager is +unreachable, such as during a network partition, maintenance window, or outage. +It is a client fallback setting and has no `enforcement` field. ## Modes @@ -55,9 +50,8 @@ For manual Kubernetes deployments, set the same fields in your ## Grace period The `grace_period` field delays the policy taking effect after the server -becomes unreachable. During the grace period, clients operate as if in `warn` -mode regardless of the configured policy. This prevents brief network -interruptions from immediately blocking developer workflows. +becomes unreachable. During the grace period, clients use `warn` mode. This +allows brief network interruptions without blocking new server installations. For example, with `grace_period: "24h"` and `policy: "block_new"`, clients continue working normally for 24 hours after losing contact with the server. @@ -67,5 +61,4 @@ After 24 hours, new server installations are blocked. - [Configure policies](./policies/) to control client behavior when the server is reachable -- [Deploy the platform](../enterprise-platform/deployment.mdx) if you have not - already done so +- [Deploy the platform](../enterprise-platform/deployment.mdx) diff --git a/docs/platform/enterprise-manager/index.mdx b/docs/platform/enterprise-manager/index.mdx index 4f751c40..bb10c8bc 100644 --- a/docs/platform/enterprise-manager/index.mdx +++ b/docs/platform/enterprise-manager/index.mdx @@ -14,14 +14,10 @@ registry access, server permissions, telemetry, and client behavior. ## Where to start -- **New to the Enterprise Manager?** Read the [Introduction](./intro.mdx) for - architecture, enforcement model, and binary access. -- **Ready to deploy?** Install it with the - [platform chart](../enterprise-platform/deployment.mdx), then see - [Configure the Enterprise Manager](./configure.mdx) for its Helm values. -- **Already running?** Jump to [Policies](./policies/) to configure enforcement - rules, or [Degraded mode](./degraded-mode.mdx) to control client behavior - during outages. +Read the [Introduction](./intro.mdx) for the architecture and enforcement model. +Then [deploy the platform](../enterprise-platform/deployment.mdx) and +[configure the Enterprise Manager](./configure.mdx). Use [Policies](./policies/) +and [Degraded mode](./degraded-mode.mdx) to control client behavior. ## Contents diff --git a/docs/platform/enterprise-manager/intro.mdx b/docs/platform/enterprise-manager/intro.mdx index 73148cc5..c19b2e07 100644 --- a/docs/platform/enterprise-manager/intro.mdx +++ b/docs/platform/enterprise-manager/intro.mdx @@ -49,10 +49,9 @@ flowchart LR ## Enforcement levels -Every policy directive carries an `enforcement` field — either `enforced` -(mandatory, cannot be overridden locally) or `default` (advisory, can be -overridden). See [Enforcement levels](./policies/#enforcement-levels) for -details. +Every policy directive carries an `enforcement` field. `enforced` values are +mandatory, while clients can override `default` values. See +[Enforcement levels](./policies/#enforcement-levels). ## How clients connect @@ -62,11 +61,10 @@ Clients bootstrap from a single well-known URL: GET /.well-known/toolhive-configuration ``` -That document returns everything a client needs to authenticate and fetch -configuration: the config endpoint, the JWKS URI used to verify envelope -signatures, and the OIDC issuer, client ID, and scopes for the PKCE auth flow. -No out-of-band credential distribution is required. You share the bootstrap URL -and clients handle the rest. +That document returns the configuration endpoint, the JSON Web Key Set (JWKS) +URI used to verify envelope signatures, and the OIDC issuer, client ID, and +scopes for the Proof Key for Code Exchange (PKCE) flow. Distribute the bootstrap +URL with the Stacklok CLI. Each configuration envelope is signed with an EC P-256 key, tagged with an ETag for efficient caching, stamped with `issued_at` / `not_after` validity diff --git a/docs/platform/enterprise-manager/policies/build-env.mdx b/docs/platform/enterprise-manager/policies/build-env.mdx index ed11004b..e1e011de 100644 --- a/docs/platform/enterprise-manager/policies/build-env.mdx +++ b/docs/platform/enterprise-manager/policies/build-env.mdx @@ -6,10 +6,8 @@ description: --- Use this directive to inject environment variables into every MCP server -container managed by ToolHive. Common uses include configuring HTTP proxies so -containers can reach the internet through your corporate proxy, pointing -containers at internal endpoints, or setting org-wide values that every server -needs. +container managed by ToolHive. For example, configure an HTTP proxy, an internal +endpoint, or another organization-wide value. You'll need the Enterprise Manager already [deployed](../configure.mdx) and reachable by clients. @@ -30,10 +28,8 @@ enterpriseConfig: enforcement: 'enforced' ``` -Use `enforced` when the variables must be present for containers to function, -for example when all outbound traffic must go through a corporate proxy. Use -`default` when you want to provide org-wide defaults that individual developers -or teams can override locally. +Use `enforced` for required variables. Use `default` to let client operators +override the variables locally. After updating your configuration, [apply the change](./index.mdx#apply-policy-changes). diff --git a/docs/platform/enterprise-manager/policies/ca-certificate.mdx b/docs/platform/enterprise-manager/policies/ca-certificate.mdx index 454fa980..c5aa2101 100644 --- a/docs/platform/enterprise-manager/policies/ca-certificate.mdx +++ b/docs/platform/enterprise-manager/policies/ca-certificate.mdx @@ -5,11 +5,9 @@ description: reach internal services secured by a private certificate authority. --- -Use this directive when your MCP server containers need to reach internal -services (private registries, proxies, or APIs) that are secured by a -certificate authority not trusted by default. Injecting your corporate CA -certificate ensures containers can verify TLS connections without disabling -certificate verification. +Use this directive to add a corporate certificate authority (CA) to every MCP +server container. This lets containers verify TLS connections to internal +registries, proxies, and APIs. You'll need either the PEM-encoded certificate or a URL from which it can be fetched, and the Enterprise Manager already [deployed](../configure.mdx) and diff --git a/docs/platform/enterprise-manager/policies/index.mdx b/docs/platform/enterprise-manager/policies/index.mdx index a033bf15..435808dd 100644 --- a/docs/platform/enterprise-manager/policies/index.mdx +++ b/docs/platform/enterprise-manager/policies/index.mdx @@ -29,10 +29,8 @@ Every policy directive carries an `enforcement` field with one of two values: | `enforced` | Mandatory. Clients must use the configured value and cannot override it locally. | | `default` | Advisory. Clients use the configured value as a default but may override it locally. | -Use `enforced` when a policy needs to hold firm across the organization, for -example in regulated environments or when compliance requires it. Use `default` -when you want to recommend a configuration while still letting individual teams -or developers adjust for local needs. +Use `enforced` for mandatory organization-wide settings. Use `default` to +provide settings that client operators can override locally. ## Apply policy changes @@ -60,5 +58,5 @@ Pick a directive to configure: - [CA certificate policy](./ca-certificate.mdx) - [Build environment policy](./build-env.mdx) -Or read about [degraded mode](../degraded-mode.mdx) to control how clients -behave when the Enterprise Manager is unreachable. +[Configure degraded mode](../degraded-mode.mdx) to control client behavior when +the Enterprise Manager is unreachable. diff --git a/docs/platform/enterprise-manager/policies/non-registry-servers.mdx b/docs/platform/enterprise-manager/policies/non-registry-servers.mdx index 170e3120..203e026e 100644 --- a/docs/platform/enterprise-manager/policies/non-registry-servers.mdx +++ b/docs/platform/enterprise-manager/policies/non-registry-servers.mdx @@ -4,9 +4,8 @@ description: Block or allow MCP servers that are not listed in the configured registry. --- -A registry policy tells clients where to find approved servers, but without a -non-registry servers policy, developers can still run unapproved servers by -adding them locally. This guide shows you how to close that gap. +Use this policy to control whether Stacklok clients can run MCP servers outside +the configured registry. Pair this guide with a [Registry policy](./registry.mdx) so clients have a single approved registry to pull from. @@ -37,10 +36,8 @@ The combined behavior of the `value` and `enforcement` fields: | `default` | `false` | Clients default to registry-only but may override locally. | | `default` | `true` | Clients default to allowing any server and may override locally. | -Use `enforced` with `value: false` in security-sensitive environments where -unreviewed code execution is not acceptable. Use `default` when you want to -nudge developers toward the registry catalog without hard-blocking local -experimentation. +Use `enforced` with `value: false` to require clients to use the registry. Use +`default` to let client operators override the setting for local testing. After updating your configuration, [apply the change](./index.mdx#apply-policy-changes). diff --git a/docs/platform/enterprise-manager/policies/registry.mdx b/docs/platform/enterprise-manager/policies/registry.mdx index e15e63d1..a0fa9083 100644 --- a/docs/platform/enterprise-manager/policies/registry.mdx +++ b/docs/platform/enterprise-manager/policies/registry.mdx @@ -3,10 +3,8 @@ title: Registry policy description: Enforce a specific MCP registry URL for all Stacklok clients. --- -Without a registry policy, developers can point ToolHive to any MCP registry, -including unapproved ones. This guide shows you how to lock all clients to your -internal registry so developers always pull from your vetted server catalog. The -registry can be a self-hosted +Use the registry policy to configure the MCP registry used by Stacklok clients. +The registry can be a self-hosted [Registry Server](../../../toolhive/guides-registry/index.mdx), the upstream MCP registry, or any MCP-compatible registry. @@ -30,10 +28,8 @@ enterpriseConfig: enforcement: 'enforced' ``` -Use `enforced` in regulated environments or when you need to guarantee that only -vetted servers are accessible. Use `default` when you want to recommend a -registry URL across your organization but allow teams or developers to switch -for testing or local development. +Use `enforced` to require the configured registry. Use `default` to let client +operators override the registry for testing or local development. After updating your configuration, [apply the change](./index.mdx#apply-policy-changes). diff --git a/docs/platform/enterprise-manager/policies/telemetry.mdx b/docs/platform/enterprise-manager/policies/telemetry.mdx index 40f26f17..68351987 100644 --- a/docs/platform/enterprise-manager/policies/telemetry.mdx +++ b/docs/platform/enterprise-manager/policies/telemetry.mdx @@ -3,10 +3,9 @@ title: Telemetry policy description: Enforce OpenTelemetry settings across Stacklok clients. --- -This guide walks you through configuring a telemetry policy that routes all -client traces and metrics to your centralized OpenTelemetry collector, -regardless of any local configuration developers may have set. For a primer on -ToolHive's OpenTelemetry support, see the +Use the telemetry policy to configure an OpenTelemetry collector for Stacklok +client traces and metrics. For a primer on ToolHive's OpenTelemetry support, see +the [OpenTelemetry integration](../../../toolhive/integrations/opentelemetry.mdx) guide. @@ -37,11 +36,8 @@ enterpriseConfig: enforcement: 'enforced' ``` -Use `enforced` when your organization requires all telemetry to flow to a -central collector, for example for compliance, cost control, or security -monitoring. Use `default` when you want to push recommended OpenTelemetry -settings to developers but allow teams to route telemetry to their own -collectors for local debugging or testing. +Use `enforced` to require the central collector. Use `default` to let client +operators override the settings for local debugging or testing. :::warning diff --git a/docs/platform/enterprise-platform/airgap-install.mdx b/docs/platform/enterprise-platform/airgap-install.mdx index 7c3d07ff..e202ff04 100644 --- a/docs/platform/enterprise-platform/airgap-install.mdx +++ b/docs/platform/enterprise-platform/airgap-install.mdx @@ -8,24 +8,21 @@ description: import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -The [standard deployment](./deployment.mdx) pulls the umbrella Helm chart and -its images from Replicated at install time, using the license credentials you -receive during onboarding. That works when your cluster can reach Replicated -while it installs. When it can't, or your security posture requires every -artifact to come from an internal registry, use this air-gapped path instead. It -produces the same umbrella release, just sourced from your own registry. +Use this workflow when your cluster cannot reach Replicated during installation +or must pull every artifact from an internal registry. You mirror the Stacklok +Enterprise chart and images into your registry, then install the same umbrella +release from there. ## How it works -You move the platform artifacts across the air gap once, then install from your -side of it: +Move the platform artifacts across the air gap, then install from your registry: 1. **Pull the bundle from Replicated.** Use the Stacklok install portal's Helm air-gap flow to download the umbrella chart and the container images it references. 1. **Mirror into your registry.** Push the chart and images into a private OCI - registry you control. Amazon Elastic Container Registry (ECR) is the worked - example below, but any OCI-compliant registry works the same way. + registry you control. The examples use Amazon Elastic Container Registry + (ECR) and include commands for other OCI-compliant registries. 1. **Repoint the install.** Override the chart's image references and Helm source to your registry, and supply a pull secret. 1. **Install with Helm.** Run `helm install` against your registry and verify @@ -43,10 +40,8 @@ flowchart LR Registry -->|"helm install\n(chart + image pulls)"| Cluster ``` -Upgrades follow the same path: when Stacklok publishes a new release, re-pull -the chart and images at the new version, mirror them into your registry again, -then re-run the install (or let your GitOps controller reconcile the bumped -version). See [Automate with GitOps](#automate-with-gitops). +For upgrades, mirror the new chart and images, then update the Helm release or +the version in your GitOps configuration. ## Prerequisites @@ -59,11 +54,8 @@ Before you start, make sure you have: PostgreSQL database for the Registry Server (the MCP and skills catalog component, distinct from the private OCI image registry this page mirrors into). -- A **StorageClass** for persistent volumes (only needed if you're enabling the - AI Gateway). Valkey runs as a StatefulSet with a `ReadWriteOnce` persistent - volume claim, so the cluster needs a StorageClass, ideally one marked - **default**. If none is marked default, the claim stays `Pending` and Valkey - never starts; set the class explicitly in your values (see +- A **StorageClass** for the AI Gateway's Valkey persistent volume. Use the + default StorageClass or set one explicitly in your values (see [Step 4](#step-4-point-the-install-at-your-registry)). - A private OCI registry you control (Amazon ECR, Google Artifact Registry, Azure Container Registry, Artifactory, Harbor, or any OCI-compliant registry) @@ -86,10 +78,7 @@ Before you start, make sure you have: :::note[Egress for the transfer workstation] -The transfer workstation needs outbound HTTPS (port 443) to the Stacklok -distribution hosts and to your own registry. In a locked-down network, get these -allowlisted first; a blocked host surfaces as an opaque TLS or connection -timeout rather than a clear error: +Allow outbound HTTPS from the transfer workstation to: - the install portal (`install.stacklok.com`) - the OCI chart host and image-proxy host the portal prints for your release @@ -97,18 +86,16 @@ timeout rather than a clear error: - `proxy.replicated.com`, if the Replicated SDK is enabled - your own registry host, so you can push -The cluster's own egress (identity provider issuer, telemetry, upstream MCP -endpoints) is separate and isn't covered here. +Configure cluster egress separately for your identity provider, telemetry, and +upstream MCP endpoints. ::: :::note[Fully disconnected environments] -If the transfer workstation can't reach your registry and your cluster at the -same time, the install portal also produces a downloadable air-gap bundle (a -`.tgz` of the chart and images). Move the bundle across the gap on your own -media, then run the mirror steps below from a workstation on the internal -network. The commands are identical once the artifacts are local. +For a fully disconnected environment, download the air-gap bundle from the +install portal and transfer it to a workstation on the internal network. Run the +mirror steps there after unpacking the bundle. When you unpack the bundle, watch the OCI layout. A `tar --strip-components` can drop the `index.json` at the layout root, and archives created on macOS carry @@ -120,11 +107,8 @@ your target operating system. ## Step 1: Get your license and image list -Stacklok distributes the platform through Replicated and gives you a -customer-facing install portal at -[install.stacklok.com](https://install.stacklok.com). Log in with the -credentials Stacklok provides during onboarding. The portal hosts your license, -the chart, and per-release install instructions. +Log in to the [Stacklok install portal](https://install.stacklok.com) with the +credentials provided during onboarding. In the portal, open the **Existing cluster with Helm** instructions and select the air-gap flow. The portal generates the exact commands for your release, @@ -141,17 +125,10 @@ the image references tell you what to mirror. :::warning[Mirror every image the portal lists] -Use the portal's image list for your release as the source of truth, and mirror -every image on it. Don't try to derive the list yourself by rendering the chart: -the platform subcharts are disabled by default, so a plain `helm template` -doesn't render most of them; some images are pinned inside subcharts or passed -through as operator settings as full `repo:tag` strings; and a few default to -other registries such as `docker.io` or `mcr.microsoft.com`. Grepping -`values.yaml` for `image.repository` fields under-reports for the same reasons. -If anything looks missing for your release, confirm the full set with Stacklok -before you install. An image you miss surfaces later as an `ImagePullBackOff`, -because the cluster pulls only from your registry and the missing image was -never mirrored into it. +Mirror every image in the portal's list for your release. Rendering the chart or +searching `values.yaml` produces an incomplete list because disabled subcharts +and operator settings contain additional image references. Confirm any apparent +omissions with Stacklok before installation. ::: @@ -196,9 +173,8 @@ mirroring. If yours requires repositories to exist first, create one per artifact from Step 1 (the umbrella chart plus each image) under a shared prefix, using your registry's console or CLI. -Whichever registry you use, note its host (for example, `myorg.jfrog.io` or -`-docker.pkg.dev/`) and a prefix to group the Stacklok -artifacts. You reference both in the next steps. +Note the registry host, such as `-docker.pkg.dev/`, and a +prefix for the Stacklok artifacts. You reference both in the next steps. @@ -210,14 +186,11 @@ your own. ### Authenticate to both registries -You authenticate to two registries: the Replicated proxy registry you pull -**from** (the source), and your own registry you push **to** (the target). - Log in to the source with your **License ID** from [Step 1](#step-1-get-your-license-and-image-list). Replicated serves the chart and the images from different hosts: the chart from an `oci.*` host (Helm) and the images from an `image-proxy.*` host (skopeo). Use the exact hosts the -install portal printed for your release; the logins follow this shape: +install portal printed for your release: ```bash # Use your License ID as the password for both. @@ -270,9 +243,8 @@ access token, or a service-account key). The rest of the flow is identical. ### Copy the images -Mirror each image with `skopeo copy --all`. The `--all` flag copies the full -multi-arch manifest list (for example, `amd64` and `arm64`) rather than a single -platform, so the image still resolves on every node type in your cluster: +Mirror each image with `skopeo copy --all` to preserve its multi-architecture +manifest: ```bash skopeo copy --all \ @@ -280,20 +252,15 @@ skopeo copy --all \ docker://$ECR_HOST/$ECR_PREFIX/: ``` -Run one `skopeo copy` per image from the portal's image list. Take the -`` and `:` values directly from that list so -the tags match what the chart expects. The `docker://` prefix is skopeo's -registry transport, not a dependency on Docker; you don't need a container -engine for this step. +Run one `skopeo copy` per image from the portal's image list. Use the +`` and `:` values from that list. The +`docker://` prefix selects skopeo's registry transport. :::note[Preserve the multi-arch manifest] -[`crane copy`](https://github.com/google/go-containerregistry) preserves the -manifest list the same way and is a drop-in alternative (it takes bare image -references, with no `docker://` prefix). Avoid mirroring with a container -engine's single-image pull and push, such as `docker pull` and `docker push` (or -the `podman` and `nerdctl` equivalents), which flattens the image to one -architecture and breaks pulls on mixed `amd64` and `arm64` clusters. +You can also use [`crane copy`](https://github.com/google/go-containerregistry) +with bare image references. Both commands preserve the manifest required for +clusters with mixed `amd64` and `arm64` nodes. ::: @@ -315,9 +282,8 @@ helm pull oci://$ECR_HOST/$ECR_PREFIX/stacklok-enterprise-platform \ --version ``` -Confirm the version that resolves is the one you pushed, not a stale cached -entry under a reused tag (a stale chart surfaces later as confusing CRD-schema -mismatches): +Confirm that the resolved version matches the chart you pushed. A cached chart +under a reused tag can cause CRD schema mismatches: ```bash helm show chart oci://$ECR_HOST/$ECR_PREFIX/stacklok-enterprise-platform \ @@ -332,10 +298,8 @@ the chart a pull secret for it. ### Create an image pull secret -Create a `docker-registry` secret in the install namespace so the cluster can -authenticate when it pulls images. Despite the name, this Kubernetes secret type -works for any OCI registry, not just Docker Hub; it's how the kubelet stores -registry pull credentials. +Create a `docker-registry` secret in the install namespace. Kubernetes uses this +secret type for OCI registry credentials. @@ -351,8 +315,8 @@ kubectl create secret docker-registry stacklok-enterprise-pull \ --docker-password="$(aws ecr get-login-password --region "$AWS_REGION")" ``` -An ECR pull token expires after 12 hours, so don't pin it into a static secret. -Use a refreshing credential instead. See +ECR pull tokens expire after 12 hours. Configure a refreshing credential as +described in [Keep registry credentials fresh](#keep-registry-credentials-fresh). @@ -378,16 +342,11 @@ cluster needs. If it issues short-lived tokens, see ### Override the image source in values -Add your registry overrides and pull secret to the `values.yaml` from the -[standard deployment](./deployment.mdx#3-configure-values), so the chart pulls -every image from your registry instead of the default. - -This chart has no global image-registry override (no `global.imageRegistry`), -and no single `global.imagePullSecrets` that reaches every component. Repoint -each component's image, and set its pull secret, in that component's own -subchart values. The example below uses verified key paths; confirm them against -the `values.yaml` in the chart you pulled in Step 1, since paths can change -between versions. +Add registry overrides and the pull secret to the `values.yaml` from the +[standard deployment](./deployment.mdx#3-configure-values). Set each component's +image and pull secret under its subchart because the umbrella chart has no +global image registry or pull-secret value. Confirm these paths against the +`values.yaml` in your chart version. ```yaml title="values.yaml (air-gap additions)" # The air-gapped path uses your own pull secret, not the Replicated SDK. The @@ -437,11 +396,8 @@ toolhive-operator: registryAPI: image: //registry-api: -# AI Gateway operator (aliased enterprise-ai-gateway-operator). Its subchart -# images live under upstream.*, and it honors a subchart-scoped -# global.imagePullSecrets. Repoint the operator, AI Gateway controller and -# ext-proc, Envoy Gateway, ratelimit, Valkey, and Presidio image references -# here, following the same paths in the chart's values.yaml. +# AI Gateway images live under upstream.*. Set each image path from the chart's +# values.yaml and apply the pull secret across its subcharts. enterprise-ai-gateway-operator: upstream: global: @@ -468,31 +424,13 @@ enterprise-ai-gateway-operator: ::: -If your cluster has no **default** StorageClass, pin one for Valkey. It's -deployed as a StatefulSet with a `ReadWriteOnce` claim, so without a class the -claim stays `Pending` and Valkey never starts: - -```yaml -enterprise-ai-gateway-operator: - upstream: - valkey: - persistence: - storageClass: # for example, an EBS gp3 class -``` - -Persistence is enabled by default with a 1Gi claim, so you only need to set the -class. - ## Step 5: Preflight, install, and verify ### Run preflight checks against the mirrored chart -Before you install, run the platform's **preflight** spec against your target -cluster. The default spec runs entirely offline, without cluster egress, so it -works unchanged in an air-gapped install. Template the local `.tgz` you pulled -in [Step 3](#step-3-mirror-the-chart-and-images), rather than an `oci://` -reference, and pipe it to the `kubectl-preflight` plugin the same way as the -[standard deployment](./deployment.mdx#run-preflight-checks): +Run the platform's **preflight** spec against your target cluster. Template the +local `.tgz` from [Step 3](#step-3-mirror-the-chart-and-images) and pipe it to +the `kubectl-preflight` plugin: ```bash helm template stacklok-enterprise \ @@ -506,43 +444,30 @@ asset for your platform (`preflight_linux_.tar.gz`, or `preflight_darwin_all.tar.gz` on macOS) from [replicatedhq/troubleshoot releases](https://github.com/replicatedhq/troubleshoot/releases) alongside the chart and image transfer in -[Step 1](#step-1-get-your-license-and-image-list). Use the -[pinned version](./deployment.mdx#install-the-cli-plugin) rather than krew here, -since krew needs index access an air-gapped host doesn't have. - -The advisory workflow above needs nothing in your registry, only the CLI binary. +[Step 1](#step-1-get-your-license-and-image-list). Install the +[pinned version](./deployment.mdx#install-the-cli-plugin) from the mirrored +release asset because krew requires index access. -:::note[Enabling in-cluster enforcement] +### Enforce preflight checks in the cluster The optional [enforcement mode](./deployment.mdx#optional-enforce-preflight-checks-in-cluster) -runs the same checks as a pre-install hook Job that blocks a failing install. -Enabling it on this path takes three things: - -- Confirm the `preflight-runner` image reached your registry. It's on the - portal's image list, so [Step 3](#step-3-mirror-the-chart-and-images) mirrors - it like any other image, but it's easy to skip as an optional component's - image. -- Repoint `preflight.image.repository` at your registry, the same way you - repoint every other component in - [Step 4](#step-4-point-the-install-at-your-registry). It defaults to a - Stacklok-hosted host that an air-gapped cluster can't reach, and the chart has - no global registry override to do this for you. +runs the checks as a pre-install hook Job. For an air-gapped installation: + +- Mirror the `preflight-runner` image from the portal's image list. +- Set `preflight.image.repository` to the image in your registry. - Set `global.replicated.dockerconfigjson` to credentials for your registry. The Job renders its own hook-ordered pull secret from that value, and it can't use the `stacklok-enterprise-pull` secret you created above, because a pre-install hook runs before ordinary chart resources exist. This value is still required even though you set `replicated.enabled: false`. -Because the Job runs before anything else installs, missing any of these hangs -the install rather than surfacing as an `ImagePullBackOff` later. - -::: +The Job runs before the chart creates ordinary resources, so it requires its own +image credentials. ### Install and verify -Install the chart from your registry with the merged values. Reference the chart -by its `oci://` URL rather than a Helm repo alias: +Install the chart from its `oci://` URL with the merged values: ```bash helm install stacklok-enterprise \ @@ -557,12 +482,6 @@ Verify the platform comes up the same way as in the [standard deployment](./deployment.mdx#5-verify-the-install): confirm every pod in `stacklok-system` reaches `Running` and the ToolHive CRDs registered. -In an air-gapped install, the failure mode to watch for is a pod stuck in -`ImagePullBackOff`. It almost always means an image the chart references wasn't -mirrored, or the pull secret can't authenticate. Recheck the pod's image against -the portal's image list to confirm you mirrored it, and confirm the -`stacklok-enterprise-pull` secret is valid. - ## Step 6: Prepare workload namespaces If you run MCP server and vMCP workloads in a namespace other than @@ -575,7 +494,7 @@ it in the pod's own namespace. Provide that access the same way as - **Long-lived credentials** (Artifactory or Harbor robot accounts, a GAR or ACR service-account key): create `stacklok-enterprise-pull` in each workload - namespace, as you did in `stacklok-system`. That's all the namespace needs: + namespace, as you did in `stacklok-system`: ```bash kubectl create namespace @@ -594,40 +513,16 @@ it in the pod's own namespace. Provide that access the same way as ## Automate with GitOps -The manual flow above is the clearest way to understand the air-gap path, but -most teams run it through their existing infrastructure-as-code and GitOps -tooling so it's repeatable and auditable. - -- **Provision with infrastructure-as-code, in the right order.** Use Terraform, - OpenTofu, or your preferred tool to create the registry repositories and the - image pull secret. They must exist before the controller first reconciles, or - the HelmRelease fails with `chart not found` or `secret not found` and you're - debugging a race. Provision the registry side first, then point the controller - at it. -- **Reconcile with GitOps, from a self-refreshing source.** Point Flux or Argo - CD at your registry's `oci://` chart URL and let the controller install and - upgrade the umbrella release. Rather than a static pull secret, let the - controller pull the chart with a workload identity (for Flux, a - `HelmRepository` with `provider: aws` and no `secretRef`); this is the - chart-pull half of - [Keep registry credentials fresh](#keep-registry-credentials-fresh). The - values you assemble in [Step 4](#step-4-point-the-install-at-your-registry) go - in your Git source of truth. -- **Mirror new releases, and keep Git in sync with what you mirrored.** Wrap the - [Step 3](#step-3-mirror-the-chart-and-images) `skopeo` and `helm push` - commands in a job that runs whenever Stacklok publishes a new release. In an - air-gapped setup, Git can drift from reality: an out-of-band mirror leaves the - cluster on a version Git never recorded. Pin the exact mirrored version and - commit the bump when you mirror. - -:::note[If you automate further] - -Flux image-automation semver matchers ignore pre-release tags by default. And -infrastructure-as-code that creates registry repositories and IAM roles in a -single apply often needs retries: a freshly created IAM principal isn't -immediately usable, and the apply can fail with `Invalid principal in policy`. +Automate the mirror and deployment workflow with your infrastructure-as-code and +GitOps tools: -::: +1. Provision the registry repositories and credentials before the GitOps + controller reconciles the Helm release. +1. Configure Flux or Argo CD to install the chart from your registry's `oci://` + URL with the values from + [Step 4](#step-4-point-the-install-at-your-registry). +1. Mirror each new release and commit the mirrored version to your Git source of + truth in the same change. ### Keep registry credentials fresh @@ -674,12 +569,10 @@ pod, so pod-level workload identity fits here: - [Verify the distribution](./verify-artifacts.mdx) to confirm the signatures, provenance, and SBOMs of the images you mirrored -- [Configure platform identity](./configure-identity.mdx) to wire your identity - provider to the platform components +- [Configure platform identity](./configure-identity.mdx) to connect your + identity provider to the platform components - [Configure policies](../enterprise-manager/policies/) to control client behavior across your organization -- [Sign in to the console](../enterprise-console/index.mdx) once the platform is - running ## Related information @@ -687,3 +580,14 @@ pod, so pod-level workload identity fits here: from Replicated at install time - [Configure the Registry Server](./configure-registry-server.mdx) - the catalog the console reads + +## Troubleshooting + +
+Pods remain in `ImagePullBackOff` + +Compare the pod's image reference with the portal's image list and confirm that +you mirrored it. Then verify that the `stacklok-enterprise-pull` secret contains +valid credentials for your registry. + +
diff --git a/docs/platform/enterprise-platform/api-reference.mdx b/docs/platform/enterprise-platform/api-reference.mdx index 5fdaeec5..df20ac58 100644 --- a/docs/platform/enterprise-platform/api-reference.mdx +++ b/docs/platform/enterprise-platform/api-reference.mdx @@ -9,9 +9,8 @@ hide_table_of_contents: true import ApiDocMdx from '@theme/ApiDocMdx'; -The platform chart deploys several backend services, three of which expose a -REST API you can drive directly. The console is one client of these APIs, so -anything it does, automation can do too. +The platform chart deploys three backend REST APIs for administration and +automation. The console uses these APIs. All three are cluster-internal. The chart creates ClusterIP Services and no ingress, so reach them over Service DNS from inside the cluster, or publish them @@ -28,9 +27,8 @@ the open source build and is documented in the ## Enterprise Manager -Users, groups, connectors, managed secrets, virtual API keys, budgets, and the -signed configuration envelopes the Stacklok CLI reads. This is the largest of -the three surfaces and the one the console exercises most. +The Enterprise Manager API covers users, groups, connectors, managed secrets, +virtual API keys, budgets, and signed Stacklok CLI configuration. The running service also self-serves this reference at `/api/doc`, which is generated from the same source and therefore always matches the version you have @@ -38,10 +36,8 @@ deployed. :::warning[Two route groups are absent from this specification] -The SCIM provisioning routes and the budgets webhook routes are live in the -service but are not annotated, so they do not appear below. Treat this reference -as a guide to the annotated surface rather than a complete inventory, and check -the running service's own `/api/doc` when an endpoint you expect is missing. +The generated specification omits the SCIM provisioning and budget webhook +routes. Use `/api/doc` on the running service for its complete API reference. ::: @@ -49,20 +45,17 @@ the running service's own `/api/doc` when an endpoint you expect is missing. ## Connector Gateway -The per-user Connector Gateway's control plane: connector enablement and the -per-user authorization state behind it. Off by default; see -[Configure the Connector Gateway](./configure-connector-gateway.mdx). +The Connector Gateway API covers connector enablement and per-user authorization +state. See [Configure the Connector Gateway](./configure-connector-gateway.mdx). ## AI Gateway management -Admin-scoped policy management for the AI Gateway. The service is a facade over -the Kubernetes API rather than a separate store, so writes here become custom -resource changes. Off by default; see -[Configure the AI Gateway](./configure-ai-gateway.mdx). +The AI Gateway management API updates custom resources through the Kubernetes +API. See [Configure the AI Gateway](./configure-ai-gateway.mdx). -For the policy decisions these endpoints express, rather than their wire format, -see [AI Gateway policy](../../ai-gateway/index.mdx). +For task-oriented AI Gateway configuration, see +[AI Gateway](../../ai-gateway/index.mdx). diff --git a/docs/platform/enterprise-platform/configure-ai-gateway.mdx b/docs/platform/enterprise-platform/configure-ai-gateway.mdx index c8ed7795..e159e132 100644 --- a/docs/platform/enterprise-platform/configure-ai-gateway.mdx +++ b/docs/platform/enterprise-platform/configure-ai-gateway.mdx @@ -6,10 +6,9 @@ description: bring up a working gateway in the right order. --- -The AI Gateway is part of the platform chart but is **off by default**. This -page covers turning it on and the order to bring it up in. Once it is running, -see [AI Gateway policy](../../ai-gateway/index.mdx) for the day-to-day -configuration. +Enable the AI Gateway through the platform chart, then apply its custom +resources in the order described below. See +[AI Gateway](../../ai-gateway/index.mdx) for ongoing configuration. ## Prerequisites @@ -33,24 +32,19 @@ global: enabled: true ``` -That installs the AI Gateway operator and its custom resource definitions. It -does not create a gateway: the operator is the thing that turns an `AIGateway` -resource into running infrastructure, so nothing serves traffic until you apply -one. +This installs the AI Gateway operator and custom resource definitions. Apply an +`AIGateway` resource to create a gateway instance. ## Bring it up in this order -The sequence matters, because two of the gateway's controls fail closed. Doing -these steps out of order produces refused requests rather than a permissive -gateway. +Complete the following sequence before sending production traffic: 1. **Apply an `AIGateway` resource** with at least one provider and one route. See [Connect model providers](../../ai-gateway/providers-and-models.mdx). -2. **Create an `AIPolicy` that targets it.** Budget enforcement is only injected - into a gateway that has one, so without it budgets are silently not enforced. - The policy's contents are not consulted for budgets, so an otherwise empty - policy is a supported shape here. +2. **Create an `AIPolicy` that targets it.** The operator adds budget + enforcement only to gateways with a matching policy. The policy can omit + screening controls when you only need budget enforcement. 3. **Create budgets for every user or group that will send traffic**, before you point the gateway at the budget service. A caller with no applicable budget @@ -68,16 +62,13 @@ gateway. ## Content screening posture -Detection failures refuse requests by default, which is the posture you want in -production. A waiver exists to let scanning fail open, but it is default-off, -governed as an experiment, and unavailable on the stable release channel. Treat -it as a rollout or incident-response tool rather than a configuration option. +Detection failures deny requests by default. An experimental waiver can allow +traffic during a rollout or incident, but it is unavailable on the stable +release channel. ## Next steps -- [AI Gateway policy](../../ai-gateway/index.mdx) for providers, routing, - screening, and budgets. -- [Connect a client](./connect-a-client.mdx) to point a developer's tools at the - gateway. -- [What you see depends on what is installed](../concepts/what-you-can-see.mdx) - for which console areas enabling this reveals. +- [AI Gateway](../../ai-gateway/index.mdx) for providers, routing, screening, + and budgets. +- [Roll out gateway clients](./roll-out-gateway-clients.mdx) to distribute + deployment-specific setup instructions. diff --git a/docs/platform/enterprise-platform/configure-connector-gateway.mdx b/docs/platform/enterprise-platform/configure-connector-gateway.mdx index 7623b8c7..381a0491 100644 --- a/docs/platform/enterprise-platform/configure-connector-gateway.mdx +++ b/docs/platform/enterprise-platform/configure-connector-gateway.mdx @@ -6,14 +6,9 @@ description: install identity, and understand how it scopes connector access per user. --- -The Connector Gateway is the per-user entry point for tool calls. Developers -point a single MCP client at it, and it presents only the connectors that person -is entitled to use, brokering the upstream OAuth flows on their behalf. It is -part of the platform chart and **off by default**. - -This page covers enabling it and how it fits into the distribution. It does not -reproduce the ToolHive operator or registry server guides; for those, see the -[ToolHive documentation](../../toolhive/index.mdx). +The Connector Gateway provides an identity-aware MCP endpoint and brokers +upstream OAuth for connectors. Enable it through the Stacklok Enterprise +platform chart. ## Prerequisites @@ -23,9 +18,9 @@ reproduce the ToolHive operator or registry server guides; for those, see the - **The directory service**, which the gateway reaches over gRPC for identity, connector configuration, and access policy. It is part of the platform chart. -## Enable it +## Enable the Connector Gateway -Enabling the Connector Gateway is a three-value decision, not a single flag: +Set the enable flag, gateway identifier, and public issuer URL: ```yaml title="values.yaml" global: @@ -36,48 +31,31 @@ global: authServerIssuer: https:// ``` -`connectorGatewayId` has no default on purpose. A gateway announces this -identity to the directory when it registers, so if the value were defaulted, -every install would announce the same placeholder. Enabling the component -without choosing an id fails the render with a message naming the value, rather -than starting a gateway with a shared identity. - -The same id scopes the console's admin **Connectors** view, so the console and -the gateway agree on which install they are talking about. +| Value | Purpose | +| -------------------------- | -------------------------------------------------- | +| `connectorGateway.enabled` | Deploy the Connector Gateway | +| `connectorGatewayId` | Identify this gateway to the directory and console | +| `authServerIssuer` | Set the gateway's public authorization server URL | -`authServerIssuer` has no default for the same reason: it is this deployment's -own public auth-server URL, and no placeholder is correct for it. The gateway -derives every identity provider's upstream OAuth callback from it, as -`{issuer}/oauth/callback`, so an install without one is refused at render rather -than starting a gateway that cannot complete a login. +Choose a stable `connectorGatewayId` such as `prod-eu` or `platform-staging`. +The value cannot be empty or contain a colon. Changing it creates a new gateway +identity and changes the prefix used for stored per-user tokens. -Set it only here. The chart owns the value and projects it, so setting -`enterpriseConfig.authServer.issuer` or `appConfig.authServer.issuer` directly -is refused, naming this key instead. - -The issuer is validated at render time, which saves an apply cycle. A trailing -slash and a tenant path prefix are both accepted. Userinfo, a query, a fragment, -percent-encoding, an uppercase scheme, and whitespace are all refused, as is -`http://` on anything but a loopback host unless you also set -`connector-gateway.enterpriseConfig.authServer.insecure_allow_http`. +Set `authServerIssuer` to the HTTPS URL that clients use to reach this +deployment. The gateway derives connector OAuth callbacks as +`{issuer}/oauth/callback`. Configure the value under `global.stacklok`; the +chart rejects equivalent settings under the component's internal configuration. :::note[Two different issuers] -This is not `global.stacklok.primaryIdp.issuer`. That one is your corporate -identity provider, the one your people sign in to the platform with. This one is -the gateway's own embedded auth server. A full install sets both, to different -values. See [Configure platform identity](./configure-identity.mdx). +`global.stacklok.primaryIdp.issuer` identifies the corporate identity provider. +`authServerIssuer` identifies the Connector Gateway's authorization server. +Configure both values. See +[Configure platform identity](./configure-identity.mdx). ::: -Any stable string works; it is not a UUID. Choose something that names the -deployment, such as `prod-eu` or `platform-staging`. Two rules are enforced on -every deployment: it cannot be empty, and it cannot contain a colon, which is -the reserved delimiter in the per-user token-storage key the gateway builds from -it. Treat it as fixed once set, since it is the identity the directory has -registered and part of the key that existing tokens are stored under. - -## How it fits together +## How connector access works ```mermaid flowchart LR @@ -91,16 +69,14 @@ flowchart LR MCPGW -->|"only the connectors they are entitled to"| Backends ``` -The gateway holds no access list of its own. It asks the directory who the -caller is and which connectors that caller may reach, then narrows the backends -it exposes accordingly. Access is granted to directory groups, so changing a -person's group membership changes what their MCP client can see. +The gateway asks the directory to resolve each caller and return their connector +grants. Directory group membership determines the MCP servers exposed to the +client. :::note -Connector access is decided by directory groups, which are **not** the same as -the OIDC claim groups named in cluster-level authorization policy. See -[The two group models](../concepts/two-group-models.mdx). +Cluster authorization policy uses OIDC claim groups. See +[Directory groups and OIDC claim groups](../concepts/two-group-models.mdx). ::: @@ -113,12 +89,8 @@ the OIDC claim groups named in cluster-level authorization policy. See [Connectors](../../connector-gateway/connectors.mdx) or through the directory API, which exposes the same operations under `/v1/gateways/{gateway_id}/connectors`. -3. Have a developer point a client at it, following - [Connect a client](./connect-a-client.mdx). The console's setup page - generates the client configuration with the real endpoint filled in. They - then enable the connectors they want from their own view, and complete any - OAuth consent those connectors need, either there or when their client first - connects. +3. Direct users to the deployment-specific instructions in the console. See + [Roll out gateway clients](./roll-out-gateway-clients.mdx). ## Next steps @@ -126,5 +98,3 @@ the OIDC claim groups named in cluster-level authorization policy. See register connectors and grant access. - [Identity and directory](../enterprise-directory/index.mdx) for the users and groups that access is granted to. -- [What you see depends on what is installed](../concepts/what-you-can-see.mdx) - for which console areas enabling this reveals. diff --git a/docs/platform/enterprise-platform/configure-identity.mdx b/docs/platform/enterprise-platform/configure-identity.mdx index 82fe6732..73955527 100644 --- a/docs/platform/enterprise-platform/configure-identity.mdx +++ b/docs/platform/enterprise-platform/configure-identity.mdx @@ -6,23 +6,15 @@ description: --- Stacklok Enterprise authenticates every request through your identity provider. -When a developer signs in to the Stacklok CLI or the console, the identity -provider issues an access token that the platform components validate before -serving any data. This page walks through the values each component expects from -your identity provider and shows two supported ways to model them, so that each -component accepts the tokens your provider issues. - -This page covers the shared platform components: the Enterprise Manager, the -console, and the Registry Server. Individual MCP servers validate tokens through -their own per-server OIDC configuration, which you set up alongside each -server's policy in [enterprise authorization](../enterprise-authz/index.mdx). -The same identity provider issues both, so the claims you configure here also -feed the role bindings on your MCP servers. - -You should already have an OIDC-compatible identity provider (Okta, Entra ID, or -a generic OIDC provider) where you can create authorization servers, OAuth -client applications, custom scopes, and custom claims. Stacklok Enterprise does -not ship its own identity provider. +Configure the issuer, audiences, scopes, and claims that the platform components +validate in access tokens. + +This page covers the Enterprise Manager, the console, and the Registry Server. +Configure identity for individual MCP servers through +[enterprise authorization](../enterprise-authz/index.mdx). + +You need an OIDC-compatible identity provider where you can create authorization +servers, OAuth client applications, custom scopes, and custom claims. ## What each component expects @@ -35,7 +27,7 @@ at a different combination of audience, scope, and claims. | Component | What the component verifies | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Enterprise Manager](../enterprise-manager/configure.mdx) | Issuer, an audience that matches the configured value, and the configured scope is present | -| [The console](../enterprise-console/index.mdx) | Issuer and an audience that matches the console's OAuth client application | +| [Console](../enterprise-console/index.mdx) | Issuer and an audience that matches the console's OAuth client application | | [Registry Server](../../toolhive/guides-registry/index.mdx) | Issuer, the role claim that maps the user to a platform role, and the group claim used for bindings | | [MCP servers](../enterprise-authz/quickstart-entra.mdx) | Issuer and an audience that matches the server's `MCPOIDCConfig`, plus the role and group claims that [enterprise authorization](../enterprise-authz/index.mdx) policies bind against | @@ -48,9 +40,8 @@ tokens. ## Pick a setup -The components do not care whether their tokens come from the same authorization -server or from different ones. They care that the audience on the token matches -the audience configured on the component. Two setups satisfy that requirement. +Each token's audience must match the audience configured for its platform +component. Use one of these identity provider setups. **One authorization server per component.** Recommended where your identity provider supports it. Create one custom authorization server per platform @@ -115,9 +106,9 @@ On the **Scopes** tab, add a custom scope: The Registry Server and the per-server MCP authorization policies both read group and role claims from the token to map each user to a platform role. The -platform matches against the claim names, so those matter, but the values flow -through from your identity provider's user profile. Adjust the value expressions -to match how your Okta tenant carries group membership and role assignment. +platform matches the claim names, while the values come from your identity +provider's user profile. Adjust the expressions for your Okta tenant's group +membership and role assignments. On the **Claims** tab, add the claims each component reads: @@ -133,10 +124,9 @@ names to platform roles, and the ConfigMap that overrides them, see ### Step 4: Add a default access policy -The Okta free tier ships the `default` authorization server without an access -policy, so client applications cannot request scopes against it until you add -one. Paid tiers usually have a permissive default policy already in place, so -this step is a no-op for them. +The Okta free tier's `default` authorization server requires an access policy +before client applications can request scopes. If your server already has an +applicable policy, continue to the next step. On the **Access Policies** tab, add a default policy with one rule that permits your OAuth clients to request the audiences and scope from steps 1 and 2. @@ -151,17 +141,14 @@ application, because it runs an authorization code flow on its server. | Application | Type | Audience requested | | ------------ | -------------------------- | -------------------- | | Stacklok CLI | Native (PKCE, no secret) | `enterprise-manager` | -| The console | Web (confidential, secret) | `cloud-ui` | +| Console | Web (confidential, secret) | `cloud-ui` | Note the client IDs and, for the console, the client secret for the next step. -### Step 6: Wire the values into the component charts +### Step 6: Configure component chart values -The issuer is platform-wide: set it once and every component trusts it. Audience -and scope are per component, so each component takes those from its own chart -values. The values you wire in here must match the audiences and scope you -configured in steps 1 through 5, or the component will reject the tokens at -runtime. +Set the platform-wide issuer once. Configure the audience and scope for each +component to match the values from steps 1 through 5. Set the identity provider for the whole platform under `global.stacklok`: @@ -205,9 +192,8 @@ structure. ## Verify a token -Once a Stacklok client can sign in, you can confirm the tokens it receives carry -the right values before wiring them into a component. Decode the access token -and check the issuer, audience, and scope: +After a Stacklok client can sign in, decode its access token and verify the +issuer, audience, and scope: ```bash echo "" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.iss, .aud, .scp' @@ -228,7 +214,7 @@ component returns `401 Unauthorized`. ## Next steps - [Configure the Enterprise Manager](../enterprise-manager/configure.mdx) with - the issuer, audience, and scope from this page wired into its chart + the issuer, audience, and scope from this page - [SCIM provisioning](../enterprise-directory/scim-provisioning.mdx) to populate the directory from the same identity provider @@ -250,7 +236,7 @@ so that access tokens are issued as JWTs. `aud` is an array, not a string Some identity providers put a single audience into an array -(`"aud": ["enterprise-manager"]`). The components accept both shapes. If +(`"aud": ["enterprise-manager"]`). The components accept both forms. If validation still fails, confirm the string inside the array matches the configured audience exactly, including any prefix like `api://`. diff --git a/docs/platform/enterprise-platform/configure-registry-server.mdx b/docs/platform/enterprise-platform/configure-registry-server.mdx index 9f3fbc54..3ee2ebfc 100644 --- a/docs/platform/enterprise-platform/configure-registry-server.mdx +++ b/docs/platform/enterprise-platform/configure-registry-server.mdx @@ -1,40 +1,27 @@ --- title: Configure the Registry Server description: - Configuration reference for the Registry Server component in the Stacklok - Enterprise platform chart. + Enable the Registry Server in the Stacklok Enterprise platform chart and + connect it to PostgreSQL. --- The Registry Server serves the approved MCP server and skills catalog that the -console and Stacklok CLI clients consume. It ships as a hardened, license-gated -build in the Stacklok Enterprise platform chart. +console and Stacklok CLI use. Install it with the +[platform chart](./deployment.mdx), either with the other platform components or +as a separate release for a distributed deployment. -:::tip[Deploy the platform first] - -Install the Registry Server with the [platform chart](./deployment.mdx), which -deploys it alongside the other components. To run it in its own cluster, or to -maintain a separate registry per environment, enable only this component as -described in [Distributed deployments](./distributed-deployments.mdx). - -::: - -The enterprise Registry Server uses the same configuration schema as the open -source [Registry Server guides](../../toolhive/guides-registry/index.mdx). Every -configuration concern (sources, registries, sync policies, database, -authentication, and authorization) is identical, so this page covers enabling -the component and points to the open source reference for the field-level -detail. +The Enterprise build uses the open source Registry Server configuration schema. +This page covers its platform chart values and links to the +[Registry Server guides](../../toolhive/guides-registry/index.mdx) for service +configuration. ## Prerequisites Before deploying, ensure you have: - A Kubernetes cluster (1.30 or later) -- An external PostgreSQL database (14 or later) that you provide; the Registry - Server stores its catalog there. Whichever user runs the migrations, the - application user by default or a separate migration user if you configure one, - needs the **`CREATEROLE` attribute**: the first migration creates a role of - its own. See [Database roles](#database-roles) below. +- An external PostgreSQL database (14 or later). The role that runs migrations + needs the **`CREATEROLE` attribute**. See [Database roles](#database-roles). - Stacklok Enterprise distribution access, which includes the Helm chart and container image registry credentials, provided by Stacklok during onboarding @@ -45,13 +32,9 @@ configuration under the `toolhive-registry-server` key. The chart wraps the open source Registry Server chart under an `upstream` alias, so those values sit under `toolhive-registry-server.upstream`. -The `upstream.config` block is the open source Registry Server configuration -schema, rendered verbatim into a ConfigMap. A functioning server needs at least -one `sources` entry and one `registries` entry alongside the `database` -connection, and the chart already ships both: a `toolhive` source tracking the -public catalog, and a `default` registry that serves it. The database wiring -below is genuinely all you have to supply, so the skeleton is complete rather -than abbreviated. +The `upstream.config` block contains the open source Registry Server +configuration. The chart provides a `toolhive` source for the public catalog and +a `default` registry. Supply the database configuration below. Override `sources` and `registries` when you want your own catalog, more registries, or claim-scoped access to them; see the @@ -59,11 +42,8 @@ registries, or claim-scoped access to them; see the ### Database roles -The Registry Server applies its schema migrations at startup, and the first one -creates a `toolhive_registry_server` role that later grants are made against. -Creating a role needs the `CREATEROLE` attribute, which is a property of the -role itself rather than a privilege you can `GRANT`, so owning the database or -holding full rights on the schema is not enough. +The first startup migration creates a `toolhive_registry_server` role. Assign +the `CREATEROLE` attribute to the role that runs migrations. By default the application user runs the migrations, so it is the one that needs the attribute: @@ -76,22 +56,6 @@ CREATE DATABASE toolhive_registry OWNER thv_user; If you configure a separate migration user, put `CREATEROLE` on that one instead and leave the application user unprivileged. -:::warning[A failed migration leaves the database wedged] - -Without the attribute the first migration fails, and the migration tracking -table is left flagged as dirty. The pod then crashloops on every restart: - -``` -Error: failed to run database migrations: failed to apply migrations: -failed to run migrations: Dirty database version 1. Fix and force version. -``` - -Granting `CREATEROLE` afterwards does not clear the flag by itself. On a fresh -install the simplest recovery is to drop and recreate the database, then let the -corrected role run the migrations from the start. - -::: - ### Create the database credential Secrets Supply database passwords from Secrets, never inline in the `config` block. @@ -186,3 +150,14 @@ The chart exposes the Registry Server through an in-cluster Service named populate the catalog - [Manage connectors](../../connector-gateway/connectors.mdx) in the console once the Registry Server is running + +## Troubleshooting + +
+Migration reports `Dirty database version 1` + +The first migration failed because its database role lacked `CREATEROLE` and +left the migration table marked dirty. For a new installation, assign the +attribute, drop and recreate the database, and restart the Registry Server. + +
diff --git a/docs/platform/enterprise-platform/connect-a-client.mdx b/docs/platform/enterprise-platform/connect-a-client.mdx deleted file mode 100644 index 16ae4f92..00000000 --- a/docs/platform/enterprise-platform/connect-a-client.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Connect a client -sidebar_label: Connect a client -description: - Point your editor, agent, or CLI at the AI gateway and the Connector Gateway, - and choose between signing in and using an API key. ---- - -There are two endpoints you might connect a tool to, and they do different jobs. - -The **AI gateway** is where model requests go. Point any OpenAI-compatible -client at it and its model calls route through your organization's policies and -count against your budget. - -The **Connector Gateway** is where tool calls go. Point any MCP client at it and -it sees the connectors you have been granted, with upstream authorization -brokered for you. - -Most people connect both, and many tools need both configured separately. - -## Get the setup instructions - -The console generates per-client instructions with your deployment's real -endpoints already filled in, which a documentation page can only show as a -placeholder. Use those rather than transcribing addresses by hand. - -- **Setup**, under Connectors, covers pointing MCP clients at the Connector - Gateway. -- **Setup**, under AI Gateway, covers routing a client's model calls through the - AI gateway. - -Both pages carry step-by-step instructions for common editors, agents, and -command-line tools, and the MCP page additionally offers a copyable prompt you -can paste into an AI-powered client to have it configure itself. - -## How you authenticate - -Two options, and the right one depends on whether a person is present. - -**Signing in** is the default for interactive use. Your tool completes a browser -sign-in against your corporate identity provider, and the resulting session -identifies you to the gateways. - -**A virtual API key** is for anything that cannot complete a browser flow: a -script, a scheduled job, a continuous integration step. Create one under **API -keys** and use it where a provider API key would go. - -An API key is bound to your identity, not a shared service credential. Requests -made with it are attributed to you, charged to your budget, and recorded against -you in the audit trail. See -[Virtual API keys](../enterprise-directory/virtual-api-keys.mdx). - -## What to expect once connected - -Your model calls are checked against your budget before they are sent, so a -request can be refused because you have run out rather than because anything is -broken. Your tool calls only reach connectors your groups have been granted, and -only the ones you have enabled. - -If a connector needs to reach a backend as you, it offers an **Authenticate** -button on your Connectors screen. Complete it once and the gateway holds the -authorization for your later calls. - -## Next steps - -- [The console](../enterprise-console/index.mdx) to see the connectors available - to you and what you have spent. diff --git a/docs/platform/enterprise-platform/deployment.mdx b/docs/platform/enterprise-platform/deployment.mdx index 448808af..338414da 100644 --- a/docs/platform/enterprise-platform/deployment.mdx +++ b/docs/platform/enterprise-platform/deployment.mdx @@ -10,22 +10,10 @@ as a single umbrella Helm chart that deploys the ToolHive Operator, the Enterprise Manager, the console, and the Registry Server in one release, along with the custom resource definitions (CRDs) the operator needs. -:::tip[Distributed and multi-cluster deployments] - -The umbrella chart can also run a subset of components, so you can spread the -platform across clusters or maintain separate registries per environment. See -[Distributed deployments](./distributed-deployments.mdx). - -::: - -:::tip[Air-gapped or strict-egress clusters] - -This page installs the chart directly from Replicated, which requires outbound -access at install time. If your cluster can't reach Replicated, or your security -posture requires every artifact to come from an internal registry, see -[Install from a private registry (air-gapped)](./airgap-install.mdx) instead. - -::: +For multi-cluster installations or separate registries per environment, see +[Distributed deployments](./distributed-deployments.mdx). For clusters that must +pull artifacts from an internal registry, see +[Install from a private registry (air-gapped)](./airgap-install.mdx). ## Prerequisites @@ -51,78 +39,48 @@ Before deploying, ensure you have: ## What the chart includes -The chart bundles each platform component and gives it an enable flag in a -single `values.yaml`, so you turn on only the components you want. - -**Every flag defaults to off.** A `helm install` with no values renders an empty -release, so this table is the list of decisions you have to make, not a list of -things you can turn off. +Enable each required component in `values.yaml`. All component flags default to +`false`, and an install without values creates no platform workloads. | Enable flag | What it deploys | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | `toolhiveOperator` | The ToolHive Operator and its custom resource definitions (`MCPServer`, `VirtualMCPServer`, and others) | | `enterpriseManager` | Enterprise Manager, which serves configuration to Stacklok clients | | `enterprise-manager.directory.enabled` | The directory service: users, groups, connectors, secrets, and virtual keys. Needs `enterpriseManager` and a PostgreSQL database you provide | -| `cloudUi` | The console, a Next.js application | +| `cloudUi` | Console (Next.js application) | | `registryServer` | Registry Server, backed by an external PostgreSQL database you provide | | `global.stacklok.aiGateway.enabled` | AI Gateway operator and its custom resource definitions | | `global.stacklok.connectorGateway.enabled` | Connector Gateway. Also requires `global.stacklok.connectorGatewayId` and `global.stacklok.authServerIssuer` | -Two things about that table are worth reading twice. - -**The last two live under `global.stacklok`, not at the top level.** A subchart -can only read `global.*`, never a sibling chart's values, so these two toggles -have to sit there. A values file that sets a top-level `aiGateway` or -`connectorGateway` key instead fails the render with a message naming the key to -use, so the wrong path surfaces as an error you can act on rather than a -component that quietly stays off. +The gateway flags belong under `global.stacklok`. The console's chart +identifiers retain their original names: the flag is `cloudUi` and its +configuration key is `toolhive-cloud-ui`, so use those spellings in the values +file. -**The console's chart identifiers keep their original names.** The flag is -`cloudUi` and its configuration key is `toolhive-cloud-ui`, so use those -spellings verbatim even though the product surface is now the console. - -Enabling the Connector Gateway is a three-value decision. Neither -`global.stacklok.connectorGatewayId` nor `global.stacklok.authServerIssuer` has -a default, and the render fails naming whichever is missing rather than starting -a gateway that announces a placeholder identity or cannot complete a login. See +The Connector Gateway requires its enable flag, gateway ID, and authorization +server issuer. See [Configure the Connector Gateway](./configure-connector-gateway.mdx) and [Configure the AI Gateway](./configure-ai-gateway.mdx). The `toolhiveOperator` subchart deploys the enterprise build of the ToolHive -Operator: the same operator codebase as ToolHive Community, repackaged as a -hardened, signed, and versioned image. It manages the same workload types as the -Community operator, so the existing guides apply unchanged. Use +Operator. Use [Run MCP servers in Kubernetes](../../toolhive/guides-k8s/run-mcp-k8s.mdx) for MCP servers and remote proxies, and the [Virtual MCP Server guides](../../toolhive/guides-vmcp/index.mdx) for Virtual MCP Server (vMCP) gateways. -:::note[Namespaces] - -This guide installs the platform into `stacklok-system` and uses that namespace -throughout. You can choose a different one; adjust the commands to match. - -The operator can manage MCP server workloads in any namespace. Running them in -their own namespace, separate from the platform components in `stacklok-system`, -keeps the platform and the workloads it manages apart, and is the recommended -setup. The Kubernetes guides linked above use `toolhive-system` in their -examples, so substitute your own namespaces as you follow them. - -::: +This guide uses the `stacklok-system` namespace. Adjust the commands if you use +a different namespace. Run MCP server workloads in separate namespaces from the +platform components. ## Run preflight checks -Before you install, run the platform's **preflight** spec against your target -cluster. The umbrella chart ships a `Preflight` custom resource as a labeled -Secret, so rendering the chart and piping it to the `kubectl-preflight` plugin -analyzes real prerequisites automatically. It catches misconfigurations before -they surface later as failed pods or a stuck rollout: a Kubernetes version below -the chart floor, tight node capacity, an unreachable OIDC issuer, a missing -signing-key Secret. - -Running the check itself requires the same registry access, namespace, Secrets, -and `values.yaml` you use to install: complete -[Step 1](#1-authenticate-to-the-replicated-registry), +Before installation, render the chart and pass its `Preflight` resource to the +`kubectl-preflight` plugin. The checks cover Kubernetes version, capacity, +identity configuration, database connectivity, and required Secrets. + +Use the registry access, namespace, Secrets, and `values.yaml` prepared for the +installation. Complete [Step 1](#1-authenticate-to-the-replicated-registry), [Step 2](#2-prepare-secrets), and [Step 3](#3-configure-values) below first, then come back here before [Step 4](#4-install-the-chart). @@ -147,56 +105,27 @@ tar -xzf preflight.tar.gz preflight sudo install -m 0755 preflight /usr/local/bin/kubectl-preflight ``` -Pinning matters because the CLI and the spec move independently. Stacklok tests -this spec against the version above, so a newer CLI can grade a check -differently than Stacklok validated it, giving you a `pass` or `fail` that -doesn't reflect your cluster's real state. +Stacklok validates the preflight specification with the version above. -:::warning[Downloading the pinned version isn't enough to stay on it] +:::warning[Disable automatic updates] -The CLI updates itself. `--auto-update` defaults to true, so the binary you just -pinned replaces itself on its first run, printing a line like -`Updating preflight from 0.131.1 to 0.133.0...` as it goes. Pass -`--auto-update=false` on every invocation to hold the version: +The CLI enables automatic updates by default. Pass `--auto-update=false` on +every invocation to retain the validated version: ```bash kubectl preflight --auto-update=false - ``` -You can't confirm this after the fact by asking the binary, either. -`kubectl preflight version` keeps reporting the version you downloaded even once -it's running newer code, so treat the flag as the control and the version output -as unreliable. +`kubectl preflight version` reports the downloaded version even after an +automatic update. ::: -### Alternative: install with krew - -[krew](https://krew.sigs.k8s.io/), the `kubectl` plugin manager, is a quicker -one-time setup: - -```bash -kubectl krew install preflight -``` - -The tradeoff is that krew always resolves to whatever version its index -currently serves, and it has no version-pin flag, so a krew install starts on -whatever is current rather than the validated version. Pass -`--auto-update=false` here too, to at least stop it moving further. If you hit -unexpected preflight behavior, mention the version you installed when you open a -support case, and note that the CLI's own `version` output can't be trusted to -confirm it. - -krew also needs index access, so use the pinned download in an air-gapped -environment. - ### Run it against your real values -Use the `values.yaml` you build in [Step 3](#3-configure-values), not an empty -or minimal render. The opt-in analyzers (OIDC issuer, signing-key Secret, -registry database) only render when their corresponding values are set, so -preflighting against an empty file silently skips them, and a `pass` result -verifies less than your real install actually needs. +Use the complete `values.yaml` from [Step 3](#3-configure-values). Checks for +identity, signing keys, and the registry database render only when their +components are configured. ```bash helm template stacklok-enterprise \ @@ -209,19 +138,15 @@ helm template stacklok-enterprise \ `` and `` are the channel slug and chart version the install portal at [install.stacklok.com](https://install.stacklok.com) generates for -your release, in its **Existing cluster with Helm** instructions. Templating an -`oci://` reference pulls the chart, so it reuses the `helm registry login` -credentials from [Step 1](#1-authenticate-to-the-replicated-registry). There's -no credential prompt: if that login is missing or expired, `helm template` fails -with an authorization error. Log in again and rerun. +your release in its **Existing cluster with Helm** instructions. `helm template` +uses the registry credentials from +[Step 1](#1-authenticate-to-the-replicated-registry). Log in again if the +command returns an authorization error. :::warning[Pass the same `--namespace` you install into] -`--namespace` is not optional here. The chart renders the namespace into the -preflight spec, and `helm template` defaults it to `default` when the flag is -absent. The signing-key check then looks for your Secret in the wrong namespace -and reports `fail` on a cluster that is correctly prepared. Pass the same -`--namespace` value you use in [Step 4](#4-install-the-chart). +Pass the namespace used for installation. Otherwise, the signing-key check looks +for the Secret in the `default` namespace. ::: @@ -239,24 +164,16 @@ and reports `fail` on a cluster that is correctly prepared. Pass the same :::note[A cluster-internal OIDC issuer can fail from your workstation] -The OIDC check probes the issuer from wherever you run `kubectl preflight`, not -from inside the cluster. If your issuer is only reachable on the cluster -network, the check reports `fail` from a workstation even though the Enterprise -Manager pod would reach it fine. - -That's a difference in network vantage point, not a misconfiguration. Confirm it -by running the check from inside the cluster, which is what -[enforcement mode](#optional-enforce-preflight-checks-in-cluster) does. For a -publicly reachable issuer, treat a `fail` as real: it usually means a typo in -the issuer URL. +The OIDC check runs from the host executing `kubectl preflight`. For an issuer +available only inside the cluster, run the check in-cluster through +[enforcement mode](#optional-enforce-preflight-checks-in-cluster). Investigate a +failure for a publicly reachable issuer. ::: ### Check registry database reachability -The registry database check needs a connection URI, which the chart doesn't -ship. To exercise it, add these flags to the command above, alongside your real -`--values values.yaml`: +Pass a connection URI to enable the registry database check: ```bash helm template stacklok-enterprise \ @@ -269,59 +186,35 @@ helm template stacklok-enterprise \ | kubectl preflight --auto-update=false - ``` -The check reports `fail` if the database is unreachable. This is the only check -that touches a credential; the rest read capability metadata such as the -Kubernetes version, node capacity, and distribution. +This is the only check that reads a credential. :::warning[Pass the URI as a one-off flag] -Supply `preflightDatabaseUri` with `--set` at preflight time, never in a -permanent or GitOps-tracked values file. The preflight spec ships as a -Kubernetes Secret, so a persistently configured URI leaves the database password -in-cluster indefinitely, readable with `kubectl get secret ... -o yaml`, rather -than existing only for the duration of one preflight run. - -Don't commit or upload the rendered output either, since it carries the URI. +Supply `preflightDatabaseUri` with `--set` for the preflight command. A value in +a persistent values file remains in the generated Kubernetes Secret. Do not +commit or upload rendered output containing the URI. ::: ### Preflight is advisory by default -On the Helm CLI install path, `kubectl preflight` can't block `helm install`. A -`fail` result is a signal to act on, not an automatic gate: don't -[install the chart](#4-install-the-chart) while any check reports `fail`. Fix -the underlying cluster condition first. +The CLI check does not block `helm install`. Resolve every `fail` result before +[installing the chart](#4-install-the-chart). ### Optional: enforce preflight checks in-cluster -Set `preflight.enforce: true` in your values (or pass -`--set preflight.enforce=true`) to opt into an in-cluster pre-install and -pre-upgrade Helm hook Job that runs the same spec and genuinely fails -`helm install` or `helm upgrade` on a `fail` outcome. This is real blocking -rather than advisory. - -This is off by default because it carries cost you should accept deliberately: - -- The Job needs `global.replicated.dockerconfigjson` set. It pulls a - license-gated `preflight-runner` image before the Replicated SDK provisions - the ordinary pull secret; without it, the Job's pod can't pull its image and - the install hangs. On this standard (non-air-gapped) install path, Replicated - injects this value from your license when Helm pulls the chart, so you - normally don't set it by hand. If enforcement fails on an image pull, get the - value from the install portal's **Existing cluster with Helm** instructions - and set it explicitly. -- The Job runs under its own ServiceAccount with cluster-scoped read RBAC - (nodes, namespaces, storage classes, CRDs), plus a namespaced read on the - signing-key Secret when `enterpriseManager.enabled` is set. - -Enforcement does not change the credential exposure described in -[Check registry database reachability](#check-registry-database-reachability). -That exposure comes from setting `toolhive-registry-server.preflightDatabaseUri` -persistently at all: the password renders into the -`stacklok-enterprise-preflight` Secret, which carries no hook annotations and so -is never deleted, whether or not enforcement is on. Enforcement adds one further -copy in the Job's own Secret, and that one is hook-deleted when the hook -finishes. Either way, keep passing the URI as a one-off `--set`. +Set `preflight.enforce: true` to run the same checks in a pre-install and +pre-upgrade Helm hook. A `fail` result blocks the Helm operation. + +The Job uses a dedicated ServiceAccount with cluster-scoped read access to +nodes, namespaces, storage classes, and CRDs. When Enterprise Manager is +enabled, it also reads the signing-key Secret. The Replicated integration +supplies `global.replicated.dockerconfigjson` so the Job can pull the licensed +runner image. If the pull fails, copy this value from the install portal's +**Existing cluster with Helm** instructions. + +Pass `toolhive-registry-server.preflightDatabaseUri` as a one-time `--set` value +to avoid retaining the password in the preflight Secret. Enabling this on the [air-gapped path](./airgap-install.mdx) takes extra setup, since the Job's image defaults to a Stacklok-hosted registry. See @@ -333,8 +226,7 @@ If enforcement blocks an install, read the Job's log to see why: kubectl logs job/stacklok-enterprise-preflight-check -n stacklok-system ``` -The Job and both Secrets are named after your Helm release, so substitute your -own release name if it isn't `stacklok-enterprise`. +Replace `stacklok-enterprise` when you use a different Helm release name. To bypass a known false positive for one run, re-run the same `helm` command with `--set preflight.enforce=false`. @@ -375,10 +267,8 @@ chart, and per-release install instructions all live in the install portal at [install.stacklok.com](https://install.stacklok.com). Log in with the credentials Stacklok provides during onboarding. -The portal serves the chart from an OCI registry (`oci.stacklok.com`), not a -classic Helm chart repository, so you authenticate with `helm registry login` -rather than `helm repo add`. Use your license email as the username and your -**License ID** as the password: +Authenticate to the OCI registry at `oci.stacklok.com` with your license email +as the username and your **License ID** as the password: ```bash helm registry login oci.stacklok.com \ @@ -393,11 +283,9 @@ and the current chart version. Note those values; you reference them when you :::note[Image pulls during install] -You don't create an image pull secret for this online install. The chart's -Replicated integration provisions the pull credentials from your license, so the -cluster pulls component images at install time. The -[air-gapped path](./airgap-install.mdx) differs: you mirror the images and -create the pull secret yourself. +The chart's Replicated integration creates the image pull credentials from your +license. For an air-gapped installation, create the pull secret as described in +[Install from a private registry](./airgap-install.mdx). ::: @@ -610,10 +498,8 @@ The example above runs the Enterprise Manager without a database, which is a complete configuration: it serves signed configuration envelopes to Stacklok clients, and its database-backed modules stay switched off. -The directory service is the main one of those. Users, groups, connectors, -managed secrets, and virtual API keys all persist to PostgreSQL, so enabling -`enterprise-manager.directory.enabled` without a database leaves those surfaces -inert. Add the block below when you want them. +The directory stores users, groups, connectors, managed secrets, and virtual API +keys in PostgreSQL. Add the block below to enable these features. Provision two roles on your PostgreSQL instance first, in a database of their own: @@ -671,18 +557,14 @@ row-level security, and that holds only while the application role is subject to the policy. Keep `database.user` and `database.migration.user` separate, and keep the application role unprivileged. -The Enterprise Manager checks this at startup and refuses to run rather than -serve unprotected data: an application role carrying `BYPASSRLS` aborts the boot -with `app DB user "..." has BYPASSRLS — RLS is voided`. So the failure is loud, -but it is a failure, and you will not get a working install until the roles are -right. +The Enterprise Manager exits at startup if the application role carries +`BYPASSRLS`, with the error `app DB user "..." has BYPASSRLS - RLS is voided`. ::: -Cross-user connector administration is a separate, optional role again. Set -`enterprise-manager.database.admin.user` and its Secret (key -`postgres-admin-password`) to enable it; leave them unset and only that one -administrative surface returns an error. +Cross-user connector administration requires a separate database role. Set +`enterprise-manager.database.admin.user` and its Secret with the key +`postgres-admin-password` to enable it. That role must carry `BYPASSRLS`, and **nothing grants it for you**. The Enterprise Manager only checks the attribute and refuses to start without it, on @@ -699,17 +581,13 @@ ships with the chart; the `ALTER ROLE` above is the whole remedy. #### Encrypting stored credentials -The directory seals the credentials it stores, and the key-encryption key that -does the sealing is off by default. Leave it off and the credential-backed -surfaces refuse with a typed error rather than storing anything in the clear: -managed secrets, connector credentials, and the catalog entry the platform seeds -for your primary identity provider. +The directory uses a key-encryption key (KEK) to encrypt managed secrets, +connector credentials, and the catalog entry for your primary identity provider. +Enable the KEK to use these features. -Switching it on pulls in a chain of related settings, because the key is read -from the Kubernetes API and the decrypted plaintext crosses the gRPC port. The -chart enforces the whole chain with render-time errors that each name the value -to set, so a partial configuration fails loudly rather than serving credentials -unprotected: +The key is read from the Kubernetes API, and the decrypted plaintext crosses the +gRPC port. The chart validates the related settings at render time and +identifies any missing value: ```yaml title="values.yaml (add alongside the other enterprise-manager keys)" enterprise-manager: @@ -765,19 +643,16 @@ enterprise-manager: key: 'token' ``` -Use the claim your provider actually keys users on. `binding_claim` has no -default and the Enterprise Manager rejects an entry without one, naming the -issuer, so a wrong or missing value surfaces at startup rather than as failed -logins later. +Use the claim your provider uses as the stable user identifier. `binding_claim` +has no default. The Enterprise Manager validates the claim and issuer at +startup. #### Global Redis/Valkey defaults -Several platform components can share an external Redis or Valkey instance for -distributed session storage. Rather than repeating the configuration on every -component, set it once under `global.redis` and let the components that support -it inherit the value. Valkey is a drop-in replacement for Redis. This block is -optional: when `global.redis.host` is empty, the global default is inactive and -each component falls back to its own configuration. +Several platform components can inherit an external Redis or Valkey instance +from `global.redis` for distributed session storage. Valkey is a drop-in +replacement for Redis. When `global.redis.host` is empty, each component uses +its own configuration. ```yaml title="values.yaml (inside the existing global block)" global: @@ -823,12 +698,12 @@ the components connect without credentials. The following components inherit `global.redis`: -| Enable flag | How it uses the global default | -| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `toolhiveOperator` | Default session storage for `MCPServer`, `MCPRemoteProxy`, and `VirtualMCPServer` (vMCP) workloads that have no explicit `spec.sessionStorage`. The per-resource setting always overrides. | -| `global.stacklok.aiGateway.enabled` | Rate limiting and virtual API key storage, when the bundled Valkey instance is disabled. This one does fall back to a Secret named `redis-auth` with key `redis-password` when no name is configured. | +| Enable flag | How it uses the global default | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `toolhiveOperator` | Default session storage for `MCPServer`, `MCPRemoteProxy`, and `VirtualMCPServer` (vMCP) workloads that have no explicit `spec.sessionStorage`. The per-resource setting always overrides. | +| `global.stacklok.aiGateway.enabled` | NER scan-result caching when an `AIGateway` enables the cache | -Note that the embedded auth server's token storage is configured separately via +The embedded auth server's token storage is configured separately through `MCPExternalAuthConfig`. See [Configure session storage](../../toolhive/guides-k8s/embedded-auth-server-k8s.mdx#configure-session-storage) in the operator guide. @@ -913,20 +788,11 @@ helm install stacklok-enterprise \ --wait --timeout 10m ``` -`--wait` is what makes the exit status mean something. Without it Helm reports -`STATUS: deployed` as soon as the objects are accepted by the API server, which -it does even when every pod then fails to start. With it, Helm blocks until the -workloads report ready and exits non-zero if they don't inside the timeout. - -A timed-out install leaves the release in place rather than rolling it back, so -the failing pods stay available to inspect in [Step 5](#5-verify-the-install). -Rerun `helm upgrade` with the same flags once you have fixed the cause. +`--wait` keeps Helm running until the workloads are ready and returns a non-zero +exit status if they do not become ready within the timeout. ### 5. Verify the install -If the install returned successfully, the pods are already ready and this step -confirms what you have. If it timed out, this is where you find out why. - List the pods and check that each is `Running` and fully ready, with no restarts: @@ -947,11 +813,6 @@ kubectl describe pod -n stacklok-system kubectl logs -n stacklok-system ``` -`ImagePullBackOff` on every pod points at the license credentials rather than at -any one component: check that `enterprise-pull-secret` exists in the namespace -and that `replicated.enabled` is `true` in your values, since that subchart is -what creates it. - Confirm the ToolHive CRDs registered: ```bash @@ -979,16 +840,14 @@ Gateway resources your controller uses: | Component | Service (port) | Reached by | Hostname to route | | ------------------ | ---------------------------------------- | -------------------- | ----------------------------- | -| The console | `-toolhive-cloud-ui` (80) | Browsers | `betterAuth.url` | +| Console | `-toolhive-cloud-ui` (80) | Browsers | `betterAuth.url` | | Enterprise Manager | `-enterprise-manager` (80) | Stacklok CLI clients | `resourceURL` | | Registry Server | `registry-api` (8080) | Stacklok CLI clients | the registry's public API URL | -The Registry Server's Service name is fixed rather than release-prefixed. +The Registry Server uses the fixed Service name `registry-api`. -`betterAuth.url` is the public address the console tells browsers to return to -during sign-in. It does not create the route: the chart ships no ingress, so -publishing that hostname is yours to do, and the value has to match whatever you -publish. +Set `betterAuth.url` to the public console address that your ingress or gateway +publishes. The in-cluster URLs (`apiBaseUrl`, `enterpriseManagerUrl`) stay as Service DNS and need no routing. Once the routes resolve, confirm the Enterprise Manager @@ -1039,3 +898,22 @@ everything in `stacklok-system`, skip this step. clusters that can't reach Replicated at install time - [Configure platform identity](./configure-identity.mdx) - the identity provider configuration this deployment depends on as a prerequisite + +## Troubleshooting + +
+`helm install` times out + +Inspect the pods as described in [Step 5](#5-verify-the-install). The release +remains installed so its events and logs are available. After resolving the +cause, run `helm upgrade` with the same values and wait options. + +
+ +
+Every pod reports `ImagePullBackOff` + +Confirm that `enterprise-pull-secret` exists in the installation namespace and +that `replicated.enabled` is `true` in your values. + +
diff --git a/docs/platform/enterprise-platform/distributed-deployments.mdx b/docs/platform/enterprise-platform/distributed-deployments.mdx index 06ab93cb..a036d93d 100644 --- a/docs/platform/enterprise-platform/distributed-deployments.mdx +++ b/docs/platform/enterprise-platform/distributed-deployments.mdx @@ -5,16 +5,10 @@ description: enabling only the components each one needs. --- -The [standard deployment](./deployment.mdx) installs every platform component in -a single Kubernetes cluster. That is the recommended setup, but the umbrella -chart doesn't require it. Each component can be turned on or off independently, -so you can install the same chart multiple times, each release enabling only the -components that belong in that cluster or environment. - -This page covers two common distributed topologies. Both use the same umbrella -chart, the same onboarding artifacts, and the same configuration keys documented -on the per-component pages; only the set of enabled components changes between -releases. +The [standard deployment](./deployment.mdx) installs platform components in one +Kubernetes cluster. For distributed deployments, install the umbrella chart in +multiple clusters and enable only the components assigned to each environment. +This page covers a centralized control plane and layered registries. ## How it works @@ -24,24 +18,14 @@ Every component has its own enable flag in the umbrella chart's values: | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `toolhiveOperator` | ToolHive operator (runs MCP workloads) | | `enterpriseManager` | Enterprise Manager (serves policies and feature flags) | -| `cloudUi` | The console | +| `cloudUi` | Console | | `registryServer` | Registry Server (MCP server and skills catalog) | | `global.stacklok.aiGateway.enabled` | AI Gateway operator | | `global.stacklok.connectorGateway.enabled` | Connector Gateway. Also requires `global.stacklok.connectorGatewayId`, which must differ per install, and `global.stacklok.authServerIssuer`, the gateway's own public URL | -To build a distributed topology, install the chart once per cluster or -environment, each time with a `values.yaml` that enables only the components -that release should run. Components in different clusters reach each other over -their external URLs, the same way the Stacklok clients do, so wire cross-cluster -references to ingress hostnames rather than in-cluster Service DNS. - -:::note - -Configure each enabled component exactly as the per-component pages describe. -See [Configure the Enterprise Manager](../enterprise-manager/configure.mdx) and -[Configure the Registry Server](./configure-registry-server.mdx). - -::: +Install the chart once per cluster or environment and enable the required +components in each `values.yaml`. Configure cross-cluster references with +external ingress hostnames. ## Centralized control plane, operators in workload clusters @@ -55,7 +39,7 @@ flowchart TB subgraph Control["Control cluster"] EM["Enterprise Manager"] RS["Registry Server"] - CloudUI["The console"] + CloudUI["Console"] end subgraph WL1["Workload cluster A"] @@ -131,18 +115,9 @@ Point the console at the production instance, and scope each registry with identity claims so reviewers see the staging and development catalogs while everyone sees production. -:::note - -The per-registry claims drive the console's dropdown: a user sees only the -registries whose claims match their identity. The common pattern surfaces -development and staging to developers and reviewers while production stays -broadly visible. - -::: - ```mermaid flowchart TB - CloudUI["The console"] + CloudUI["Console"] subgraph Prod["Production Registry Server"] RegProd["production registry"] @@ -209,16 +184,14 @@ with the open source [Registry Server configuration](../../toolhive/guides-registry/configuration.mdx) reference. The enterprise build reads the same configuration schema. -If you'd rather not have the production instance carry the console and the -cross-environment `api` sources, deploy a dedicated Registry Server instance for -aggregation and point the console at it instead. The registry model is the same; -only the instance hosting the aggregating registries changes. +For a dedicated aggregation layer, deploy another Registry Server and point the +console to it. ## Next steps - [Deploy the platform](./deployment.mdx) for the single-cluster install the distributed topologies build on -- [Configure the Registry Server](./configure-registry-server.mdx) to wire +- [Configure the Registry Server](./configure-registry-server.mdx) to set sources, registries, and the database for each registry release - [Configure platform identity](./configure-identity.mdx) so every cluster's components share one identity provider diff --git a/docs/platform/enterprise-platform/index.mdx b/docs/platform/enterprise-platform/index.mdx index 4f858475..34bfb828 100644 --- a/docs/platform/enterprise-platform/index.mdx +++ b/docs/platform/enterprise-platform/index.mdx @@ -9,9 +9,8 @@ import DocCardList from '@theme/DocCardList'; :::enterprise -Stacklok Enterprise ships as one umbrella Helm chart that bundles every platform -component, so you install the whole platform in a single Helm release rather -than wiring up each chart yourself. +Stacklok Enterprise ships as an umbrella Helm chart that installs its platform +components in one release. [Learn more about Stacklok Enterprise](../index.mdx). @@ -27,13 +26,13 @@ authenticates every client and component. ```mermaid flowchart TB - Dev["Developer"] + Dev["End user"] Admin["Platform admin"] IdP["Identity provider (OIDC)"] subgraph Enterprise["Stacklok Enterprise (Kubernetes cluster)"] direction TB - Console["The console"] + Console["Console"] RS["Registry Server"] EM["Enterprise Manager"] Operator["ToolHive Operator
(MCP server workloads)"] @@ -65,12 +64,12 @@ its detailed guide. clients can authenticate. Do this first, because deployment wires in the client IDs and audiences you create here. See [Configure platform identity](./configure-identity.mdx). -1. **Deploy the platform.** Install the umbrella chart, wiring in the identity - values from the previous step. The chart deploys the ToolHive operator, the +1. **Deploy the platform.** Install the umbrella chart with the identity values + from the previous step. The chart deploys the ToolHive operator, the Enterprise Manager, the console, and the Registry Server as subcharts. See [Deploy the platform](./deployment.mdx). Before you install, [run preflight checks](./deployment.mdx#run-preflight-checks) to catch - cluster problems that would otherwise surface partway through the release. + cluster problems before Helm installs the release. 1. **Enable the gateways you need.** Both are off by default. See [Configure the AI Gateway](./configure-ai-gateway.mdx) and [Configure the Connector Gateway](./configure-connector-gateway.mdx). @@ -78,14 +77,15 @@ its detailed guide. provider, since access and budgets are granted to them. See [SCIM provisioning](../enterprise-directory/scim-provisioning.mdx). 1. **Configure policies.** Use the Enterprise Manager to pin the registry, - control non-registry servers, standardize telemetry, and shape the client + control non-registry servers, and standardize telemetry across clients. experience. See [Configure policies](../enterprise-manager/policies/). 1. **Set up authorization.** Map identity-provider groups and roles to MCP access with the enterprise authorization custom resources. See [Enterprise authorization](../enterprise-authz/index.mdx). 1. **Roll out the clients.** Distribute the [Stacklok CLI](../enterprise-cli/index.mdx) to your users, and point them at - [Connect a client](./connect-a-client.mdx) for their editors and agents. + [Roll out gateway clients](./roll-out-gateway-clients.mdx) to distribute + setup instructions for editors and agents. 1. **Verify end to end.** Sign in to [the console](../enterprise-console/index.mdx) and confirm the path from catalog to client. diff --git a/docs/platform/enterprise-platform/roll-out-gateway-clients.mdx b/docs/platform/enterprise-platform/roll-out-gateway-clients.mdx new file mode 100644 index 00000000..aa8c1861 --- /dev/null +++ b/docs/platform/enterprise-platform/roll-out-gateway-clients.mdx @@ -0,0 +1,47 @@ +--- +title: Roll out gateway clients +sidebar_label: Roll out gateway clients +description: + Direct users to deployment-specific client setup and choose authentication + methods for interactive and automated clients. +--- + +After you deploy the gateways, direct users to the setup instructions in the +console. The console supplies the endpoints and client-specific configuration +for your deployment. + +## Choose the gateway endpoints + +Clients can use either or both gateways: + +- Send OpenAI-compatible model traffic to the **AI Gateway**. +- Send MCP tool calls to the **Connector Gateway**. + +Users can open **Setup** in each gateway area of **Your workspace** for +deployment-specific instructions. This keeps endpoint details out of rollout +documentation and supports the clients listed in the console. + +## Choose an authentication method + +- Use browser-based sign-in for interactive clients. The session identifies the + caller through your corporate identity provider. +- Use a [virtual API key](../enterprise-directory/virtual-api-keys.mdx) for + scripts, scheduled jobs, continuous integration, and other clients without a + browser flow. Requests remain attributed to the key owner. + +## Prepare access before rollout + +Before distributing the setup instructions: + +- Assign an AI Gateway budget to each user or group that will send model + traffic. +- Grant directory groups access to the required connectors. +- Tell users that connectors requiring OAuth prompt them to authenticate through + **Your workspace** or during their first client connection. + +## Next steps + +- [Manage budgets](../../ai-gateway/manage-budgets.mdx) before enabling model + traffic. +- [Manage connectors](../../connector-gateway/connectors.mdx) to grant tool + access. diff --git a/docs/platform/enterprise-platform/verify-artifacts.mdx b/docs/platform/enterprise-platform/verify-artifacts.mdx index 00f083e6..dd057220 100644 --- a/docs/platform/enterprise-platform/verify-artifacts.mdx +++ b/docs/platform/enterprise-platform/verify-artifacts.mdx @@ -5,16 +5,11 @@ description: Enterprise container images with cosign. --- -You can independently verify the Stacklok-built container images you pull -through the Replicated proxy (`image-proxy.stacklok.com`) without any access to -Stacklok's internal registry or source repository. Verification works straight -through the proxy with your license and the open source `cosign` tool. - -All Stacklok images are signed with **keyless** Cosign signing through GitHub -OIDC. There are no long-lived signing keys. Each signature is tied to the GitHub -Actions workflow identity that produced it and recorded in Sigstore's public -transparency log (Rekor). Each image also carries three signed attestations: an -SPDX SBOM, SLSA build provenance, and an OpenVEX vulnerability assessment. +Verify Stacklok-built container images through the Replicated proxy with your +license and the open source Cosign tool. Stacklok signs each image through +GitHub OIDC and records the signature in Sigstore's Rekor transparency log. Each +image also includes an SPDX software bill of materials (SBOM), SLSA build +provenance, and an OpenVEX vulnerability assessment. Replace these placeholders throughout: @@ -26,27 +21,15 @@ Replace these placeholders throughout: - `` and `` - the namespace and label selector for a running workload, when you read a digest from the cluster. -:::note[Verification relies on the proxy's Referrers behavior] - -Customers pull images through the Replicated proxy, not directly from a public -registry. Cosign stores signatures and attestations as OCI referrers. The proxy -returns a spec-compliant `404` on the OCI Referrers endpoint, so Cosign falls -back to the referrers tag schema and verification works through the proxy with -no re-signing. If you are on an older proxy deployment and verification fails -with a `500` or `Internal Server Error` on a `/referrers/` request, contact -Stacklok. - -::: - ## Prerequisites - [`cosign`](https://docs.sigstore.dev/cosign/system_config/installation/) v2.x - or v3.x. This is the only tool you must install. -- An OCI client you almost certainly already have, used only to log in and look - up a digest: `docker` (or `podman`). [`oras`](https://oras.land/) or - [`crane`](https://github.com/google/go-containerregistry) work too if you - prefer. Cosign reuses this client's credential store, so there is no separate - Cosign login. + or v3.x. +- An OCI client to authenticate and resolve image digests: `docker`, `podman`, + [`oras`](https://oras.land/), or + [`crane`](https://github.com/google/go-containerregistry). Cosign uses the + client's credential store. +- `jq` and `base64` to inspect decoded attestation payloads. - Outbound HTTPS to `image-proxy.stacklok.com`, plus `fulcio.sigstore.dev`, `rekor.sigstore.dev`, and `tuf-repo-cdn.sigstore.dev` for Sigstore's roots. - A valid, non-expired license. The proxy authenticates every request against @@ -55,15 +38,12 @@ Stacklok. ## Step 1: Authenticate to the proxy -Log in once with whatever client you already use. The username is your customer -email, and the **password is the license ID** (it is the bearer credential for -the proxy, exactly as for chart pulls). All of these write to the same -credential store (`~/.docker/config.json` or `$REGISTRY_AUTH_FILE`) that Cosign -reads, so you do not run any Cosign-specific login. +Log in with your customer email as the username and your **license ID** as the +password. The client writes the credentials to `~/.docker/config.json` or +`$REGISTRY_AUTH_FILE`, where Cosign can use them. ```bash -# Docker (most common, since you already use it for image pulls). -# Paste at the password prompt. +# Docker docker login image-proxy.stacklok.com -u "" # Podman @@ -78,34 +58,35 @@ crane auth login image-proxy.stacklok.com -u "" --password-stdin <<< :::tip[Keep the license ID out of your shell history] -Pipe it on stdin instead of typing it inline: +Read the license ID into a temporary variable, then pipe it to `docker login`: ```bash -echo "" | docker login image-proxy.stacklok.com \ +read -rsp 'License ID: ' LICENSE_ID +printf '\n' +printf '%s' "$LICENSE_ID" | docker login image-proxy.stacklok.com \ -u "" --password-stdin +unset LICENSE_ID ``` ::: ## Step 2: Resolve the image digest -Verify **by digest**, never by a floating tag. Get the digest whichever way is -easiest, using a tool you already have. The proxy path the examples use is the -current one; the install portal prints the exact registry path for your release. +Resolve the digest of the image you deployed. The install portal provides the +registry path for your release. ```bash BASE=image-proxy.stacklok.com/proxy/stacklok-enterprise/ghcr.io/stacklok/stacklok-enterprise -# Best: read the digest of what you are ACTUALLY running, straight from the -# cluster (no extra tooling). +# Read the digest from the running workload. kubectl -n get pod -l \ -o jsonpath='{.items[0].status.containerStatuses[0].imageID}' -# docker, without pulling (buildx ships with modern Docker) +# Resolve with Docker without pulling the image. docker buildx imagetools inspect "$BASE/:" \ --format '{{json .Manifest.Digest}}' | tr -d '"' -# docker, plain (pulls the image, which you are doing anyway) +# Pull and inspect with Docker. docker pull -q "$BASE/:" >/dev/null \ && docker inspect --format '{{index .RepoDigests 0}}' "$BASE/:" @@ -120,19 +101,15 @@ Set `DIGEST=sha256:...` from whichever command you used. :::note -Cosign can also verify a tag directly -(`cosign verify ... "$BASE/:"`) and resolves the digest itself. -Pinning to the digest you actually deployed (the `kubectl` line above) is the -stronger check, since a tag can be repointed. +Cosign can resolve and verify a tag directly. Verifying the deployed digest +ensures that you check the exact image running in your cluster. ::: ## Step 3: Verify the image -The trust anchor is the signing identity: the signature was produced by a -workflow under -`github.com/stacklok/stacklok-enterprise-platform/.github/workflows/`, through -GitHub's OIDC issuer. The regex below matches all three image-signing workflows. +Verify that GitHub's OIDC issuer signed the image through one of Stacklok's +release workflows: ```bash IDENTITY='^https://github\.com/stacklok/stacklok-enterprise-platform/\.github/workflows/_release-(image|toolhive-cloud-ui|upstream-repackaged)\.yml@.*$' @@ -149,10 +126,7 @@ cosign verify \ "$IMAGE" ``` -A successful run prints `Verification for ... --` followed by the validated -claims, including the line "The code-signing certificate was verified using -trusted certificate authority certificates", and a JSON array of the signed -payloads. +A successful run prints the validated claims and signed payloads. ### Verify the SBOM attestation @@ -168,10 +142,8 @@ cosign verify-attestation \ | jq '{name: .predicate.name, spdxVersion: .predicate.spdxVersion, packages: (.predicate.packages | length)}' ``` -The first lines confirm the attestation's signature, transparency-log, and -certificate checks. The optional `jq` pipeline decodes the SPDX document and -prints a summary (its name, SPDX version, and package count). Drop the `jq` -filter to see the full SBOM. +The pipeline decodes the SPDX document and prints its name, SPDX version, and +package count. Remove the final `jq` filter to inspect the full SBOM. ### Verify the SLSA build provenance @@ -188,23 +160,9 @@ cosign verify-attestation \ | jq '.predicate.runDetails.builder.id, .predicate.buildDefinition.buildType' ``` -`--type slsaprovenance1` selects the SLSA provenance v1.0 schema; it is not a -"level" selector. The build L3 property comes from how the provenance was -produced (isolated GitHub Actions reusable workflows, non-forgeable OIDC-bound -signing). Confirm it by checking that `runDetails.builder.id` is a trusted -`stacklok-enterprise-platform/.github/workflows/...` reusable-workflow identity -and that the certificate identity above matched. - -:::note[Provenance availability] - -Cosign-verifiable provenance is present on releases built after Stacklok -introduced it. For older releases, only a GitHub-native provenance attestation -exists, which is not customer-verifiable because it requires read access to the -private source repository. If `cosign verify-attestation --type slsaprovenance1` -reports no matching attestation, the image predates this and you can request the -provenance from Stacklok. - -::: +`--type slsaprovenance1` selects the SLSA provenance v1.0 schema. Confirm that +`runDetails.builder.id` identifies a trusted +`stacklok-enterprise-platform/.github/workflows/...` reusable workflow. ### Verify the OpenVEX attestation @@ -223,10 +181,8 @@ cosign verify-attestation \ | jq -r '.predicate.statements[] | "[\(.status)] \(.vulnerability.name) \(.justification // .impact_statement // "")"' ``` -The first lines confirm the attestation's signature, transparency-log, and -certificate checks. The `jq` pipeline lists each CVE and its assessment. To feed -the document to a scanner that consumes OpenVEX (for example, `trivy --vex`), -write the decoded predicate to a file: +The `jq` pipeline lists each CVE and its assessment. To use the document with an +OpenVEX-compatible scanner, write the decoded predicate to a file: ```bash cosign verify-attestation \ @@ -248,40 +204,18 @@ oras discover "$IMAGE" ## Which images are signed -| Image (``) | Signing workflow | Verifiable | -| ------------------------------------------------------------------------------------------------------ | ---------------------------------- | :--------: | -| `thv`, `operator`, `proxyrunner`, `vmcp`, `registry-api`, `toolhive-enterprise` | `_release-image.yml` | ✓ | -| `ai-gateway-operator`, `ai-gateway-main-processor`, `ai-gateway-api-key-service` | `_release-image.yml` | ✓ | -| `cloud-ui` | `_release-toolhive-cloud-ui.yml` | ✓ | -| `ai-gateway-controller`, `ai-gateway-extproc`, `envoy-gateway`, `envoy-ratelimit`, `presidio-analyzer` | `_release-upstream-repackaged.yml` | ✓ | -| Third-party dependencies (Stacklok License Manager) | upstream, not re-signed | n/a | - -Envoy Gateway, Envoy Ratelimit, and Presidio are repackaged (re-signed with a -Stacklok signature, SBOM, and provenance) rather than shipped as upstream -passthrough — verify them the same way as any other image in this table. Only -the Stacklok License Manager image remains genuine third-party passthrough. - -:::note[Third-party dependency images] - -The Stacklok License Manager (the in-cluster component branded from the -Replicated SDK — it appears as `stacklok-license-manager-*` in -`kubectl get pods`) is not re-signed by Stacklok, and unlike every other image -on this page, it is not rewritten through `image-proxy.stacklok.com` at all — it -is pulled directly from Replicated's own registry at -`proxy.replicated.com/library/replicated-sdk-image`. Verify it against its -original publisher's signature if required. It is pulled by digest, so its -content is pinned. +| Image (``) | Signing workflow | +| ------------------------------------------------------------------------------------------------------ | ---------------------------------- | +| `thv`, `operator`, `proxyrunner`, `vmcp`, `registry-api`, `toolhive-enterprise` | `_release-image.yml` | +| `ai-gateway-operator`, `ai-gateway-main-processor`, `ai-gateway-api-key-service` | `_release-image.yml` | +| `cloud-ui` | `_release-toolhive-cloud-ui.yml` | +| `ai-gateway-controller`, `ai-gateway-extproc`, `envoy-gateway`, `envoy-ratelimit`, `presidio-analyzer` | `_release-upstream-repackaged.yml` | +| Stacklok License Manager | Upstream signature | -::: - -The repackaged third-party images each play a specific role in the platform: - -| Dependency | Role in the stack | -| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | -| Envoy Gateway | Kubernetes Gateway API data plane that fronts the AI gateway | -| Envoy Ratelimit | Rate-limiting service backing the AI gateway's token budget enforcement | -| Presidio | Microsoft Presidio engine for PII and PCI detection and redaction in prompts | -| Stacklok License Manager | In-cluster component (Replicated SDK, rebranded) that reports install status and license state to Replicated (not re-signed) | +Stacklok repackages and signs Envoy Gateway, Envoy Ratelimit, and Presidio. The +Stacklok License Manager retains its upstream image and signature. It appears as +`stacklok-license-manager-*` in `kubectl get pods` and is pulled by digest from +`proxy.replicated.com/library/replicated-sdk-image`. ## Verify across all images @@ -312,8 +246,8 @@ done ## Next steps -- [Configure platform identity](./configure-identity.mdx) to wire your identity - provider to the platform components +- [Configure platform identity](./configure-identity.mdx) to connect your + identity provider to the platform components - [Configure policies](../enterprise-manager/policies/) to control client behavior across your organization @@ -337,17 +271,25 @@ OCI Referrers endpoint so Cosign falls back to the tag schema. Contact Stacklok.
`401 Unauthorized` on login or pull -Confirm `` is the password (not the username) and that your license -is assigned to the channel serving the version. Expired licenses fail here. +Use your customer email as the username and `` as the password. +Confirm that your license is valid and assigned to the channel serving the +version.
`certificate identity ... does not match` -Cosign prints the actual certificate identity it found. Confirm it is a -`github.com/stacklok/stacklok-enterprise-platform/.github/workflows/...` path -signed by `https://token.actions.githubusercontent.com`. If it is not, do not -trust the image and contact Stacklok. +Trust only a +`github.com/stacklok/stacklok-enterprise-platform/.github/workflows/...` +identity signed by `https://token.actions.githubusercontent.com`. Contact +Stacklok for any other identity or issuer. + +
+ +
+`no matching attestation` for SLSA provenance + +Request the provenance for your release from Stacklok.
diff --git a/docs/platform/index.mdx b/docs/platform/index.mdx index 162310d4..16e02c2a 100644 --- a/docs/platform/index.mdx +++ b/docs/platform/index.mdx @@ -13,7 +13,7 @@ import BrandedList from '@site/src/components/BrandedList'; Stacklok Enterprise gives your organization centralized control over how LLMs and MCP-connected tools are accessed, used, and secured. It combines an AI -Gateway for model routing, budget enforcement, and broad model coverage with an +Gateway for model routing, budget enforcement, and broad model coverage with a Connector Gateway for tool catalog management, connection policies, and per-user configuration. Built on ToolHive, our popular open source MCP project, Stacklok Enterprise brings enterprise-grade control, visibility, and security to AI @@ -84,60 +84,60 @@ them. ### Distribution & packaging -| Capability | Community | Enterprise | -| :-------------------------------------------------------- | :--------: | :----------------------------------------: | -| ToolHive core platform | ✓ | ✓ | -| Release model | Continuous | Semantically versioned (MAJOR.MINOR.PATCH) | -| Sigstore Cosign package signing with SBOM | ✓ | ✓ | -| Patch versions retained for bugfixes and security updates | — | ✓ | -| Scanning attestations | — | ✓ | -| SLSA build provenance | — | ✓ | +| Capability | Community | Enterprise | +| :--------------------------------------------------------- | :--------: | :----------------------------------------: | +| ToolHive core platform | ✓ | ✓ | +| Release model | Continuous | Semantically versioned (MAJOR.MINOR.PATCH) | +| Sigstore Cosign package signing with SBOM | ✓ | ✓ | +| Patch versions retained for bug fixes and security updates | No | ✓ | +| Scanning attestations | No | ✓ | +| SLSA build provenance | No | ✓ | ### Security and supply chain | Capability | Community | Enterprise | | :------------------------------------------------------- | :-------: | :--------: | | Basic scanning (Trivy, unit tests, integration tests) | ✓ | ✓ | -| Static analysis on every release (attested via SigStore) | — | ✓ | -| Autonomous pen testing on every minor release | — | ✓ | -| Hardened container base images (Chainguard or equiv.) | — | ✓ | -| Proactive notification of vulnerabilities | — | ✓ | -| CVEs addressed within SLO with responsible disclosure | — | ✓ | -| All Sev 0-3 vulnerabilities backported as patch updates | — | ✓ | - -### Auth, identity & governance - -| Capability | Community | Enterprise | -| :---------------------------------------------------------------- | :-------: | :--------: | -| OIDC/OAuth authentication | ✓ | ✓ | -| Policy-as-code engine (Cedar) | ✓ | ✓ | -| Audit logging & compliance reporting | ✓ | ✓ | -| Token exchange (RFC 8693) | ✓ | ✓ | -| Turnkey IdP integration (Okta, Entra ID) | — | ✓ | -| [IdP group → ToolHive role mapping](./enterprise-authz/index.mdx) | — | ✓ | -| Entra ID on-behalf-of flow | — | ✓ | -| Canonical policy packs (read-only, full CRUD, custom) | — | ✓ | - -### Interfaces & management - -| Capability | Community | Enterprise | -| :----------------------------------------------------------------------------------- | :-------: | :--------: | -| ToolHive CLI | ✓ | ✓ | -| Usage telemetry & analytics (OpenTelemetry) | ✓ | ✓ | -| Enterprise MCP registry server and catalog | ✓ | ✓ | -| [The console](./enterprise-console/index.mdx) (administration and end-user surfaces) | — | ✓ | -| [Stacklok CLI](./enterprise-cli/index.mdx) (centrally enforced client policy) | — | ✓ | - -### Versioning, maintenance & support +| Static analysis on every release (attested via Sigstore) | No | ✓ | +| Autonomous pen testing on every minor release | No | ✓ | +| Hardened container base images | No | ✓ | +| Proactive notification of vulnerabilities | No | ✓ | +| CVEs addressed within SLO with responsible disclosure | No | ✓ | +| All Sev 0-3 vulnerabilities backported as patch updates | No | ✓ | + +### Authentication, identity, and governance + +| Capability | Community | Enterprise | +| :----------------------------------------------------------------- | :-------: | :--------: | +| OIDC/OAuth authentication | ✓ | ✓ | +| Policy-as-code engine (Cedar) | ✓ | ✓ | +| Audit logging and compliance reporting | ✓ | ✓ | +| Token exchange (RFC 8693) | ✓ | ✓ | +| Turnkey IdP integration (Okta, Entra ID) | No | ✓ | +| [IdP group to ToolHive role mapping](./enterprise-authz/index.mdx) | No | ✓ | +| Entra ID on-behalf-of flow | No | ✓ | +| Canonical policy packs (read-only, full CRUD, custom) | No | ✓ | + +### Interfaces and management + +| Capability | Community | Enterprise | +| :------------------------------------------------------------------------------------ | :-------: | :--------: | +| ToolHive CLI | ✓ | ✓ | +| Usage telemetry and analytics (OpenTelemetry) | ✓ | ✓ | +| Enterprise MCP registry server and catalog | ✓ | ✓ | +| [Console](./enterprise-console/index.mdx) (administration and self-service workflows) | No | ✓ | +| [Stacklok CLI](./enterprise-cli/index.mdx) (centrally enforced client policy) | No | ✓ | + +### Versioning, maintenance, and support | Capability | Community | Enterprise | | :--------------------------------------------- | :-------: | :--------: | | Latest release | ✓ | ✓ | -| Supported versions: LATEST, LATEST-1, LATEST-2 | — | ✓ | +| Supported versions: LATEST, LATEST-1, LATEST-2 | No | ✓ | | Community support (GitHub) | ✓ | ✓ | -| Dedicated support with SLA | — | ✓ | -| Proactive security advisories | — | ✓ | -| Onboarding & integration assistance | — | ✓ | +| Dedicated support with SLA | No | ✓ | +| Proactive security advisories | No | ✓ | +| Onboarding and integration assistance | No | ✓ | Seen enough to want a closer look? [Schedule a demo](#schedule-a-demo) to walk through the capabilities that matter most to your team. @@ -171,7 +171,7 @@ CLI**. Functionally it is the enterprise edition of its Community counterpart, with additional policy-enforcement and identity-provider features, so documentation that refers to the ToolHive CLI applies to it as well. -### Registry: No more fighting shadow AI +### Registry | The source of truth for approved MCP servers within the enterprise. | | :----------------------------------------------------------------------- | @@ -182,7 +182,7 @@ documentation that refers to the ToolHive CLI applies to it as well. | Verify provenance and sign servers with built-in security controls | | Preset configurations and permissions for a frictionless user experience | -### Runtime: Kubernetes-native deployment +### Runtime | Deploy, run, and manage MCP servers in Kubernetes with security guardrails. | | :-------------------------------------------------------------------------- | @@ -192,7 +192,7 @@ documentation that refers to the ToolHive CLI applies to it as well. | Kubernetes Operator for fleet and resource management | | Leverage OpenTelemetry for centralized monitoring and audit logging | -### Connector Gateway: Tool catalog and connection governance +### Connector Gateway | Single endpoint for MCP tool access, policy enforcement, and per-user configuration. | | :------------------------------------------------------------------------------------- | @@ -202,7 +202,7 @@ documentation that refers to the ToolHive CLI applies to it as well. | Reduce context bloat and token usage | | Connect with local clients like Claude Desktop, Cursor, and VS Code | -### AI Gateway: Model routing and spend control +### AI Gateway | Governed access point for LLM providers with budget enforcement and full audit trail. | | :------------------------------------------------------------------------------------ | @@ -214,7 +214,7 @@ documentation that refers to the ToolHive CLI applies to it as well. [Explore the AI Gateway documentation](../ai-gateway/index.mdx) for full details. -### Console: Self-service with guardrails +### Console | One place for teams to discover, deploy, and manage approved MCP servers. | | :------------------------------------------------------------------------ | diff --git a/docs/platform/reference/crds/index.mdx b/docs/platform/reference/crds/index.mdx index d39c4c3f..310bd1bb 100644 --- a/docs/platform/reference/crds/index.mdx +++ b/docs/platform/reference/crds/index.mdx @@ -6,10 +6,9 @@ description: import DocCard from '@theme/DocCard'; -The Stacklok Enterprise platform extends the Kubernetes API with custom -resources for role-based access control and authorization policy enforcement. -Each page below documents one resource - its fields, defaults, validation rules, -and a minimal example manifest - and links to the other resources it references. +Stacklok Enterprise adds Kubernetes custom resources for role-based access +control and authorization policy enforcement. Each reference includes the +resource fields, defaults, validation rules, and an example manifest. ## Enterprise authorization diff --git a/sidebars.ts b/sidebars.ts index e21b23ef..3e0cd923 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -365,7 +365,7 @@ const platformSidebar: SidebarsConfig[string] = [ 'platform/enterprise-platform/configure-identity', 'platform/enterprise-platform/configure-ai-gateway', 'platform/enterprise-platform/configure-connector-gateway', - 'platform/enterprise-platform/connect-a-client', + 'platform/enterprise-platform/roll-out-gateway-clients', 'platform/enterprise-platform/api-reference', ], }, @@ -454,18 +454,25 @@ const platformSidebar: SidebarsConfig[string] = [ defaultStyle: false, }, - 'platform/concepts/what-you-can-see', 'platform/concepts/two-group-models', ]; const connectorGatewaySidebar: SidebarsConfig[string] = [ - 'connector-gateway/index', + { + type: 'doc', + id: 'connector-gateway/index', + className: 'enterprise-only enterprise-only--tooltip-below', + }, 'connector-gateway/connectors', 'connector-gateway/tool-usage', ]; const aiGatewaySidebar: SidebarsConfig[string] = [ - 'ai-gateway/index', + { + type: 'doc', + id: 'ai-gateway/index', + className: 'enterprise-only enterprise-only--tooltip-below', + }, 'ai-gateway/providers-and-models', 'ai-gateway/model-routing', 'ai-gateway/budgets-and-pricing', diff --git a/src/css/custom.css b/src/css/custom.css index a6d7b6f5..142b53ee 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -523,6 +523,16 @@ details summary:hover { z-index: 10; } +/* Keep tooltips for the first sidebar item clear of the top navigation. */ +.enterprise-only--tooltip-below + > .menu__link:is(:hover, :focus-visible)::before, +.enterprise-only--tooltip-below + > .menu__list-item-collapsible + > .menu__link:is(:hover, :focus-visible)::before { + top: calc(100% + 6px); + bottom: auto; +} + /* Screenshot styling with subtle border */ .screenshot { border: 1px solid var(--ifm-table-border-color);