fix: correct RBAC verb for pods/exec from get to create (#2037)

fix: correct RBAC verb and WebSocket fallback for pods/exec

This commit fixes three related issues that prevented exec collectors from
working with minimal RBAC permissions:

1. RBAC preflight check used wrong verb for pods/exec
   Changed from "get" to "create" in v1beta1 and v1beta2 AccessReviewSpecs.
   The pods/exec subresource requires "create" to execute commands.

2. WebSocket fallback used wrong httpstream package import
   The fallback executor checked IsUpgradeFailure using the apimachinery
   httpstream package, but the roundtripper creates UpgradeFailureError using
   the streaming httpstream package. These are different Go types, so
   errors.As always returned false and fallback to SPDY never triggered.
   Changed import to k8s.io/streaming/pkg/httpstream.

3. Stdin mismatch caused SPDY fallback to hang
   PodExecOptions always set Stdin:true but StreamOptions always passed
   Stdin:nil. When WebSocket failed and fell back to SPDY, the server
   waited for stdin data that never arrived. Changed Stdin to false in
   PodExecOptions for exec, copy, and copy_from_host collectors.
This commit is contained in:
Ethan Mosbaugh
2026-05-01 13:03:42 -07:00
committed by GitHub
parent a4ce199005
commit 4c6af55e7c
7 changed files with 20 additions and 12 deletions
+1 -1
View File
@@ -57,6 +57,7 @@ require (
k8s.io/cli-runtime v0.36.0
k8s.io/client-go v0.36.0
k8s.io/klog/v2 v2.140.0
k8s.io/streaming v0.36.0
oras.land/oras-go/v2 v2.6.0
sigs.k8s.io/controller-runtime v0.23.3
sigs.k8s.io/e2e-framework v0.7.0
@@ -164,7 +165,6 @@ require (
gotest.tools/v3 v3.5.2 // indirect
k8s.io/component-base v0.36.0 // indirect
k8s.io/kubectl v0.36.0 // indirect
k8s.io/streaming v0.36.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
)
@@ -261,7 +261,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: pickNamespaceOrDefault(c.Exec.Namespace, overrideNS),
Verb: "get",
Verb: "create",
Group: "",
Version: "",
Resource: "pods",
@@ -286,7 +286,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: pickNamespaceOrDefault(c.Copy.Namespace, overrideNS),
Verb: "get",
Verb: "create",
Group: "",
Version: "",
Resource: "pods",
@@ -529,7 +529,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: pickNamespaceOrDefault(c.Exec.Namespace, overrideNS),
Verb: "get",
Verb: "create",
Group: "",
Version: "",
Resource: "pods",
@@ -554,7 +554,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: pickNamespaceOrDefault(c.Copy.Namespace, overrideNS),
Verb: "get",
Verb: "create",
Group: "",
Version: "",
Resource: "pods",
+4 -1
View File
@@ -98,11 +98,14 @@ func copyFilesFromPod(ctx context.Context, dstPath string, clientConfig *restcli
return nil, nil, errors.Wrap(err, "failed to add runtime scheme")
}
// Stdin must be false because StreamOptions.Stdin is nil below.
// A mismatch causes the SPDY fallback (after WebSocket fails on RBAC)
// to hang: the API server opens a stdin stream but never receives EOF.
parameterCodec := runtime.NewParameterCodec(scheme)
req.VersionedParams(&corev1.PodExecOptions{
Command: command,
Container: containerName,
Stdin: true,
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
+4 -1
View File
@@ -299,11 +299,14 @@ func copyFilesFromHost(ctx context.Context, dstPath string, clientConfig *restcl
return nil, nil, errors.Wrap(err, "failed to add runtime scheme")
}
// Stdin must be false because StreamOptions.Stdin is nil below.
// A mismatch causes the SPDY fallback (after WebSocket fails on RBAC)
// to hang: the API server opens a stdin stream but never receives EOF.
parameterCodec := runtime.NewParameterCodec(scheme)
req.VersionedParams(&corev1.PodExecOptions{
Command: command,
Container: containerName,
Stdin: true,
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
+4 -1
View File
@@ -129,10 +129,13 @@ func getExecOutputs(
}
parameterCodec := runtime.NewParameterCodec(scheme)
// Stdin must be false because StreamOptions.Stdin is nil below.
// A mismatch causes the SPDY fallback (after WebSocket fails on RBAC)
// to hang: the API server opens a stdin stream but never receives EOF.
req.VersionedParams(&corev1.PodExecOptions{
Command: append(execCollector.Command, execCollector.Args...),
Container: container,
Stdin: true,
Stdin: false,
Stdout: true,
Stderr: true,
TTY: false,
+3 -4
View File
@@ -3,9 +3,9 @@ package k8sutil
import (
"net/url"
"k8s.io/apimachinery/pkg/util/httpstream"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/streaming/pkg/httpstream"
)
// NewFallbackExecutor creates an executor that tries WebSocket first and falls
@@ -21,8 +21,7 @@ func NewFallbackExecutor(config *restclient.Config, u *url.URL) (remotecommand.E
if err != nil {
return nil, err
}
shouldFallback := func(err error) bool {
return remotecommand.NewFallbackExecutor(wsExec, spdyExec, func(err error) bool {
return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
}
return remotecommand.NewFallbackExecutor(wsExec, spdyExec, shouldFallback)
})
}