Skip to content

Commit 00dc1a4

Browse files
committed
Document SPIFFE association declarations
A configuration-only foundation could otherwise be mistaken for completed SVID authentication and bundle management. Document the trust and static-client flow, validation boundaries, restart behavior, and the explicit absence of live credential verification. Also add missing item-length CRD validation markers on SPIFFEClientConfig's Audiences/Scopes fields, matching the sibling fields on the same type. Refs #6200 Refs #6205 Signed-off-by: Jakub Hrozek <jakub@stacklok.com>
1 parent f8ce076 commit 00dc1a4

10 files changed

Lines changed: 159 additions & 2 deletions

cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,8 @@ type SPIFFEClientConfig struct {
768768
// +kubebuilder:validation:Required
769769
// +kubebuilder:validation:MinItems=1
770770
// +kubebuilder:validation:MaxItems=50
771+
// +kubebuilder:validation:items:MinLength=1
772+
// +kubebuilder:validation:items:MaxLength=2048
771773
// +listType=set
772774
Audiences []string `json:"audiences"`
773775

@@ -776,6 +778,8 @@ type SPIFFEClientConfig struct {
776778
// +kubebuilder:validation:Required
777779
// +kubebuilder:validation:MinItems=1
778780
// +kubebuilder:validation:MaxItems=50
781+
// +kubebuilder:validation:items:MinLength=1
782+
// +kubebuilder:validation:items:MaxLength=256
779783
// +listType=set
780784
Scopes []string `json:"scopes"`
781785
}

deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,8 @@ spec:
558558
description: Audiences are RFC 8693 token audiences
559559
this association may request.
560560
items:
561+
maxLength: 2048
562+
minLength: 1
561563
type: string
562564
maxItems: 50
563565
minItems: 1
@@ -614,6 +616,8 @@ spec:
614616
Scopes are OAuth scopes granted to this association. Must be a subset
615617
of the server's effective supported scopes.
616618
items:
619+
maxLength: 256
620+
minLength: 1
617621
type: string
618622
maxItems: 50
619623
minItems: 1
@@ -3209,6 +3213,8 @@ spec:
32093213
description: Audiences are RFC 8693 token audiences
32103214
this association may request.
32113215
items:
3216+
maxLength: 2048
3217+
minLength: 1
32123218
type: string
32133219
maxItems: 50
32143220
minItems: 1
@@ -3265,6 +3271,8 @@ spec:
32653271
Scopes are OAuth scopes granted to this association. Must be a subset
32663272
of the server's effective supported scopes.
32673273
items:
3274+
maxLength: 256
3275+
minLength: 1
32683276
type: string
32693277
maxItems: 50
32703278
minItems: 1

deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,8 @@ spec:
434434
description: Audiences are RFC 8693 token audiences
435435
this association may request.
436436
items:
437+
maxLength: 2048
438+
minLength: 1
437439
type: string
438440
maxItems: 50
439441
minItems: 1
@@ -490,6 +492,8 @@ spec:
490492
Scopes are OAuth scopes granted to this association. Must be a subset
491493
of the server's effective supported scopes.
492494
items:
495+
maxLength: 256
496+
minLength: 1
493497
type: string
494498
maxItems: 50
495499
minItems: 1
@@ -5096,6 +5100,8 @@ spec:
50965100
description: Audiences are RFC 8693 token audiences
50975101
this association may request.
50985102
items:
5103+
maxLength: 2048
5104+
minLength: 1
50995105
type: string
51005106
maxItems: 50
51015107
minItems: 1
@@ -5152,6 +5158,8 @@ spec:
51525158
Scopes are OAuth scopes granted to this association. Must be a subset
51535159
of the server's effective supported scopes.
51545160
items:
5161+
maxLength: 256
5162+
minLength: 1
51555163
type: string
51565164
maxItems: 50
51575165
minItems: 1

deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,8 @@ spec:
561561
description: Audiences are RFC 8693 token audiences
562562
this association may request.
563563
items:
564+
maxLength: 2048
565+
minLength: 1
564566
type: string
565567
maxItems: 50
566568
minItems: 1
@@ -617,6 +619,8 @@ spec:
617619
Scopes are OAuth scopes granted to this association. Must be a subset
618620
of the server's effective supported scopes.
619621
items:
622+
maxLength: 256
623+
minLength: 1
620624
type: string
621625
maxItems: 50
622626
minItems: 1
@@ -3212,6 +3216,8 @@ spec:
32123216
description: Audiences are RFC 8693 token audiences
32133217
this association may request.
32143218
items:
3219+
maxLength: 2048
3220+
minLength: 1
32153221
type: string
32163222
maxItems: 50
32173223
minItems: 1
@@ -3268,6 +3274,8 @@ spec:
32683274
Scopes are OAuth scopes granted to this association. Must be a subset
32693275
of the server's effective supported scopes.
32703276
items:
3277+
maxLength: 256
3278+
minLength: 1
32713279
type: string
32723280
maxItems: 50
32733281
minItems: 1

deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,8 @@ spec:
437437
description: Audiences are RFC 8693 token audiences
438438
this association may request.
439439
items:
440+
maxLength: 2048
441+
minLength: 1
440442
type: string
441443
maxItems: 50
442444
minItems: 1
@@ -493,6 +495,8 @@ spec:
493495
Scopes are OAuth scopes granted to this association. Must be a subset
494496
of the server's effective supported scopes.
495497
items:
498+
maxLength: 256
499+
minLength: 1
496500
type: string
497501
maxItems: 50
498502
minItems: 1
@@ -5099,6 +5103,8 @@ spec:
50995103
description: Audiences are RFC 8693 token audiences
51005104
this association may request.
51015105
items:
5106+
maxLength: 2048
5107+
minLength: 1
51025108
type: string
51035109
maxItems: 50
51045110
minItems: 1
@@ -5155,6 +5161,8 @@ spec:
51555161
Scopes are OAuth scopes granted to this association. Must be a subset
51565162
of the server's effective supported scopes.
51575163
items:
5164+
maxLength: 256
5165+
minLength: 1
51585166
type: string
51595167
maxItems: 50
51605168
minItems: 1

docs/arch/09-operator-architecture.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ MCPExternalAuthConfig resources can be referenced via two paths:
238238

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

241+
`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.
242+
241243
### MCPOIDCConfig
242244

243245
Defines shared OIDC provider configuration that can be referenced by multiple workload CRDs (MCPServer, MCPRemoteProxy, VirtualMCPServer) in the same namespace.

docs/arch/11-auth-server-storage.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,17 @@ All call sites use `unwrapStorage(stor)` or the equivalent JWT-bearer constructi
431431
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.
432432

433433
**Implementation:** `pkg/authserver/storage/cimd_decorator.go`
434+
435+
## SPIFFE Storage Decorator
436+
437+
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.
438+
439+
### What it does
440+
441+
`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.
442+
443+
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.
444+
445+
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.
446+
447+
**Implementation:** `pkg/authserver/storage/spiffe_decorator.go`

0 commit comments

Comments
 (0)