HTTP Method Restriction / Access Control (read-only endpoints)

HIGH
kubernetes/kubernetes
Commit: 7f605824a074
Affected: v1.36.0-beta.0 and earlier (1.36.x releases prior to this patch)
2026-05-26 18:24 UTC

Description

This commit adds explicit HTTP method restrictions on kubelet endpoints to harden access control for read-only paths. Specifically: - kubelet_server_journal.go now rejects methods other than GET and POST for journal-related queries, returning 405 Method Not Allowed and an Allow header listing GET and POST. - Additional read-only endpoints registered via the RESTful server are wrapped with a GET-only filter (GETOnlyRestfulFilter) so that only GET requests are processed; non-GET requests receive 405 with an Allow header indicating the permitted methods. The change reduces the attack surface by preventing abuse of unsupported HTTP methods (e.g., PUT, DELETE, PATCH) against kubelet read endpoints that could otherwise trigger unintended behavior or side effects. Tests were added to verify the method-gating behavior for journal endpoints and other read-only endpoints. Overall this is a security hardening fix rather than a dependency or cleanup only change.

Proof of Concept

PoC (proof of concept) to reproduce the method-restriction behavior: - Prerequisites: Access to a kubelet API endpoint running with the patched code (v1.36.0-beta.0 or later that includes the fix). - Target endpoints (as covered by tests): /logs/, /pods/, /containerLogs/default/mypod/mycontainer, /runningpods/, /debug/pprof/profile?seconds=1 1) Using curl (example for a single endpoint): curl -i -X POST http://<kubelet-host>:10250/logs/ Expected output: HTTP/1.1 405 Method Not Allowed Allow: GET, POST 2) Programmatic proof (Python): import requests host = "http://<kubelet-host>:10250" endpoints = ["/logs/", "/pods/", "/containerLogs/default/mypod/mycontainer", "/runningpods/", "/debug/pprof/profile?seconds=1"] for ep in endpoints: r = requests.post(host + ep) print(ep, r.status_code, r.headers.get("Allow")) Expected result for all endpoints is 405 with Allow header listing GET (and POST for journal if applicable per endpoint).

Commit Details

Author: Kubernetes Prow Robot

Date: 2026-04-22 23:41 UTC

Message:

Merge pull request #138088 from amritansh1502/fix-137503-nodelogquery-post kubelet: enforce explicit HTTP method restrictions for logs-related endpoints

Triage Assessment

Vulnerability Type: HTTP method restriction / access control hardening

Confidence: HIGH

Reasoning:

The commit enforces explicit HTTP method restrictions on kubelet endpoints (logs-related and other read-only endpoints) by rejecting non-GET/POST methods and returning 405 with an Allow header. This reduces attack surface by preventing unintended methods from triggering endpoints, addressing potential abuse or bypasses via unsupported HTTP methods. Tests were added to verify the behavior.

Verification Assessment

Vulnerability Type: HTTP Method Restriction / Access Control (read-only endpoints)

Confidence: HIGH

Affected Versions: v1.36.0-beta.0 and earlier (1.36.x releases prior to this patch)

Code Diff

