An authorization error from kubectl exec often looks illogical:
cannot create resource "pods/exec"
The Pod already exists. The command does not create another Pod or modify its specification. get or update might therefore seem like the expected permission, but Kubernetes checks for create.
The reason is that RBAC describes Kubernetes API operations, not the literal meaning of kubectl commands.
exec is a separate subresource
kubectl exec does not operate directly on the pods resource. It calls the pods/exec subresource:
/api/v1/namespaces/<namespace>/pods/<pod>/exec
RBAC treats a resource and its subresources separately:
resources:
- pods
- pods/log
- pods/exec
Permission to get pods does not grant access to pods/exec. These are different API endpoints with different operations and risks.
get pods allows a client to retrieve a Pod. get pods/log allows it to read logs. create pods/exec allows it to start a new command execution session inside a container.
Where create comes from
Kubernetes maps HTTP methods to authorization verbs:
| HTTP method | Kubernetes verb |
|---|---|
GET | get, list, or watch |
POST | create |
PUT | update |
PATCH | patch |
DELETE | delete or deletecollection |
Historically, kubectl exec opened its streaming connection over SPDY. The initial request to pods/exec used HTTP POST, so the API server converted it into this authorization check:
create pods/exec
In this context, create does not mean creating a new Pod or another persistent Kubernetes resource. It authorizes a new operation through the subresource.
This distinction matters. An RBAC verb describes how the API is invoked. It does not guarantee that the operation stores a new object in etcd.
What changed with WebSockets
Since Kubernetes 1.31, kubectl has used WebSockets by default instead of SPDY for exec, attach, cp, and port-forward.
A WebSocket handshake starts with HTTP GET:
GET /api/v1/namespaces/<namespace>/pods/<pod>/exec
Connection: Upgrade
Upgrade: websocket
The old explanation based only on the POST to create mapping therefore became incomplete. During the transition, a WebSocket request to pods/exec could be authorized as get, while the same operation over SPDY required create.
The authorization requirement depended on the transport protocol even though the requested operation was the same.
Kubernetes 1.35 introduced the default-enabled AuthorizePodWebsocketUpgradeCreatePermission feature gate. For WebSocket connections, the API server now performs an additional create authorization check for these subresources:
pods/execpods/attachpods/portforward
This restored one rule across both transports. Changing the streaming protocol must not turn an active operation into a read permission.
A practical Role
A typical Role for an operator who can inspect Pods and execute commands when required looks like this:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-debugger
namespace: example
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- pods/log
verbs:
- get
- apiGroups:
- ""
resources:
- pods/exec
verbs:
- create
The permissions are separated deliberately.
Reading Pods and logs does not allow command execution inside a container. Access to exec can be granted only to users and ServiceAccounts that actually need it.
This separation also matters because Kubernetes RBAC cannot restrict which commands are run after an exec session is established. A user with access to pods/exec can execute anything permitted to the container process.
Checking the permission
The subresource permission can be checked without executing a command:
kubectl auth can-i create pods \
--subresource=exec \
--namespace=example
Access to the Pod itself should be checked separately:
kubectl auth can-i get pods \
--namespace=example
Use --as to check another user or ServiceAccount:
kubectl auth can-i create pods \
--subresource=exec \
--namespace=example \
--as=system:serviceaccount:example:debugger
A yes result for get pods says nothing about access to pods/exec. The resource, subresource, and verb must be checked together.
Conclusion
kubectl exec requires create because it calls the active pods/exec subresource and starts a new command execution session, not because it creates another Pod.
Historically, the permission followed directly from the HTTP POST request used by SPDY. After WebSockets introduced a GET handshake, Kubernetes retained the same authorization model through an additional check.
RBAC verbs should therefore not be inferred from a command name or from whether an object appears to change. A more reliable approach is to identify the resource, the subresource, and the operation that the API server authorizes for the request.
