Allow opt-in private endpoints for remote URLs - #6195
Conversation
ValidateRemoteURL unconditionally rejected private and in-cluster endpoints, blocking the legitimate case of a vMCP reaching a co-located in-cluster backend in-mesh, where routing through the mesh preserves Istio/SPIFFE workload identity for the backend's authorization policy. Add an allowPrivateEndpoint field to MCPServerEntry and MCPRemoteProxy that relaxes only the private-network checks: RFC 1918 ranges, IPv6 unique-local addresses, and hostnames ending in cluster.local. Loopback, link-local, cloud-metadata, localhost, and kubernetes.default endpoints stay blocked regardless of the flag, and behavior with the flag unset is unchanged. Signed-off-by: Prem Kumar Sompura <prem_sompura@hotmail.com>
6b7ffbc to
3e53d21
Compare
amirejaz
left a comment
There was a problem hiding this comment.
Reviewed with a security focus and cross-checked against the carve-out agreed in #5784 — the never-relaxed set, the ordering (kubernetes.default* blocked before the cluster.local relaxation), the IPv4-mapped IPv6 normalization, and the byte-for-byte-unchanged flag-off behavior all check out. Looks good, approving.
One check for @aponcedeleonch before merge: you'd listed .svc as relaxable (blocked unless opted in), but the PR leaves bare .svc allowed in both flag states (documented as never-blocked since no DNS resolution happens). The reasoning is sound — gating it would break the flag-off-unchanged contract and the existing .svc e2e workaround — but are you good with that, or do you want .svc genuinely behind the flag?
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6195 +/- ##
==========================================
- Coverage 72.51% 72.46% -0.06%
==========================================
Files 739 739
Lines 76719 76722 +3
==========================================
- Hits 55634 55595 -39
- Misses 17107 17161 +54
+ Partials 3978 3966 -12 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Summary
ValidateRemoteURLunconditionally rejects private/loopback IPs and internal hostnames (cluster.local,kubernetes.default*,localhost,metadata.google.internal) with no opt-in. That blocks a legitimate topology: a Virtual MCP reaching a co-located in-cluster backend in-mesh, where going through an external address would strip the Istio/SPIFFE workload identity the backend's authorization policy keys on. The design (per-resource opt-in field + exact carve-out below) was agreed with @aponcedeleonch in #5784.allowPrivateEndpoint(bool, defaultfalse) to bothMCPServerEntryandMCPRemoteProxyspecs, and changeValidateRemoteURLto take aValidateRemoteURLOptionscarrying the flag, threaded from both controllers.10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), IPv6 ULA (fc00::/7), and hostnames ending incluster.local(.svcnames were never blocked, since no DNS resolution is performed).169.254.0.0/16,fe80::/10), loopback/unspecified (127.0.0.0/8,::1/128,::/128,0.0.0.0/8),localhost,metadata.google.internal, and allkubernetes.default*variants — includingkubernetes.default.svc.cluster.local, which is checked before thecluster.localrelaxation can apply. IPv4-mapped IPv6 forms are normalized before matching on both the blocked and relaxed sides.Fixes #5784
Type of change
Test plan
task test,task operator-test) — table-driven coverage of every carve-out category with the flag on and off, including the never-relaxed set staying blocked with the flag on and IPv4-mapped IPv6 forms; controller tests for bothMCPServerEntryandMCPRemoteProxyproving the CR field flows through to validation (private URL rejected with flag unset, accepted with flag true)task test-e2e)task lint-fix)API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Additive optional field only (
allowPrivateEndpoint, defaults tofalse).Does this introduce a user-facing change?
Yes.
MCPServerEntryandMCPRemoteProxygain an optionalspec.allowPrivateEndpointfield (defaultfalse). When set, the remote URL may point at a private/in-cluster endpoint (RFC-1918, IPv6 ULA,cluster.local/.svchostnames) — intended for reaching a co-located in-cluster backend in-mesh so workload-identity authorization still applies. Loopback, link-local, cloud-metadata, andkubernetes.default*endpoints remain blocked regardless of the flag.Implementation plan
Approved implementation plan (agreed in #5784)
allowPrivateEndpoint bool(defaultfalse) to bothMCPServerEntryandMCPRemoteProxyCRD specs.ValidateRemoteURL(rawURL string) errorto take an options parameter (rather than the bare string) carrying this flag, and thread it through from bothmcpserverentry_controller.goandmcpremoteproxy_controller.go.allowPrivateEndpoint: true: RFC-1918 (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16), IPv6 ULA (fc00::/7), hostnames ending incluster.localor.svc.169.254.0.0/16,fe80::/10), loopback/unspecified (127.0.0.0/8,::1/128,0.0.0.0/8),localhost,metadata.google.internal,kubernetes.default*. Loopback staying blocked is explicitly the point that matters most — a pod reaching loopback hits itself, which is never a legitimate co-located backend.Special notes for reviewers
kubernetes.default.svc.cluster.local) are evaluated before thecluster.localrelaxation, so opting in cannot expose the API server's DNS names.kubernetes.default*hostname block cannot protect the API server from being addressed by its ClusterIP directly (e.g.10.96.0.1is an RFC-1918 address the flag allows). The hostname block should not be read as API-server protection once a resource opts in — the flag is a deliberate, per-resource trust statement about the private network..svc-suffixed hostnames (other thankubernetes.default.svc) were already allowed before this change (the validator does no DNS resolution), so the flag does not change their handling; the "relaxable" list documents intent rather than adding a new block.zz_generated.deepcopy.gois unchanged — a plain bool needs no generated deepcopy code. CRD YAML anddocs/operator/crd-api.mdare regenerated.test/e2e/thv-operator/virtualmcp/mcpremoteproxy_scaling_test.goclaimingMCPRemoteProxy"never" targets in-cluster servers is updated (comment-only).Generated with Claude Code