diff --git a/pkg/kubelet/kubelet_server_journal.go b/pkg/kubelet/kubelet_server_journal.go index d85817a042a33..201edeefdea00 100644 --- a/pkg/kubelet/kubelet_server_journal.go +++ b/pkg/kubelet/kubelet_server_journal.go @@ -63,6 +63,13 @@ type journalServer struct{} // to journalctl on the current system. It supports content-encoding of // gzip to reduce total content size. func (journalServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req.Method != http.MethodGet && req.Method != http.MethodPost { + // Only GET and POST are supported for journal log queries. + w.Header().Set("Allow", "GET, POST") + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + var out io.Writer = w nlq, errs := newNodeLogQuery(req.URL.Query()) diff --git a/pkg/kubelet/kubelet_server_journal_test.go b/pkg/kubelet/kubelet_server_journal_test.go index bf42f685f453a..03705f7b435f7 100644 --- a/pkg/kubelet/kubelet_server_journal_test.go +++ b/pkg/kubelet/kubelet_server_journal_test.go @@ -19,6 +19,8 @@ package kubelet import ( "bytes" "context" + "net/http" + "net/http/httptest" "net/url" "os" "path/filepath" @@ -34,6 +36,48 @@ import ( "k8s.io/utils/ptr" ) +func TestJournalServerMethodRestriction(t *testing.T) { + tests := []struct { + name string + method string + expectedStatus int + expectedAllow string + }{ + { + name: "GET is allowed by method gate", + method: http.MethodGet, + expectedStatus: http.StatusBadRequest, + }, + { + name: "POST is allowed by method gate", + method: http.MethodPost, + expectedStatus: http.StatusBadRequest, + }, + { + name: "PUT is rejected", + method: http.MethodPut, + expectedStatus: http.StatusMethodNotAllowed, + expectedAllow: "GET, POST", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // invalid sinceTime forces an early validation error (400), so we only + // validate method gating and avoid invoking external log commands. + req := httptest.NewRequest(tt.method, "/logs?sinceTime=invalid", nil) + recorder := httptest.NewRecorder() + + journal.ServeHTTP(recorder, req) + + assert.Equal(t, tt.expectedStatus, recorder.Code) + if tt.expectedAllow != "" { + assert.Equal(t, tt.expectedAllow, recorder.Header().Get("Allow")) + } + }) + } +} + func Test_getLoggingCmd(t *testing.T) { var emptyCmdEnv []string tests := []struct { diff --git a/pkg/kubelet/server/server.go b/pkg/kubelet/server/server.go index fe96a4ab366ef..1d9b77a057564 100644 --- a/pkg/kubelet/server/server.go +++ b/pkg/kubelet/server/server.go @@ -482,6 +482,7 @@ func (s *Server) InstallAuthNotRequiredHandlers(ctx context.Context) { s.addMetricsBucketMatcher("pods") ws := new(restful.WebService) + ws.Filter(GETOnlyRestfulFilter()) ws. Path(podsPath). Produces(restful.MIME_JSON) @@ -636,6 +637,7 @@ func (s *Server) InstallAuthRequiredHandlers(ctx context.Context) { s.addMetricsBucketMatcher("containerLogs") ws = new(restful.WebService) + ws.Filter(GETOnlyRestfulFilter()) ws. Path("/containerLogs") ws.Route(ws.GET("/{podNamespace}/{podID}/{containerName}"). @@ -649,6 +651,7 @@ func (s *Server) InstallAuthRequiredHandlers(ctx context.Context) { // The /runningpods endpoint is used for testing only. s.addMetricsBucketMatcher("runningpods") ws = new(restful.WebService) + ws.Filter(GETOnlyRestfulFilter()) ws. Path(runningPodsPath). Produces(restful.MIME_JSON) @@ -699,6 +702,7 @@ func (s *Server) InstallSystemLogHandler(enableSystemLogHandler bool, enableSyst s.addMetricsBucketMatcher("logs") if enableSystemLogHandler { ws := new(restful.WebService) + ws.Filter(GETOnlyRestfulFilter()) ws.Path(logsPath) ws.Route(ws.GET(""). To(s.getLogs). @@ -770,6 +774,7 @@ func (s *Server) InstallProfilingHandler(enableProfilingLogHandler bool, enableC // Setup pprof handlers. ws := new(restful.WebService).Path(pprofBasePath) + ws.Filter(GETOnlyRestfulFilter()) ws.Route(ws.GET("/{subpath:*}").To(handlePprofEndpoint)).Doc("pprof endpoint") s.restfulCont.Add(ws) @@ -933,6 +938,32 @@ func (s *Server) getLogs(request *restful.Request, response *restful.Response) { s.host.ServeLogs(response, request.Request) } +// GETOnlyRestfulFilter allows only GET. Use on WebServices that register read-only +// kubelet APIs. +func GETOnlyRestfulFilter() restful.FilterFunction { + return AllowedMethodsRestfulFilter(http.MethodGet) +} + +// AllowedMethodsRestfulFilter returns a restful.FilterFunction that rejects requests +// whose HTTP method is not listed in allowed. It responds with 405 Method Not Allowed +// and an Allow header listing the permitted methods (RFC 9110). +func AllowedMethodsRestfulFilter(allowed ...string) restful.FilterFunction { + allowedSet := make(map[string]struct{}, len(allowed)) + for _, m := range allowed { + allowedSet[m] = struct{}{} + } + allowHeader := strings.Join(allowed, ", ") + + return func(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) { + if _, ok := allowedSet[req.Request.Method]; ok { + chain.ProcessFilter(req, resp) + return + } + resp.Header().Set("Allow", allowHeader) + _ = resp.WriteErrorString(http.StatusMethodNotAllowed, "Method Not Allowed") + } +} + type execRequestParams struct { podNamespace string podName string diff --git a/pkg/kubelet/server/server_test.go b/pkg/kubelet/server/server_test.go index 1875897f01b0b..b9d1093b52388 100644 --- a/pkg/kubelet/server/server_test.go +++ b/pkg/kubelet/server/server_test.go @@ -421,13 +421,11 @@ func TestServeLogs(t *testing.T) { defer fw.testHTTPServer.Close() content := string(`<pre><a href="kubelet.log">kubelet.log</a><a href="google.log">google.log</a></pre>`) - fw.fakeKubelet.logFunc = func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusOK) w.Header().Add("Content-Type", "text/html") w.Write([]byte(content)) } - resp, err := http.Get(fw.testHTTPServer.URL + "/logs/") if err != nil { t.Fatalf("Got error GETing: %v", err) @@ -443,6 +441,38 @@ func TestServeLogs(t *testing.T) { if !strings.Contains(result, "kubelet.log") || !strings.Contains(result, "google.log") { t.Errorf("Received wrong data: %s", result) } + +} + +func TestGETOnlyEndpointsRejectPostWithAllowHeader(t *testing.T) { + tCtx := ktesting.Init(t) + fw := newServerTest(tCtx) + defer fw.testHTTPServer.Close() + + tests := []struct { + name string + path string + }{ + {name: "pods", path: "/pods/"}, + {name: "containerLogs", path: "/containerLogs/default/mypod/mycontainer"}, + {name: "runningpods", path: "/runningpods/"}, + {name: "logs", path: "/logs/"}, + {name: "pprof", path: "/debug/pprof/profile?seconds=1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, fw.testHTTPServer.URL+tt.path, nil) + require.NoError(t, err) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() //nolint:errcheck + + assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) + assert.Equal(t, http.MethodGet, resp.Header.Get("Allow")) + }) + } } func TestServeRunInContainer(t *testing.T) {
← Back to Alerts View on GitHub →