diff --git a/pkg/yurthub/proxy/util/util.go b/pkg/yurthub/proxy/util/util.go index 5dc809bb3f1..039a23cb3cd 100644 --- a/pkg/yurthub/proxy/util/util.go +++ b/pkg/yurthub/proxy/util/util.go @@ -36,6 +36,7 @@ import ( apirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/klog/v2" + "github.com/openyurtio/openyurt/pkg/projectinfo" "github.com/openyurtio/openyurt/pkg/yurthub/filter" "github.com/openyurtio/openyurt/pkg/yurthub/metrics" "github.com/openyurtio/openyurt/pkg/yurthub/tenant" @@ -457,3 +458,33 @@ func IsSubjectAccessReviewCreateGetRequest(req *http.Request) bool { info.Resource == "subjectaccessreviews" && (info.Verb == "create" || info.Verb == "get") } + +// WithRequireAuthorization rejects any request that does not carry an Authorization +// header with a 401 Unauthorized response, unless the request originates from +// YurtHub itself (identified by its well-known User-Agent prefixes). +// +// This middleware MUST be applied to the insecure (plain-HTTP) proxy port. +// Without it, RemoteProxy.RoundTrip forwards credential-free requests using +// YurtHub's own node client certificate, effectively lending the node identity +// (system:node:) to any pod that can reach the dummy interface IP +// (169.254.2.1:10261). See: https://github.com/openyurtio/openyurt/issues/2782 +func WithRequireAuthorization(handler http.Handler, nodeName string) http.Handler { + multiplexerUserAgent := util.MultiplexerProxyClientUserAgentPrefix + nodeName + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // Allow requests from YurtHub itself: the internal multiplexer client + // and the shared-informer clients both omit Authorization headers but + // carry well-known User-Agent values. They must not be blocked. + ua := req.UserAgent() + if ua == multiplexerUserAgent || strings.HasPrefix(ua, projectinfo.GetHubName()) { + handler.ServeHTTP(w, req) + return + } + + if strings.TrimSpace(req.Header.Get("Authorization")) == "" { + klog.V(2).Infof("rejected unauthenticated request %s: missing Authorization header on insecure proxy port", util.ReqString(req)) + http.Error(w, "Unauthorized: requests to the YurtHub proxy must include an Authorization header", http.StatusUnauthorized) + return + } + handler.ServeHTTP(w, req) + }) +} diff --git a/pkg/yurthub/server/server.go b/pkg/yurthub/server/server.go index 2c59936b2f2..484bac87496 100644 --- a/pkg/yurthub/server/server.go +++ b/pkg/yurthub/server/server.go @@ -33,6 +33,7 @@ import ( "github.com/openyurtio/openyurt/pkg/yurthub/healthchecker" ota "github.com/openyurtio/openyurt/pkg/yurthub/otaupdate" otautil "github.com/openyurtio/openyurt/pkg/yurthub/otaupdate/util" + proxyutil "github.com/openyurtio/openyurt/pkg/yurthub/proxy/util" ) // RunYurtHubServers is used to start up all servers for yurthub @@ -53,13 +54,12 @@ func RunYurtHubServers(cfg *config.YurtHubConfiguration, // start yurthub proxy servers for forwarding requests to cloud kube-apiserver if cfg.YurtHubProxyServerServing != nil { - if err := cfg.YurtHubProxyServerServing.Serve(proxyHandler, 0, stopCh); err != nil { - return err - } - } - - if cfg.YurtHubDummyProxyServerServing != nil { - if err := cfg.YurtHubDummyProxyServerServing.Serve(proxyHandler, 0, stopCh); err != nil { + // Wrap the proxy handler so that requests arriving on the plain-HTTP port + // without an Authorization header are rejected with 401 instead of being + // forwarded using YurtHub's own node client certificate. + // See: https://github.com/openyurtio/openyurt/issues/2782 + insecureProxyHandler := proxyutil.WithRequireAuthorization(proxyHandler, cfg.NodeName) + if err := cfg.YurtHubProxyServerServing.Serve(insecureProxyHandler, 0, stopCh); err != nil { return err } }