Skip to main content

Kubernetes pods/exec

· 8 min read

Kubernetes pods/exec #

pods/exec is a convenient way to execute commands inside a container for debugging and administration. But here's a challenge right off the bat: how safe do you think the following Kubernetes role is — one that could be granted to any user, assuming the absurd scenario that the Secret resource is not used in the cluster?

pods/exec audit
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: test-role
rules:
- apiGroups: [""]
resources: ["*"]
verbs: ["get"]

If you answered "safe," you're not alone — and you would have granted pods/exec access to any user in the cluster, provided you also created a ClusterRoleBinding on top of it.

Table of Contents

What changed

In reality — nothing. The issue has been around for over 7 years, yet few people are aware of it. The problem is that a widespread belief has taken root among knowledgeable engineers that access to pods/exec can be granted with a simple rule like this:

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: test-role
rules:
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]

In principle, you would be right, but as practice has shown — that's not the whole story =)

The first mention of the issue was found with the help of the folks from ever_secure:

kubernetes RBAC role verbs to exec to pod

I had to add the verb "get" to my pods/exec section as well since the client library I'm using is doing an http GET to negotiate a websocket first. Using kubectl it sends an http POST and requires the "create" verb in that case. It may be worth updating this example to include the "get" verb.

So, as early as 7 years ago there was a warning sign: kubectl exec can work not only via POST, but also via GET.

This was definitively confirmed after kubectl transitioned to WebSocket in version 1.31, where kubectl exec now uses GET to establish the connection instead of POST by default.

Source: Streaming Transitions from SPDY to WebSockets

Impact on kubectl and RBAC

To understand why get is now required, you need to understand how HTTP methods map to Kubernetes verbs:

HTTP methodKubernetes verb
GETget
POSTcreate
PUTupdate
DELETEdelete

Some clients use GET to establish a WebSocket connection with pods/exec rather than POST, as kubectl used to. Therefore, for exec to work, not only create but also get may be required in RBAC.

Testing on Minikube

Cluster and contexts

minikube -p 13201 start --kubernetes-version=1.32.1 --ssh-port=13201
kubectl get nodes
NAME     STATUS   ROLES           AGE   VERSION
13201 Ready control-plane 60s v1.32.1

cluster-admin and anonymous

export KUBECONFIG=cluster-admin
minikube -p 13201 update-context
export KUBECONFIG=anonymous-view
minikube -p 13201 update-context
sed -i '/^[[:space:]]*users:/,$d' anonymous-view

Pod and exec verification

kubectl \
--kubeconfig=cluster-admin \
apply -f - <<EOF
---
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
EOF
kubectl --kubeconfig=cluster-admin get po
NAME    READY   STATUS    RESTARTS   AGE
nginx 1/1 Running 0 23s

exec: cluster-admin vs anonymous

kubectl --kubeconfig=cluster-admin exec -it nginx -- sh
# ls -al
total 76
drwxr-xr-x 1 root root 4096 Aug 18 15:09 .
drwxr-xr-x 1 root root 4096 Aug 18 15:09 ..
kubectl --kubeconfig=anonymous-view exec -it nginx -- sh
Error from server (Forbidden): pods "nginx" is forbidden: User "system:anonymous" cannot get resource "pods" in API group "" in the namespace "default"

Adding get access only

note

Please note that along with pods/exec, access to pods is also added, otherwise exec will not work.

kubectl \
--kubeconfig=cluster-admin \
apply -f - <<EOF
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: anonymous-view
rules:
- apiGroups: [""]
resources:
- pods
- pods/exec
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: anonymous-view
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: anonymous-view
subjects:
- kind: User
name: system:anonymous
apiGroup: rbac.authorization.k8s.io
EOF
clusterrole.rbac.authorization.k8s.io/anonymous-view created
clusterrolebinding.rbac.authorization.k8s.io/anonymous-view created
kubectl --kubeconfig=anonymous-view exec -it nginx -- sh
# ls -la
total 76
drwxr-xr-x 1 root root 4096 Aug 18 15:09 .
drwxr-xr-x 1 root root 4096 Aug 18 15:09 ..
-rwxr-xr-x 1 root root 0 Aug 18 15:09 .dockerenv
drwxr-xr-x 2 root root 4096 Mar 26 2019 bin
drwxr-xr-x 2 root root 4096 Feb 3 2019 boot
drwxr-xr-x 5 root root 360 Aug 18 15:09 dev

Demonstration (kubectl 1.33.3 vs 1.29.0)

kubectl v1.33.3

kubectl version
Client Version: v1.33.3
kubectl exec -it nginx -- sh
****** verb="GET" url="/api/v1/namespaces/default/pods/incloud-web-incloud-web-chart-d99877d74-27tcd/exec?command=sh&container=nginx&stdin=true&stdout=true&tty=true" ******
/ $ ls -al
total 76
drwxr-xr-x 1 root root 4096 Aug 16 20:43 .
drwxr-xr-x 1 root root 4096 Aug 16 20:43 ..
drwxr-xr-x 2 root root 4096 Jul 15 10:42 bin
drwxr-xr-x 5 root root 360 Aug 16 20:43 dev

kubectl v1.29.0

kubectl version
Client Version: v1.29.0
kubectl exec -it nginx -- sh
****** POST https://api/v1/namespaces/default/pods/incloud-web-incloud-web-chart-d99877d74-27tcd/exec?command=sh&container=nginx&stdin=true&stdout=true&tty=true ******
Error from server (Forbidden): pods "nginx" is forbidden:
User "system:anonymous" cannot get resource "pods" in API group "" in the namespace "default"
Warning!

In this example, we wanted to demonstrate the behavior of kubectl exec across different client versions. With the same cluster and role configuration. As you can see, the behavior differs, and depending on the client version, a different RBAC configuration may be required.


Kubernetes Audit

{
"annotations": {
"authorization.k8s.io/decision": "allow",
"authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding \"anonymous-view-all\" of ClusterRole \"anonymous-view\" to User \"system:anonymous\""
},
"apiVersion": "audit.k8s.io/v1",
"auditID": "142e967e-4958-4a77-b691-2f71e26ba6ec",
"kind": "Event",
"level": "Request",
"objectRef": {
"apiVersion": "v1",
"name": "nginx",
"namespace": "default",
"resource": "pods",
"subresource": "exec"
},
"requestURI": "/api/v1/namespaces/default/pods/nginx/exec?...",
"responseStatus": {
"code": 101,
"metadata": {}
},
"sourceIPs": ["10.0.0.33"],
"stage": "ResponseComplete",
"stageTimestamp": "2025-08-15T16:46:50.296700Z",
"user": {
"groups": ["system:unauthenticated"],
"username": "system:anonymous"
},
"userAgent": "Mozilla/5.0 (...) Safari/537.36",
"verb": "get"
}

Conclusions and recommendations

Unfortunately, you can't know everything, but you can try to minimize the risks.

Review your RBAC — don't blindly trust roles with * in resources and verbs.

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: test-role
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create", "get"]
Dangerous pattern

The best policies are explicit. Do not use * in resources or verbs. This will save you from unexpected surprises.