Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,8 @@ type SPIFFEClientConfig struct {
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=50
// +kubebuilder:validation:items:MinLength=1
// +kubebuilder:validation:items:MaxLength=2048
// +listType=set
Audiences []string `json:"audiences"`

Expand All @@ -776,6 +778,8 @@ type SPIFFEClientConfig struct {
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=50
// +kubebuilder:validation:items:MinLength=1
// +kubebuilder:validation:items:MaxLength=256
// +listType=set
Scopes []string `json:"scopes"`
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,8 @@ spec:
description: Audiences are RFC 8693 token audiences
this association may request.
items:
maxLength: 2048
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -614,6 +616,8 @@ spec:
Scopes are OAuth scopes granted to this association. Must be a subset
of the server's effective supported scopes.
items:
maxLength: 256
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -3209,6 +3213,8 @@ spec:
description: Audiences are RFC 8693 token audiences
this association may request.
items:
maxLength: 2048
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -3265,6 +3271,8 @@ spec:
Scopes are OAuth scopes granted to this association. Must be a subset
of the server's effective supported scopes.
items:
maxLength: 256
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ spec:
description: Audiences are RFC 8693 token audiences
this association may request.
items:
maxLength: 2048
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -490,6 +492,8 @@ spec:
Scopes are OAuth scopes granted to this association. Must be a subset
of the server's effective supported scopes.
items:
maxLength: 256
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -5096,6 +5100,8 @@ spec:
description: Audiences are RFC 8693 token audiences
this association may request.
items:
maxLength: 2048
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -5152,6 +5158,8 @@ spec:
Scopes are OAuth scopes granted to this association. Must be a subset
of the server's effective supported scopes.
items:
maxLength: 256
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,8 @@ spec:
description: Audiences are RFC 8693 token audiences
this association may request.
items:
maxLength: 2048
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -617,6 +619,8 @@ spec:
Scopes are OAuth scopes granted to this association. Must be a subset
of the server's effective supported scopes.
items:
maxLength: 256
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -3212,6 +3216,8 @@ spec:
description: Audiences are RFC 8693 token audiences
this association may request.
items:
maxLength: 2048
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -3268,6 +3274,8 @@ spec:
Scopes are OAuth scopes granted to this association. Must be a subset
of the server's effective supported scopes.
items:
maxLength: 256
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,8 @@ spec:
description: Audiences are RFC 8693 token audiences
this association may request.
items:
maxLength: 2048
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -493,6 +495,8 @@ spec:
Scopes are OAuth scopes granted to this association. Must be a subset
of the server's effective supported scopes.
items:
maxLength: 256
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -5099,6 +5103,8 @@ spec:
description: Audiences are RFC 8693 token audiences
this association may request.
items:
maxLength: 2048
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down Expand Up @@ -5155,6 +5161,8 @@ spec:
Scopes are OAuth scopes granted to this association. Must be a subset
of the server's effective supported scopes.
items:
maxLength: 256
minLength: 1
type: string
maxItems: 50
minItems: 1
Expand Down
2 changes: 2 additions & 0 deletions docs/arch/09-operator-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ MCPExternalAuthConfig resources can be referenced via two paths:

**Controller**: `cmd/thv-operator/controllers/mcpexternalauthconfig_controller.go`

`EmbeddedAuthServerConfig` can also carry top-level SPIFFE trust-domain declarations in `spiffeTrustDomains` and canonical client associations in `inboundGrants.spiffeClientAuth` (`cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go`). The operator converts these CRD fields into runtime config via `buildSPIFFETrustDomainRunConfigs`/`buildSPIFFEClientAuthRunConfigs` in `cmd/thv-operator/pkg/controllerutil/authserver.go`, which flow into the RunConfig delivered to the proxy-runner alongside the rest of the embedded auth server settings. See [SPIFFE Association Declarations](18-spiffe-association-declarations.md) for the full configuration and policy model — these declarations currently register associations and static clients only; they do not perform live SVID verification or load trust bundles.

### MCPOIDCConfig

Defines shared OIDC provider configuration that can be referenced by multiple workload CRDs (MCPServer, MCPRemoteProxy, VirtualMCPServer) in the same namespace.
Expand Down
14 changes: 14 additions & 0 deletions docs/arch/11-auth-server-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,17 @@ All call sites use `unwrapStorage(stor)` or the equivalent JWT-bearer constructi
When the embedded authorization server is deployed in an environment that cannot reach `https://toolhive.dev/oauth/client-metadata.json` or any public CIMD metadata URL, set `authServer.cimd.enabled: false`. Clients will fall back to DCR (`/oauth/register`) which uses only the local storage backend and requires no outbound connectivity.

**Implementation:** `pkg/authserver/storage/cimd_decorator.go`

## SPIFFE Storage Decorator

When top-level `spiffeTrustDomains` and `inboundGrants.spiffeClientAuth` are configured, the embedded authorization server wraps its storage backend in a `SPIFFEStorageDecorator` — installed as the outermost decorator, after CIMD (`decorateStorageForSPIFFE` in `pkg/authserver/server_impl.go`). This decorator overlays a fixed set of statically configured OAuth clients ahead of the dynamic DCR/CIMD backend. These declarations register associations and clients; the current server does not yet verify live X.509-SVIDs or JWT-SVIDs.

### What it does

`SPIFFEStorageDecorator` embeds the full `storage.Storage` interface and overrides `GetClient`, `RegisterClient`, and `ReconcileConfiguredClient`. `GetClient` checks its static client map first and only falls through to the wrapped storage (CIMD, then DCR) when the requested client ID is not one of the configured associations. `RegisterClient` and `ReconcileConfiguredClient` reject any DCR, delegate-client, or configured-client attempt that targets a client ID reserved by a static SPIFFE association. CIMD never calls `RegisterClient`; it only resolves and caches HTTPS client IDs, so it cannot collide with a static association.

Its clients come entirely from the configured SPIFFE trust-domain and client-association declarations (see [SPIFFE Association Declarations](18-spiffe-association-declarations.md)). They are built once at startup, held in memory, and never written to the storage backend (memory or Redis); they are never eligible for dynamic registration or replacement.

At startup, the decorator durably claims each configured static client ID in the storage backend (memory or Redis) via `ReconcileConfiguredClient` (`preflightDurableCollisions`), using an inert placeholder rather than the real client. This is create-only for anything except a matching restart: it succeeds when the ID is unclaimed or already holds a matching placeholder from a prior run with the same configuration, and fails — refusing to start the server — when the ID is DCR-issued or holds a placeholder for a *different* association. This closes a cross-replica race that a read-only `GetClient` check alone cannot: with Redis and multiple replicas, an older or still-rolling replica without this SPIFFE config could otherwise DCR-register the same client ID after a newer replica's read-only check passed. The reverse collision can't happen: the decorator's `GetClient` always checks its static map first, so a durable client can never shadow a static one.

**Implementation:** `pkg/authserver/storage/spiffe_decorator.go`
Loading
Loading