Expose SPIFFE client-auth registration through the operator CRDs - #6500
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6500 +/- ##
==========================================
+ Coverage 78.49% 78.51% +0.02%
==========================================
Files 776 776
Lines 76415 76494 +79
==========================================
+ Hits 59983 60063 +80
+ Misses 16427 16426 -1
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
JAORMX
left a comment
There was a problem hiding this comment.
The CRD conversion and generated artifacts are largely aligned, but the panel found material validation/lifecycle gaps:
-
principalPatternrejects~in path segments (cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:731-738), although it is valid in SPIFFE URI path segments. This makes the served CRD schema narrower than the authoritative runtime parser. Please accept the full permitted segment grammar (including~) and add concrete/wildcard admission coverage. -
Context-independent SPIFFE errors can still lead
MCPExternalAuthConfigto reportValid=True: bundle endpoint URLs are not structurally validated and overlapping principal policies are only rejected later by runtime validation (cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:647-653, 890-891, 2366-2373). Validate these at admission or in the config controller; defer only checks that genuinely require workload-derived allowlists. -
The multi-issuer validator owns background
jwk.Cacheworkers but neither it nor the auth server closes them; constructor error paths also leave earlier caches running (pkg/authserver/server/tokenexchange/multi_issuer_validator.go:415-445,549;pkg/authserver/server_impl.go:396-400). Add lifecycle cleanup on server close and on partial construction failure, with tests.
The red Go Vulnerability Check is inherited from the unchanged dependency in the stacked base and appears unrelated.
5834d9c to
6dbb23e
Compare
b758757 to
f57d37e
Compare
6dbb23e to
e038ceb
Compare
f57d37e to
c5e6f6e
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Re-review at c5e6f6e6654575c784b132321caf72c543d9dd3a: none of the three prior blockers are resolved, and the same inline-configuration parity gap exists here.
- CRD
principalPatternstill rejects valid SPIFFE~path segments (cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:731-738). - Malformed bundle endpoints and overlapping principal patterns still bypass validation on the owning config CRD and can be reported
Valid=True(mcpexternalauthconfig_types.go:647-653,883-893;controllers/mcpexternalauthconfig_controller.go:106-176). - JWK cache workers still have no cleanup on server shutdown or partial validator construction (
pkg/authserver/server/tokenexchange/multi_issuer_validator.go:433-470,557-578;pkg/authserver/server_impl.go:375-400). VirtualMCPServeralso still rejects advertised SPIFFE-only inline configuration because its guard ignoresinboundGrants.spiffeClientAuth(cmd/thv-operator/controllers/virtualmcpserver_controller.go:670-685).
CI is green; no local tests were run.
JAORMX
left a comment
There was a problem hiding this comment.
Changes requested:
cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:737:principalPatternexcludes~, which the authoritative SPIFFE parser permits in path segments. Permit it and add admission coverage.cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:649-653,2366-2373andcmd/thv-operator/pkg/controllerutil/authserver.go:1022-1024: malformed bundle endpoints and overlapping principal patterns are publishedValid=Trueuntil a consuming workload fails. Validate context-independent invariants at the owning config boundary.cmd/thv-operator/controllers/virtualmcpserver_controller.go:670: includeinboundGrants.spiffeClientAuthin inline auth-server validation; the shared CRD currently admits a SPIFFE-only shape that this guard rejects.
e038ceb to
ae26bc4
Compare
c5e6f6e to
ff2fc59
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Changes requested:
cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:737:principalPatternstill rejects~, which the authoritative SPIFFE parser permits in path segments. Permit it and add concrete and wildcard admission coverage.cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:649-653,890-891andcmd/thv-operator/controllers/mcpexternalauthconfig_controller.go:106-176: malformed bundle endpoints and overlapping principal patterns can be publishedValid=Trueat the owning config boundary and only fail when a consuming workload validates the runtime configuration. Validate these context-independent invariants at admission or in the config controller.cmd/thv-operator/controllers/virtualmcpserver_controller.go:670-685: includeinboundGrants.spiffeClientAuthin inline auth-server validation; a SPIFFE-only inline shape admitted by the shared CRD is still rejected here.
All current CI checks are green; no local tests were run.
ae26bc4 to
ce2447f
Compare
ff2fc59 to
3b10dc8
Compare
d634568 to
6123649
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Correction after rechecking the deployed go-spiffe parser and your pushback: you are right about ~. ToolHive pins go-spiffe/v2 v2.7.0 without the spiffeid_charset_backcompat build tag, so the runtime accepts [A-Za-z0-9._-] and rejects ~. The CRD regex correctly matches that runtime. I withdraw that finding.
One independently established correctness blocker remains at this head:
cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:756-780,2368-2374: the owningMCPExternalAuthConfigcan reportValid=Truefor independently invalid SPIFFE permission values. Audience and scope entries may be empty; resource entries may be empty or malformed instead of absolute HTTP(S) URIs without fragments. Runtime validation already separates these shape checks from the derived scope-subset/resource-allowlist checks (pkg/authserver/spiffe_trust.go:625-649,710-761), but it only runs later when a workload consumes the config.
Please validate the context-independent shapes at admission or in MCPExternalAuthConfig.Validate(), while keeping checks that require derived ScopesSupported and AllowedAudiences at reconciliation. This is based on the owning resource publishing a false validity result, not on CI; exact-head CI status is separate.
7f4d0b8 to
4d3eb12
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Re-review at 4d3eb12a637bdf7693e1ec7bfd4bb5df34fbad4f (only 6123649..4d3eb12, excluding the stacked base):
The prior validity-boundary blocker is partially resolved: empty audience and scope entries are now rejected, but resource validation is still too weak.
- Blocking:
cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:756-770,2542-2556accepts a non-empty resource matching^https?://[^[:space:]]+$, includinghttps://backend.example/#fragmentand userinfo-bearing forms. The authoritative runtime validator rejects these independently invalid resource indicators inpkg/authserver/spiffe_trust.go:710-744, but it only runs when a workload consumes the config. Consequently, the owningMCPExternalAuthConfigmay publishValid=Truefor an invalid spec. Validate the context-independent absolute HTTP(S) URI shape at admission/config validation; retain the derived allowlist-membership check at reconciliation. Add fragment, userinfo, and missing-authority regression coverage, including theValid=Falsestatus path.
All other previously requested items remain resolved. This request is based on the independently established correctness/status-boundary defect, not CI.
74c96a8 to
d7abacf
Compare
4d3eb12 to
0cd94a9
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Re-review at 0cd94a97f350855fd56579cc01a98366cb79da4a (only d7abacf..0cd94a9, excluding the stacked base): the previously requested resource validation is still incomplete.\n\n- Blocking: cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:756-770 accepts resource values matching ^https?://[^[:space:]]+$, including https://user@example.com, https://example.com/path#fragment, and https:///missing-authority. MCPExternalAuthConfig.validateEmbeddedAuthServer deliberately defers permission-shaped validation (:2563-2581), so the owning controller can publish Valid=True before a consumer reaches the stricter runtime parser (pkg/authserver/spiffe_trust.go:730-744). These URI-shape checks do not require derived allowlists: reject userinfo, fragments, and empty/missing authorities at the owning configuration boundary (with CEL where expressible plus Go validation as appropriate), and add Valid=False regressions for all three cases.\n\nThis is an independently established configuration-validity correctness/security-boundary defect (CWE-20; RFC 8707 §2), not a CI-based decision. The other previously requested SPIFFE validation work is verified in this scoped layer.
d7abacf to
1c698d7
Compare
0cd94a9 to
bcbe083
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Re-review at bcbe08381052c669191a1cb10ab1089186d71072, scoped to this PR layer against merged base 500f478 (and treating the rebased, non-ancestor 0cd94a9 head as prior review context only):\n\n- Blocking — configuration-validity boundary: SPIFFEClientConfig.Resources uses only ^https?://[^[:space:]]+$ (cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:756-770), while MCPExternalAuthConfig.validateEmbeddedAuthServer explicitly defers resource validation (:2561-2581). Consequently the owning configuration can report Valid=True for independently invalid RFC 8707 resource indicators, including userinfo (https://user@example.com), fragments (https://example.com/#fragment), empty-host authorities (https://:443), and malformed/missing authorities. Shape validation does not require the workload-derived AllowedAudiences or ScopesSupported; keep those membership checks deferred, but reject invalid HTTP(S) URI shape at this owning boundary (including a non-empty URL.Hostname()), then add Valid=False regression coverage for these forms. The runtime parser currently checks URL.Host, not Hostname() (pkg/authserver/spiffe_trust.go:737-742), so it does not close the empty-host case either.\n\nThis is an independently established correctness/security-boundary issue (CWE-20; RFC 8707 §2), not a CI-based decision. Exact-head CI is currently 46 successful, 2 in progress, with no failures; that status is author-owned and does not affect this review decision.
bcbe083 to
9dd0362
Compare
The runtime already supports declaring SPIFFE trust domains and associating SPIFFE principals with OAuth clients, but only through a hand-authored auth-server RunConfig file; there was no way to declare it through the MCPExternalAuthConfig/VirtualMCPServer CRDs. Add SPIFFETrustDomainConfig and SPIFFEClientConfig CRD types (mirroring the runtime shape field-for-field), CEL admission validation for the paired-configuration, duplicate-name, and trust-domain/principal cross-reference invariants, and Go-level bridging into authserver.ValidateSPIFFETrust for the checks CEL cannot express. Audiences, scopes, and resource-indicator items now carry length and shape constraints at admission time, matching the convention already used for AllowedResources and TrustedIssuerConfig elsewhere in this file, instead of deferring all of it to reconcile time. Refs #6199 Signed-off-by: Jakub Hrozek <jakub@stacklok.com>
9dd0362 to
a9921a6
Compare
JAORMX
left a comment
There was a problem hiding this comment.
Re-review at a9921a6891ced2f88b8b30b8d0a1778f8431f4e1, scoped to the new delta from prior reviewed head 9dd036248a5d90721eff25980b0cb32f679c3c39.
Approved. This refactor folds resource-shape validation into the existing per-entry SPIFFE validation loop to bring validateEmbeddedAuthServer below the lint complexity threshold. It preserves the prior admission-time validation and indexed error paths: each resource still passes ValidateResourceIndicators before pairwise pattern overlap checks (cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go:2639-2656). Fresh code-review and Kubernetes-operator panel checks found no concrete blockers in this delta.
CI remains author-owned and separate from this approval decision.
Summary
The SPIFFE client-authentication runtime model (trust domains + static workload-to-client associations) already exists in
pkg/authserver, but was only reachable by hand-authoring an auth-server RunConfig file directly — there was no way to declare it throughVirtualMCPServer/MCPExternalAuthConfig.An earlier, never-reviewed attempt at this CRD exposure existed on an abandoned branch, but assumed a stale runtime shape (SPIFFE clients nested under
inboundGrants.tokenExchange) that no longer matches the reviewed model (inboundGrants.spiffeClientAuthis now a sibling oftokenExchange/jwtBearer, with a larger field surface: abundleSourcediscriminated union,resources, and a fixedgrantTypes). This PR redoes the CRD exposure from scratch against the current model, reusing the old attempt's validation contract (its CEL test matrix) as an acceptance checklist, not its code.spiffeTrustDomainsandinboundGrants.spiffeClientAuthto the sharedEmbeddedAuthServerConfig, with CEL admission validation for everything derivable from the object's own spec (paired configuration, no duplicate names/trust-domains/client-IDs/principal-patterns, full cross-referencing between domains and clients, method-subset enforcement, client-ID hygiene).authserver.RunConfigvia the same converter pattern used fordelegateClients/trustedIssuers, and extend reconcile-time revalidation to cover SPIFFE trust domains.grantTypesas a CRD field — the runtime only accepts exactly one value (urn:ietf:params:oauth:grant-type:token-exchange), so the converter sets it rather than exposing a field with one legal value.allowedAudiences,scopesSupported— neither is CRD-exposed) are deliberately left to the existing reconcile-time revalidation path rather than a premature admission-time Go check — an earlier draft of this PR added such a check passingnilfor both, which the runtime validator treats as "validate against nothing" rather than "skip," silently rejecting anyresourcesentry or non-defaultscopesvalue. Caught and removed during adversarial review; see commit history for detail.Refs #6199, #6200, #6205
Known, intentional consequence: a syntactically valid SPIFFE config is admitted by CEL but still fails reconciliation with a terminal error until
RunConfig's "not yet enforced" placeholder gate (pkg/authserver/config.go) is lifted by a future PR — no live SVID verification exists yet, so this is expected, not a regression.Type of change
Test plan
task test)task lint-fix)31-case CEL admission test matrix (
spiffe_cel_test.go) ported from the old attempt's test list and extended for thebundleSourcediscriminated union andresources; unit tests for the two new converter functions; a reconcile-level test proving a SPIFFE client withresources/customscopesis accepted at the admission-equivalent Go layer but still correctly validated (accepted or rejected as appropriate) once real derived values reachBuildAuthServerRunConfig.API Compatibility
v1beta1API — both new fields are optional and additive.Changes
Large diff (~3,384 lines), dominated by generated CRD YAML/docs (
deploy/charts/operator-crds/**,docs/operator/crd-api.md— roughly 2,200 of the changed lines) and the new CEL test file. Hand-written surface:mcpexternalauthconfig_types.go(new CRD types + 11 CEL rules),controllerutil/authserver.go(converter wiring), and their tests.Does this introduce a user-facing change?
Yes — operators can now declare
spiffeTrustDomains/inboundGrants.spiffeClientAuthonVirtualMCPServer/MCPExternalAuthConfig. Reconciliation of a non-empty config will fail with a clear terminal error until live SVID verification lands (see "Known, intentional consequence" above).Implementation plan
Approved implementation plan
Expose SPIFFE client-auth registration through the operator CRDs
Context
The SPIFFE client-auth epic (#6199, refs #6200/#6205) already landed a full
runtime model in
pkg/authserver(SPIFFETrustDomainRunConfig,SPIFFEClientAuthRunConfig,ValidateSPIFFETrust, the static-clientregistry/storage overlay, etc.), but that model is only reachable by hand-
authoring an auth-server
RunConfigfile — there is no way to declare itthrough
VirtualMCPServer/MCPExternalAuthConfigCRDs.An old, never-reviewed branch (
spiffe-integration-split3-5/6/7, abandonedmid-epic when the stack was reworked through review) had already built this
CRD-exposure feature once, but its commit (
0c921b295, "Expose SPIFFEregistration in CRDs") assumed a runtime shape that no longer matches: it
nested SPIFFE clients under
InboundGrants.TokenExchange.SPIFFEClientsandnever modeled
BundleSource/Resources/GrantTypes. The current, reviewedruntime model puts
SPIFFEClientAuthas a top-level sibling ofTokenExchangeonInboundGrantsRunConfig(deliberately independent of thetoken-exchange capability toggle — see the doc comment on
InboundGrantsRunConfiginpkg/authserver/spiffe_trust.go), and has amaterially larger
SPIFFEClientAuthRunConfig/SPIFFETrustDomainRunConfigsurface than the old commit ever covered. Rather than merge that stale
commit, we're redoing the CRD-exposure work from scratch against the current
model, reusing the old commit's validation contract (its CEL test matrix)
as an acceptance checklist, not its code.
Known, intentional consequence:
RunConfig.Validate()currentlyhard-rejects any non-empty
SPIFFETrustDomainsviavalidateSPIFFENotYetEnforced— a deliberate placeholder until real SVIDverification lands (see
pkg/authserver/config.go:328-345). Wiring CRDfields into
RunConfig.SPIFFETrustDomainsmeans a syntactically valid CRDspec will be admitted by CEL/webhook validation but will still fail
reconciliation with a terminal
InvalidEmbeddedAuthServerConfigErroruntilthat gate is lifted by a future PR. This is expected, not a bug — call it
out explicitly in the PR description.
Scope
In scope: two new CRD types (
SPIFFETrustDomainConfig,SPIFFEClientConfig) plus a bundle-source discriminated union, added to theshared
EmbeddedAuthServerConfig(used by bothMCPExternalAuthConfigandVirtualMCPServer— no VMCP-specific code needed, same as the old commitfound). CEL admission validation for everything expressible from the
object's own spec. Go-level bridging into the existing
authserver.ValidateSPIFFETrust/NewSPIFFETrustConfigfor everything thatneeds derived values (
AllowedAudiences,ScopesSupported— neither isCRD-exposed; both are derived at reconcile time, confirmed via
cmd/thv-operator/pkg/controllerutil/authserver.go:883-895) or the fullSPIFFE-ID/URL grammar. Reconcile-time revalidation via the existing
validateDelegateClientsAndTrustedIssuerspattern.Out of scope: bundle fetching/loading, live SVID verification, lifting
the
validateSPIFFENotYetEnforcedgate (all still placeholder by design),and the legacy-field-removal refactor that was tangled into the old commit
(
0c921b295) — that already landed separately via0501c3fea/split3-5.CRD types —
cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.goMirror the runtime shape exactly (field-for-field with
SPIFFETrustDomainRunConfig/SPIFFEClientAuthRunConfiginpkg/authserver/spiffe_trust.go), using the same validation-marker idioms asneighboring types (
DelegateClientConfigat line 361,TrustedIssuerConfigat line 404) —
+kubebuilder:validation:MinLength/MaxLength,MinItems/MaxItems,+listType=atomic/set,+optionalon every pointerfield,
//nolint:lllon types carrying long CEL rules.SPIFFEAuthenticationMethodenum (spiffe_x509,spiffe_jwt),SPIFFEBundleSourceTypeenum (bundle_endpoint,workload_api),SPIFFEBundleEndpointProfileenum (https_web,https_spiffe) —straight string-const mirrors of the runtime enums.
SPIFFEBundleSourceConfig(discriminated union, required field onSPIFFETrustDomainConfig— the runtime field has noomitempty):Type SPIFFEBundleSourceTypeEndpoint *SPIFFEBundleEndpointSourceConfig(URL string,Profile SPIFFEBundleEndpointProfile)WorkloadAPI *SPIFFEWorkloadAPIBundleSourceConfig(empty struct)XValidationrules reusing the exact pattern already used forMCPExternalAuthConfig's own type-discriminated union (line 67-75):self.type == 'bundle_endpoint' ? has(self.endpoint) : !has(self.endpoint)and the
workload_api/workloadAPImirror.SPIFFETrustDomainConfig:Name,TrustDomain(pattern-validated,copy the old commit's regex — it's a reasonable approximation and the
type doc comment should say runtime parsing via
spiffeid.TrustDomainFromStringremains authoritative, matching the existing "CEL is best-effort" convention
already used elsewhere in this file),
Methods(MinItems=1,MaxItems=2,enum),
BundleSource SPIFFEBundleSourceConfig(required, no+optional).SPIFFEClientConfig(lives in a newSPIFFEClientAuth []SPIFFEClientConfigfield onInboundGrantsConfig— a siblingof
TokenExchange/JWTBearer, not nested underTokenExchange, matchingInboundGrantsRunConfig's shape):TrustDomainRef,PrincipalPattern(pattern-validated + the oldcommit's path-traversal CEL rule
self.principalPattern.split('/').all(segment, segment != '.' && segment != '..')),ClientID,Methods(MinItems=1,MaxItems=2),Audiences(
MinItems=1,MaxItems=50),Scopes(MinItems=1,MaxItems=50),Resources(+optional,MaxItems=50— new vs. the old commit; RFC8707 resources, Go-validated against
AllowedAudiencessince that'sderived, not CRD-known).
GrantTypesas a CRD field. The runtime only acceptsexactly
["urn:ietf:params:oauth:grant-type:token-exchange"](
validateSPIFFEGrants,spiffe_trust.go:758-766) — a field with onelegal value is a field the converter should just set, not a knob users
configure. Set it in the converter (
GrantTypes: []string{authserver.SPIFFEGrantTypeTokenExchange}).SPIFFETrustDomains []SPIFFETrustDomainConfigstays onEmbeddedAuthServerConfigdirectly (top-level, matchingRunConfig.SPIFFETrustDomains) —MinItems=1,MaxItems=50, +listType=atomic, +optional, same placement the old commit used.CEL rules on
EmbeddedAuthServerConfig(add alongside its existingXValidationblock): reuse the old commit's semantic set, adjusted forthe new field path (
self.inboundGrants.spiffeClientAuth, notself.inboundGrants.tokenExchange.spiffeClients):spiffeTrustDomainsandinboundGrants.spiffeClientAuthempty/non-empty together)nametrustDomainvaluetrustDomainRefresolves to a declared domainmethods⊆ referenced domain'smethodsprincipalPattern's trust-domain segment matches the referenceddomain's
trustDomainclientIdprincipalPatternclientIdmust not start withsynthetic:(reserved) or look like anabsolute URL (reserved for CIMD) — reuse
storage.ValidateRegisterableClientID's intent as the CEL approximation,Go-side stays authoritative
Go bridging — same file, new
validateSPIFFETrustConfighelperAlongside the existing
validateEmbeddedAuthServer(): convertcfg.SPIFFETrustDomains→[]authserver.SPIFFETrustDomainRunConfigandcfg.InboundGrants.SPIFFEClientAuth→[]authserver.SPIFFEClientAuthRunConfig(setting
GrantTypesto the fixed value here), callauthserver.ValidateSPIFFETrust(trustDomains, inboundGrants, nil, nil)for aconfig-shape-only check at admission time (no
AllowedAudiences/ScopesSupportedavailable yet — same limitation already accepted forDelegateClients/TrustedIssuersat this layer).Controller wiring —
cmd/thv-operator/pkg/controllerutil/authserver.gobuildSPIFFETrustDomainRunConfigs([]mcpv1beta1.SPIFFETrustDomainConfig) []authserver.SPIFFETrustDomainRunConfigand
buildSPIFFEClientAuthRunConfigs([]mcpv1beta1.SPIFFEClientConfig) []authserver.SPIFFEClientAuthRunConfig(pure field copy + slice clone, no error — same shape as
buildTrustedIssuerRunConfigsat line 269, since nothing here needs secretresolution).
buildInboundGrantsRunConfig(line 824): setgrants.SPIFFEClientAuth = buildSPIFFEClientAuthRunConfigs(config.SPIFFEClientAuth)as a sibling assignment alongside the existing
TokenExchange/JWTBearerblocks.
BuildAuthServerRunConfig(line 889): setconfig.SPIFFETrustDomains = buildSPIFFETrustDomainRunConfigs(authConfig.SPIFFETrustDomains)next to the existing
config.InboundGrants = inboundGrantsassignment.validateDelegateClientsAndTrustedIssuers(line 1034): extend the early-return guard to also check
len(config.SPIFFETrustDomains) == 0, and addSPIFFETrustDomains: config.SPIFFETrustDomainstovalidationConfigsoRunConfig.Validate()catches SPIFFE misconfiguration (including the"not yet enforced" gate) as a reconcile error, not a pod crash loop —
consistent with why this function exists at all.
Tests
(
spiffe_cel_test.go, ~30 cases: paired-config, duplicate name/trustDomain/clientId/principalPattern, unreferenced/unknown trust-domain-ref,
methods-not-a-subset, principal/trust-domain-segment mismatch, reserved
clientId prefixes, trust-domain string edge cases, principal path-segment
edge cases) rewritten against the new field path
(
inboundGrants.spiffeClientAuth, not.tokenExchange.spiffeClients) andextended for
BundleSource's discriminated union and the newResourcesfield.
admitted by CEL but produces a terminal
InvalidEmbeddedAuthServerConfigErrorreconcile failure (proving the "not yet enforced" gate is reachable end-to-
end through the CRD path, not just the RunConfig-file path already tested
in
pkg/authserver).validateSPIFFETrustConfig, following this file's existing table-drivenconvention.
Verification
task operator-manifeststhentask operator-generate— confirm CRDYAML and deepcopy regenerate cleanly (watch for the same missing-JSON-tag
class of controller-gen failure hit earlier in this session).
task lint-fix— 0 issues.task test— full suite green, including the new CEL/reconcile tests(these need
envtest/a real apiserver — same suite that already runscmd/thv-operator/test-integration/mcp-external-auth/*_cel_test.go).VirtualMCPServerwith valid SPIFFE fields against alocal kind cluster (or envtest) and confirm: CRD admission succeeds, then
the reconciler surfaces the expected "not yet enforced" terminal
condition — not a crash loop, not silent success.
Delegation
Hand off implementation to a fresh agent (this is CRD/operator work spanning
Go API types, CEL, and controller wiring —
kubernetes-expertor a freshgo-expert-developer), then run an adversarial Opus review pass beforecommitting, matching this session's established mechanism for anything
security/protocol-relevant. Land as
spiffe-integration-split3-6on top ofthe already-verified
spiffe-integration-split3-5.Special notes for reviewers
This PR was implemented by a fresh agent against the approved plan above, then went through one full adversarial review round: the reviewer found two real bugs (an admission-time Go check that unconditionally rejected any
resources/custom-scopesentry due to a bad nil-handling assumption in the plan itself, and missingtask crdref-genoutput) plus an untested design deviation — all fixed and re-verified before this PR was opened. Stacked on #6499.