Kubernetes: User Tokens with Python

Introduction

Recently I’ve been experimenting with creating users with the Kubernetes API and have even created a module called kubernetes-user. While the subject of Kubernetes users is a complicated one, this Python module explores different methods to authenticate with the Kubernetes API.

One such method for authenticating with the API is through Kubernetes Service Account (SA) tokens. Basically, Kubernetes automatically associates a secret token with each Service Account. These tokens exist as a Kubernetes Secret and reside in the same namespace as the SA. A SA definition references the secret it is associated with.

This article demonstrates how you can find these tokens and use them for authenticating with the Kubernetes API.

Shell Script Interaction

There are several methods for finding the token associated with a Service Account, and first we will cover how we can extract that token using a simple shell script. The example below demonstrates how we can get the token for the service account named default in the kube-system namespace. I’m using this service account because it’s likely to have loose RBAC permissions, but this should work with any service account in your cluster.

Here is the definition of the Service Account, and note that Kubernetes automatically created a secret called default-token-vvv5w when the SA was created.

1
$ kubectl get sa -n kube-system default -o yaml
1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: "2020-08-03T01:50:54Z"
  name: default
  namespace: kube-system
  ...
secrets:
- name: default-token-vvv5w

The shell script below looks up a given SA k8s resource, queries out the secret name, looks up the Secret k8s resource, and saves the token associated with the secret to a variable. Also note, that the script saves out the k8s cluster’s CA Certificate; each cluster contains a public CA cert which is used during authentication to validate the cluster. That cert is also stored as part of the Secret’s data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
NAMESPACE='kube-system'
SA_NAME='default'

# Get token resource from SA
TOKEN_NAME=$(kubectl get sa -n ${NAMESPACE} ${SA_NAME} \
    -o=jsonpath='{.secrets[0].name}')

# Get the SA PEM cert and save to file
kubectl get secret -n $NAMESPACE $TOKEN_NAME \
    -o=jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt.pem

# Get the Bearer token; it is base64 encoded when stored in the secret.
TOKEN=`kubectl get secret -n $NAMESPACE $TOKEN_NAME \
   -o=jsonpath='{.data.token}' | base64 -d`

# Print the token
echo $TOKEN

As an interesting note, these tokens are simply JWT tokens that are signed by the Kubernetes API. The JWT data contains some basic information about the token, such as the Service Account name and Secret name that this token represents.

We can view that JWT data with the Python pyjwt command line tool (or your JWT tool of choice).

1
2
pip install --user pyjwt
pyjwt decode --no-verify ${TOKEN} | jq
{
"iss": "kubernetes/serviceaccount",
"kubernetes.io/serviceaccount/namespace": "default",
"kubernetes.io/serviceaccount/secret.name": "default-token-ptk4f",
"kubernetes.io/serviceaccount/service-account.name": "default",
"kubernetes.io/serviceaccount/service-account.uid": "a469fa87-f149-4c02-9fc4-440a6ab1b910",
"sub": "system:serviceaccount:default:default"
}

Now the fun part: we can use the token to authenticate against the Kubernetes API. We can get the API url from the kubeconfig, and curl it with our token and CA Certificate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# handy alternative to jq that lets you filter down yaml docs
pip install --user yq

# Lookup the API Server url from the default kubeconfig (i.e. https://192.168.39.239:8443)
# Note: you might have to adjust this command depending on the layout of your kubeconfig file.
API_SERVER=`cat ~/.kube/config  | yq .clusters[0].cluster.server | tr -d '"'`

# Using the ca.crt.pem, token and api server url, query the k8s api
curl --cacert ca.crt.pem -H "Authorization: Bearer ${TOKEN}" ${API_SERVER}/version
{
  "major": "1",
  "minor": "18",
  "gitVersion": "v1.18.3",
  "gitCommit": "2e7996e3e2712684bc73f0dec0200d64eec7fe40",
  "gitTreeState": "clean",
  "buildDate": "2020-05-20T12:43:34Z",
  "goVersion": "go1.13.9",
  "compiler": "gc",
  "platform": "linux/amd64"
}

Python Script Interaction

While the shell script approach might be good for automating a build process or running a quick and dirty token lookup, sometimes we might want to talk programmatically with the Kubernetes API to achieve the same result.

Below is an example of a Python script that interacts with the kubernetes api to lookup the same token. This code was extracted from a much more complicated example in my kubernetes-user module.

While the steps here are basically the same as the shell script, this time we are using only the official kubernetes Python client to do the lookups. We don’t even need to have kubectl installed, but it does assume a kubeconfig is setup.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import base64
import kubernetes
from kubernetes import client, config

name = "default"
namespace = "kube-system"

# load the kubernetes config from the default config file 
# or from the path in KUBECONFIG env var
api_client = config.new_client_from_config()

# load the core api group which has methods to access SAs and Secrets
api_instance = kubernetes.client.CoreV1Api(api_client)

# get the service account resource
sa_resource = api_instance.read_namespaced_service_account(name=name, namespace=namespace)

# get the token associated with the service account
token_resource_name = [s for s in sa_resource.secrets if 'token' in s.name][0].name

# get the secret resource associated with the service account
secret = api_instance.read_namespaced_secret(
    name=token_resource_name, namespace=namespace)

# get the token data out of the secret
btoken = secret.data['token']

# the token data is base64 encoded, so we decode it
token = base64.b64decode(btoken).decode()

# get the ca.crt out of the secret
bcacrt = secret.data['ca.crt']

# the ca.crt data is base64 encoded, so we decode it; the result is a PEM string
cacrt = base64.b64decode(bcacrt).decode()

The token variable contains a JWT token.

1
token
'eyJhbGciOiJSUzI1NiI....snip....ivtcVhWKLr5s38K6dFnq1tdglmLEDN1h8qt9xfA'

As before, if we want to view the JWT token contents, we can use the pyjwt module to decode it.

1
2
import jwt
jwt.decode(token, verify=False)
{'iss': 'kubernetes/serviceaccount',
'kubernetes.io/serviceaccount/namespace': 'kube-system',
'kubernetes.io/serviceaccount/secret.name': 'default-token-vvv5w',
'kubernetes.io/serviceaccount/service-account.name': 'default',
'kubernetes.io/serviceaccount/service-account.uid': 'bc52984b-4aa8-4f9c-ba08-cdeb3d4479cd',
'sub': 'system:serviceaccount:kube-system:default'}
Written on August 4, 2020