Trust private CAs for upstream auth servers - #6428
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #6428 +/- ##
==========================================
+ Coverage 77.82% 77.88% +0.05%
==========================================
Files 762 766 +4
Lines 73449 73959 +510
==========================================
+ Hits 57165 57600 +435
- Misses 16279 16354 +75
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
samuv
left a comment
There was a problem hiding this comment.
Thanks for the thorough vertical implementation—the per-upstream mounts, additive system/private trust, DCR propagation, and checksum-driven rollouts are thoughtfully designed. The TLS propagation itself looks correct and retains hostname verification. However, several reconciliation and status-lifecycle paths need correction, and the Swagger documentation check currently fails.
Blockers
-
[BLOCKER]
cmd/thv-operator/pkg/controllerutil/authserver.go:346— CA validation occurs too late forauthServerRef.handleAuthServerRefmarks the reference valid before bundle validation; malformed bundles then fail during Deployment construction and repeatedly requeue whileAuthServerRefValidated=True. Validate bundles in bothhandleAuthServerRefpaths, record the correct terminal condition, and add MCPServer/MCPRemoteProxy regression tests. Rule: operator “Terminal vs transient errors.” -
[BLOCKER]
cmd/thv-operator/controllers/mcpserver_controller.go:2328andmcpremoteproxy_controller.go:950— repairing an invalid ConfigMap does not clear the failure condition. MCPServer never restoresExternalAuthConfigValidated=True; MCPRemoteProxy deliberately preservesInvalidConfigwhile the external-auth spec hash is unchanged, and ConfigMap content changes do not alter that hash. Add invalid→valid recovery handling and tests. Rule: operator “Status is reconstructable from observed state.” -
[BLOCKER]
cmd/thv-operator/controllers/virtualmcpserver_controller.go:486— all CA-resolution errors are converted into terminal validation failures. Ordinary transient ConfigMapGetfailures are swallowed throughreturn false, so reconciliation can stop without backoff until another event happens. OnlyInvalidCABundleErrorshould be terminal; return other errors to controller-runtime. Rule: operator “No log-and-swallow.” -
[BLOCKER]
pkg/authserver/config.go:551— the new JSON/YAML-visible fields require regenerated Swagger artifacts, butdocs/server/swagger.json,swagger.yaml, anddocs.goare stale. The required “Verify Swagger Documentation” check currently fails. Runtask docsand commit the generated changes.
Should-fix
-
[SHOULD-FIX]
cmd/thv-operator/pkg/controllerutil/ca_bundle.go:47—ValidateCABundleSourceapplies the legacy 48-character ConfigMap-name limit needed byoidc-ca-bundle-<name>. These new volumes are index-named, so otherwise valid longer ConfigMap names are rejected terminally. Use shape-only validation for embedded-auth bundles and test a longer legal name. -
[SHOULD-FIX]
examples/operator/external-auth/mcpexternalauthconfig_private_ca.yaml:34— the example omitsconfigMapRef.keyand says it defaults toca.crt, but the generated CRD requireskey. Addkey: ca.crt; otherwise the example is rejected by admission. -
[SHOULD-FIX] PR commits — commits
0703597and0e43d3flack theSigned-off-bytrailers required byCONTRIBUTING.md.
Verification notes
- CRD compatibility, code generation, linting, unit tests, security scans, and Kubernetes lifecycle E2E checks pass.
- Private CA trust is consistently propagated through OIDC discovery/JWKS/token/userinfo and OAuth2 DCR.
- No existing review comments were present.
89af105 to
baddbd6
Compare
samuv
left a comment
There was a problem hiding this comment.
Follow-up on the earlier review: Swagger regeneration landed and the docs check is green. The three reconciliation blockers are still open on this head, and two of the should-fixes were not applied.
Still blocking
-
authServerRefstill validates too late.ValidateEmbeddedAuthServerCABundleswas added tohandleExternalAuthConfigonly.handleAuthServerRefstill marksAuthServerRefValidated=Truebefore any bundle check; malformed bundles fail during Deployment construction and requeue.handleInvalidUpstreamCABundlealways writesExternalAuthConfigValidated, including forauthServerReffailures. -
Repairing an invalid ConfigMap still does not restore a valid condition. MCPServer never sets
ExternalAuthConfigValidated=Trueafter a successful CA check. MCPRemoteProxy still preservesInvalidConfigwhile the external-auth spec hash is unchanged, and ConfigMap content does not change that hash. No invalid→valid recovery tests were added. -
VirtualMCPServer still treats every CA-resolution error as terminal.
validateAuthServerConfigCABundlesstampsAuthServerConfigInvalidfor all errors, andrunAuthValidationsreturnsfalse(no requeue). OnlyInvalidCABundleErrorshould be terminal; ConfigMapGetfailures must return to controller-runtime. The helper tests already classify this correctly; the controller does not use that distinction.
Still should-fix
- 48-character ConfigMap name limit —
ResolveCABundlestill callsValidateCABundleSource, which enforces theoidc-ca-bundle-volume-name cap. These volumes are index-named (authserver-upstream-ca-{index}). - Example still omits required
key— the CRD requiresconfigMapRef.key;examples/operator/external-auth/mcpexternalauthconfig_private_ca.yamlstill has only a comment. Admission will reject it. Signed-off-by— the original commits were rewritten with the trailer, but the three follow-up commits (b7a0f616,b3152f88,bdadc0b) still lack it.
Addressed
- Stale Swagger artifacts / failing “Verify Swagger Documentation” check.
Checklist
- Tests: CA helper classification and terminal-invalid coverage exist; authServerRef validation and invalid→valid recovery tests are still missing.
- Docs: Swagger and CRD reference regenerated. Example manifest is still invalid under the generated schema.
- Registry: no impact.
- Security: TLS trust path unchanged from the first review (hostname verification retained, additive system+bundle trust).
- Backwards compatibility: additive
caBundleRef; no v1beta1 break.
|
Thanks for the second pass. That review landed against bdadc0b a few minutes before I pushed the fixes, so it's reviewing the older head — all three blockers and both code should-fixes are on 8e95962 now. Mapping them: Blocker 1, late validation on authServerRef: 3e79675. Both Blocker 2, recovery: 30a7798 and 315a153. Root cause was CA failures sharing Blocker 3, vMCP: 45c2579. Should-fix 1: 06b5157, One thing your review didn't flag that I hit while writing the tests: the existing Tests: 8 authServerRef CA tests across the two controllers, 3 repair-then-reconcile tests, 2 vMCP classification tests, plus long-name coverage in the validation and ca_bundle packages. I checked each one fails without its production change — the first flap test I wrote passed either way because metav1.Time is second-resolution and a remove-then-re-add inside one second is invisible, so it seeds a timestamp an hour back instead. The steady-state tests assert the condition is unchanged rather than the object's ResourceVersion. Status content is byte-identical across reconciles but the ResourceVersion still advances, because other writers in Reconcile issue unconditional no-op status patches. That's pre-existing and felt out of scope here. No CRD schema change in any of this, condition reasons are plain string constants, so there's no missing generated output to look for. On the sign-off trailer: the three follow-up commits are doc regeneration and still don't have it. Say the word if that blocks and I'll rewrite them. |
An embedded auth server could not complete an authorization-code exchange with an OAuth2 or OIDC provider whose certificate is signed by a private CA, so an in-cluster IdP behind an internal PKI was unusable as an upstream. Add caBundleRef to the OIDC and OAuth2 upstream configs. The operator projects the selected ConfigMap key as a read-only ca.crt per upstream and passes its path to the provider and DCR clients, so one upstream's private CA is not implicitly trusted for another or for unrelated traffic. Such an upstream may present either a publicly trusted certificate or a private one, so these clients need the system roots as well as the bundle. Add WithSystemRootsPlusCABundle for that and leave WithCABundle pinning: its other callers include the JWKS and introspection clients that validate incoming tokens, where a bundle is often configured precisely to restrict trust to a private issuer. The bundle is mounted with subPath, which kubelet never refreshes, so a rotated CA reaches a running pod only through a pod template change. Hash the selected bytes into a pod template annotation and compare it during drift detection on MCPServer, MCPRemoteProxy and VirtualMCPServer, including when caBundleRef is removed. Resolve and PEM-validate each reference during reconciliation. A missing ConfigMap, absent key, or malformed certificate surfaces as a status condition rather than a pod that starts without the trust roots its upstream requires. Fixes #6417 Signed-off-by: Jakub Hrozek <jakub@stacklok.com>
Signed-off-by: Jakub Hrozek <jakub@stacklok.com>
The new CAFilePath fields on the authserver upstream run-configs are part of the generated API surface, so the committed spec no longer matched what swag produces and the docgen check failed. Regenerating also drops the package qualification from 21 schema keys (authserver, tokenexchange, ratelimit/types, audit, operator v1beta1). swag qualifies a key only when it sees the same package name twice, and the added fields shift which packages it double-counts. No API change -- the renames and their $ref updates account for nearly all of the diff. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dedupe-enums workaround only recognized an exactly doubled enum array, but swag's repeat count varies by machine -- this branch's docs were generated on one that tripled them, so the arrays survived untouched and the docgen check still failed against CI's deduped output. Match any whole-number repeat instead of only 2x, and collapse the three affected arrays in the generated spec. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CABundleRef and ConfigMapRef doc comments were reworded when the embedded auth CA key requirement was clarified, but crd-api.md is generated by a separate task and still carried the old text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ValidateCABundleSource caps ConfigMap names at 48 characters because OIDC CA bundle volumes are named oidc-ca-bundle-<configmap-name> and must fit a 63-character RFC 1123 label. Embedded auth server upstream bundles use volumes named by provider index, so the ConfigMap name never reaches a volume name and the cap terminally rejected otherwise-valid names. Add ValidateCABundleSourceShape for the callers that do not embed the name, and point ResolveCABundle at it. The OIDC call sites keep the cap.
CA bundle failures reused ConditionReasonInvalidConfig, which also marks spec-derived failures from handleInvalidEmbeddedAuthServerConfig. Two guards hold such a failure steady across reconciles by comparing generation and the referenced config's spec hash. Neither covers ConfigMap content, so a CA failure recorded under that reason can never be cleared once the ConfigMap is repaired. Add ConditionReasonInvalidCABundle for the content-derived failure so the guards keep protecting only what they were written for. Widen the mirror's ownership predicate to recognise the new reason as well, otherwise mirrorInvalidOn* removes the condition a step before the CA check re-adds it, restamping LastTransitionTime on every reconcile.
The condition was only ever removed or set False: MCPServer had no True writer and no success reason, unlike its MCPRemoteProxy twin. A recorded failure therefore outlived its cause, since nothing restored the condition once validation started passing again. Add the success reason and writer, mirroring setMCPRemoteProxyExternalAuthConfigValidCondition. The guard that preserves a terminal RunConfig failure is carried over unchanged, but deliberately does not cover CA bundle failures, whose input is ConfigMap content that neither generation nor the config hash tracks. Three mirror subtests asserted the condition stays absent when the source is valid, which only held because no writer existed. They now expect the validated state that MCPRemoteProxy already reports.
handleAuthServerRef reported the reference as valid without ever resolving the CA bundles it names. A malformed bundle was first noticed during Deployment construction, which reports a generic build failure and requeues forever while AuthServerRefValidated still reads True. Resolve the bundles where the sibling externalAuthConfigRef path already does, and record a failure on AuthServerRefValidated. Terminal errors are routed there by passing the condition type into handleInvalidUpstreamCABundle, which previously hardcoded the externalAuthConfigRef condition. MCPRemoteProxy signals the handled case to Reconcile with a sentinel, since the unwrap at its validateAndHandleConfigs boundary cannot tell the two reference paths apart. Classify terminal against transient in all four validation sites, including the two pre-existing externalAuthConfigRef ones: only a content error is recorded as a spec defect, while a failed ConfigMap read surfaces so the caller requeues.
validateAuthServerConfigCABundles recorded every failure as a spec error and runAuthValidations then discarded the error entirely, returning a bare bool. Reconcile therefore stopped with no requeue on a failed ConfigMap read, so an apiserver blip halted reconciliation until an unrelated watch event arrived, under a status that blamed the spec. Classify with the InvalidCABundleError the util layer already returns: stamp and stop for malformed content, propagate untouched for a failed read. runAuthValidations gains the (bool, error) contract runValidations already documents and its other branches already use. Apply the same classification where the deployment path reads the CA bundle checksum. Validation gates that path, but returning a terminal error raw would requeue forever if it were ever reached first.
The example omitted configMapRef.key and noted that it defaults to ca.crt. The default applies to objects already stored, but the generated CRD marks key as required, so admission rejects the example as written.
Rebasing onto main conflicted in the generated OpenAPI output, since main gained the private_key_jwt DCR schema in the same files. The conflict was resolved by regenerating rather than hand-merging.
8e95962 to
16995a9
Compare
samuv
left a comment
There was a problem hiding this comment.
Re-reviewed after the rebase. The earlier reconciliation blockers are addressed: authServerRef validates CA bundles before marking Valid, terminal CA failures use InvalidCABundle on the correct condition, ConfigMap repair restores Valid, and VirtualMCPServer requeues transient Get errors. The 48-character name cap no longer applies to embedded-auth bundles, and the example includes key: ca.crt.
Checklist
- Tests: terminal vs transient, authServerRef, and invalid→valid recovery coverage is in place.
- Docs: Swagger/CRD docs regenerated; example is admission-valid.
- Registry: no impact.
- Security: additive system-root-plus-bundle trust; hostname verification retained.
- Backwards compatibility: additive
caBundleRef; no v1beta1 break.
Adding the terminal CA bundle unwrap to validateAndHandleConfigs pushed it to cyclomatic complexity 16, one over the limit. Move the branch into its own method rather than raising the threshold.
The operator CRD's OIDC upstream config gained allowPrivateIPs in stacklok#6428, which also wired it through buildOIDCUpstreamRunConfig, so the capability this branch originally added now exists upstream. stacklok#6428's own tests cover only the OAuth2 side (TestBuildOAuth2UpstreamRunConfig_TransportOptions), leaving the OIDC-side mapping without direct regression coverage. Add a default-false assertion on the existing OIDC-upstream test case and an end-to-end test that exercises both OIDCUpstreamConfig.AllowPrivateIPs and OAuth2UpstreamConfig.AllowPrivateIPs reaching their run configs through BuildAuthServerRunConfig. This closes the coverage half of the stacklok#4523 ask; the field itself shipped via stacklok#6428. Signed-off-by: Aleksandr Filippov <71711753+alex-feel@users.noreply.github.com>
…#6288) The operator CRD's OIDC upstream config gained allowPrivateIPs in #6428, which also wired it through buildOIDCUpstreamRunConfig, so the capability this branch originally added now exists upstream. #6428's own tests cover only the OAuth2 side (TestBuildOAuth2UpstreamRunConfig_TransportOptions), leaving the OIDC-side mapping without direct regression coverage. Add a default-false assertion on the existing OIDC-upstream test case and an end-to-end test that exercises both OIDCUpstreamConfig.AllowPrivateIPs and OAuth2UpstreamConfig.AllowPrivateIPs reaching their run configs through BuildAuthServerRunConfig. This closes the coverage half of the #4523 ask; the field itself shipped via #6428. Signed-off-by: Aleksandr Filippov <71711753+alex-feel@users.noreply.github.com>
Summary
caBundleRefsupport, PEM validation, read-only ConfigMap projection, and additive system-root-plus-private-CA TLS trust for embedded auth-server clients.Fixes #6417
Type of change
Test plan
task test)task test-e2e)task lint-fix)task operator-test, including CA rotation/removal and terminal invalid-bundle status handling)API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Changes
cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go, generated CRDscaBundleRefAPI documentation and generated schema updates.cmd/thv-operator/pkg/controllerutil/{authserver,ca_bundle}.gocmd/thv-operator/controllers/{mcpserver,mcpremoteproxy,virtualmcpserver}_*pkg/networking/http_client.gopkg/authserver/**,pkg/auth/dcr/**docs/arch/**,docs/operator/crd-api.md, example manifest*_test.gofilesDoes this introduce a user-facing change?
Yes. Operators can configure
caBundleRefon embedded-auth OIDC and OAuth2 upstreams to trust an internal CA. The bundle augments system trust roots; it does not pin the upstream to that CA or disable publicly trusted certificates.Special notes for reviewers
This is one vertical feature spanning CRD API, operator projection and rollout behavior, and runtime TLS clients. Please pay particular attention to the intentional additive trust semantics and to the checksum-driven rollout required because
subPathConfigMap mounts do not refresh in running pods.