Table of Contents

A common way for users to authenticate Kubernetes clusters is through an API that issues JSON Web Tokens (JWT). These tokens can be used to identify a Kubernetes user or service account and grant them access to the environment. While this is not a new feature, Kubernetes 1.24 adds a command that allows users to create these tokens more easily using the TokenRequest API.

  • We can use JWT to authenticate Kubernetes users and service accounts.

  • The new Kubernetes version makes it easier to create JWT tokens.

  • Creating JWT tokens using the TokenRequest API is easier than ever.

In the K8s version before 1.24, every time we would create a service account, a non-expiring secret token (Mountable secrets & Tokens) was created by default. However, from version 1.24 onwards, it was disbanded and no secret token is created by default when we create a service account. However, we can create it when need be. Now let us take a look at the service account token in a bit more depth.

Service Account Token

Kubernetes supports two types of tokens from version 1.22 onwards.

  • Long-Lived Token
  • Time Bound Token

Long-Lived Token

As its name indicates, a long-lived token is one that never expires. Hence, it is less secure and discouraged to use.

Creating Long-Lived Token

Before K8s version 1.24, whenever a service account was created, a secret object was also created that contains the secret token. These token would be long-lived token, which means it has no expiry. However, In Kubernetes version 1.24, it was disbanded due to security and scalability concerns.

Although not recommended, K8s allows us to create a long-lived token. It is achieved in two different steps:

  • Create a service account
  • Create a secret and specify the name of the service account as annotations within the metadata section.
kubect create serviceaccount my-sa

kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: my-long-lived-secret
  annotations:
    kubernetes.io/service-account.name: my-sa
type: kubernetes.io/service-account-token
EOF

Time Bound Token

From version 1.22 onwards, Kubernetes introduced TokenRequest API. A token generated through this API is a time-bound token that expires after a time. It applies to both — the default service account and the custom-defined service accounts.

Creating a time-bound token

We can create a time-bound token using the below command:

echo '{"apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest", "spec": {"audiences": ["https://kubernetes.default.svc.cluster.local"], "expirationSeconds": 3600}' \
  | kubectl create -f- --raw /api/v1/namespaces/default/serviceaccounts/my-sa/token

echo '{
  "apiVersion": "authentication.k8s.io/v1",
  "kind": "TokenRequest",
  "spec": {
    "audiences": [
      "https://kubernetes.default.svc.cluster.local"
    ],
    "expirationSeconds": 3600
  }
}' \
 | kubectl create -f- --raw /api/v1/namespaces/default/serviceaccounts/my-sa/token

In the following sections, we will see a completed example using various methods, from creating a ServiceAccount to creating a Token, using Token, TokenReview

Create ServiceAccount

To create a ServiceAccount in Kubernetes, follow these steps:

ServiceAccount
  • go
  • yaml
  • bash
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
func updateOrCreateServiceAccount(clientset *kubernetes.Clientset, ctx context.Context, namespace, serviceAccountName string) (*corev1.ServiceAccount, error) {
	get, err := clientset.CoreV1().ServiceAccounts(namespace).Get(ctx, serviceAccountName, metav1.GetOptions{})
	if err != nil {
		if !errors.IsNotFound(err) {
			return nil, err
		}
	}

	labels := map[string]string{"app": "myapp"}

	if get != nil {
		get.ObjectMeta.Labels = labels
		return clientset.CoreV1().ServiceAccounts(namespace).Update(ctx, get, metav1.UpdateOptions{})
	}

	sa := &corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      serviceAccountName,
			Labels:    labels,
		},
	}

	return clientset.CoreV1().ServiceAccounts(namespace).Create(ctx, sa, metav1.CreateOptions{})
}

Create Role

To create a Role in Kubernetes, follow these steps:

Role
  • go
  • yaml
  • bash
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
func updateOrCreateRole(clientset *kubernetes.Clientset, ctx context.Context, namespace, roleName string) (*rbacv1.Role, error) {
	get, err := clientset.RbacV1().Roles(namespace).Get(ctx, roleName, metav1.GetOptions{})
	if err != nil {
		if !errors.IsNotFound(err) {
			return nil, err
		}
	}

	rules := []rbacv1.PolicyRule{
		{
			APIGroups: []string{""},
			Resources: []string{"pods"},
			Verbs:     []string{"list", "get", "watch"},
		},
	}

	if get != nil {
		get.Rules = rules
		return clientset.RbacV1().Roles(namespace).Update(ctx, get, metav1.UpdateOptions{})
	}

	role := &rbacv1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      roleName,
		},
		Rules: rules,
	}

	return clientset.RbacV1().Roles(namespace).Create(ctx, role, metav1.CreateOptions{})
}

This will create the Role in the default namespace.

That’s it! You have now created a Role in Kubernetes.

Create RoleBinding

In Kubernetes, RoleBinding is used to bind a role to a user, group, or service account in a given namespace. The RoleBinding grants the permissions defined in the role to the subject.

To create a RoleBinding, follow these steps:

RoleBinding
  • go
  • yaml
  • bash
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
func updateOrCreateRoleBinding(clientset *kubernetes.Clientset, ctx context.Context, namespace, roleName, roleBindingName, serviceAccountName string) (*rbacv1.RoleBinding, error) {
	get, err := clientset.RbacV1().RoleBindings(namespace).Get(ctx, roleBindingName, metav1.GetOptions{})
	if err != nil {
		if !errors.IsNotFound(err) {
			return nil, err
		}
	}

	roleBinding := &rbacv1.RoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      roleBindingName,
		},
		Subjects: []rbacv1.Subject{
			{
				Kind:      rbacv1.ServiceAccountKind,
				Name:      serviceAccountName,
				Namespace: namespace,
			},
		},
		RoleRef: rbacv1.RoleRef{
			APIGroup: rbacv1.GroupName,
			Kind:     "Role",
			Name:     roleName,
		},
	}

	if get != nil {
		get = roleBinding
		return clientset.RbacV1().RoleBindings(namespace).Update(ctx, get, metav1.UpdateOptions{})
	}

	return clientset.RbacV1().RoleBindings(namespace).Create(ctx, roleBinding, metav1.CreateOptions{})
}

This will create the RoleBinding in the default namespace.

That’s it! You have now created a RoleBinding in Kubernetes.

Get a user bearer token

To successfully make HTTP requests to the Kubernetes API a bearer token must be included as an authorization header. Below is an example command one could run to get the bearer token for a user named admin-user in the namespace of kube-system. This same command could apply to any other user or namespace.

kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')

TokenRequest

The TokenRequest API allows you to request a token for a ServiceAccount in Kubernetes. To use TokenRequest, follow these steps:

TokenRequest
  • go
  • yaml
  • bash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func createToken(clientset *kubernetes.Clientset, ctx context.Context, namespace, serviceAccountName string) (*authenticationv1.TokenRequest, error) {
	// Create a TokenRequest
	tokenRequest := &authenticationv1.TokenRequest{
		Spec: authenticationv1.TokenRequestSpec{
			Audiences: []string{"https://kubernetes.default.svc.cluster.local"}, // service-account-issuer
			ExpirationSeconds: func() *int64 {
				var seconds int64 = 3600 // Token expiration time in seconds (1 hour)
				return &seconds
			}(),
		},
	}

	// Request the token
	return clientset.CoreV1().ServiceAccounts(namespace).CreateToken(ctx,
		serviceAccountName, tokenRequest, metav1.CreateOptions{})
}

This will create the TokenRequest and return the token for the specified ServiceAccount.

That’s it! You have now used TokenRequest to request a token for a ServiceAccount in Kubernetes.

Get Pods

Use the generated token to communicate with APIServer to obtain the Pods of default namespace:

GetPods
  • go
  • bash
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
36
37
38
39
40
41
42
package main

import (
	"crypto/tls"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	kubeApiUrl := "https://{kubernetes API IP}:{kubernetes API Port}"
	namespace := "default"
	bearerToken := "..."

	client := &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		},
	}

	req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/v1/namespaces/%s/pods/reviews-v3-d88774f9c-gt5gg", kubeApiUrl, namespace), nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", bearerToken))
	req.Header.Set("Accept", "application/json")

	response, err := client.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer response.Body.Close()

	body, err := io.ReadAll(response.Body)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(body))
}

TokenReview

The TokenReview API allows you to validate a token for a ServiceAccount in Kubernetes. To use TokenReview, follow these steps:

TokenReview
  • go
  • bash
1
2
3
4
5
6
7
8
9
10
11
12
13
func createTokenReview(clientset *kubernetes.Clientset, ctx context.Context, namespace, token string) (*authenticationv1.TokenReview, error) {
	tokenReview := &authenticationv1.TokenReview{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
		},
		Spec: authenticationv1.TokenReviewSpec{
			Token:     token,
			Audiences: []string{"https://kubernetes.default.svc.cluster.local"},
		},
	}

	return clientset.AuthenticationV1().TokenReviews().Create(ctx, tokenReview, metav1.CreateOptions{})
}

This will display the status of the TokenReview request, including whether the token is valid or not.

Complete code

main
  • go
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"

	authenticationv1 "k8s.io/api/authentication/v1"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/klog/v2"
)

var ctx = context.Background()

var (
	namespace          string
	serviceAccountName string
	roleName           string
	roleBindingName    string
)

func init() {
	flag.StringVar(&namespace, "n", "default", "target namespace")
	flag.StringVar(&serviceAccountName, "sa", "my-sa", "service account name")
	flag.StringVar(&roleName, "role", "my-role", "role name")
	flag.StringVar(&roleBindingName, "rb", "my-rb", "role binding name")
	flag.Parse()
}

func main() {
	// Load kubeconfig from home file
	config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
	if err != nil {
		klog.Errorf("Error getting config: %v", err)
	}
	// Create a clientset
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		klog.Errorf("Error creating clientset: %v", err)
	}

	// Creat ServiceAccount
	_, err = updateOrCreateServiceAccount(clientset, ctx, namespace, serviceAccountName)
	if err != nil {
		klog.Errorf("Error create ServiceAccount: %v", err)
		return
	}

	// Create Role
	_, err = updateOrCreateRole(clientset, ctx, namespace, roleName)
	if err != nil {
		klog.Errorf("Error create Role: %v", err)
		return
	}

	// Create RoleBinding
	_, err = updateOrCreateRoleBinding(clientset, ctx, namespace, roleName, roleBindingName, serviceAccountName)
	if err != nil {
		klog.Errorf("Error create RoleBinding: %v", err)
		return
	}

	// Create Token
	token, err := createToken(clientset, ctx, namespace, serviceAccountName)
	if err != nil {
		klog.Errorf("Error create Token: %v", err)
		return
	}

	//fmt.Printf("Name: %s\nToken: %s\n", token.Name, token.Status.Token)
	bytes, _ := json.Marshal(token)
	fmt.Println(string(bytes))
}

func createToken(clientset *kubernetes.Clientset, ctx context.Context, namespace, serviceAccountName string) (*authenticationv1.TokenRequest, error) {
	// Create a TokenRequest
	tokenRequest := &authenticationv1.TokenRequest{
		Spec: authenticationv1.TokenRequestSpec{
			Audiences: []string{"https://kubernetes.default.svc.cluster.local"}, // service-account-issuer
			ExpirationSeconds: func() *int64 {
				var seconds int64 = 3600 // Token expiration time in seconds (1 hour)
				return &seconds
			}(),
		},
	}

	// Request the token
	return clientset.CoreV1().ServiceAccounts(namespace).CreateToken(ctx,
		serviceAccountName, tokenRequest, metav1.CreateOptions{})
}

func updateOrCreateRoleBinding(clientset *kubernetes.Clientset, ctx context.Context, namespace, roleName, roleBindingName, serviceAccountName string) (*rbacv1.RoleBinding, error) {
	get, err := clientset.RbacV1().RoleBindings(namespace).Get(ctx, roleBindingName, metav1.GetOptions{})
	if err != nil {
		if !errors.IsNotFound(err) {
			return nil, err
		}
	}

	roleBinding := &rbacv1.RoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      roleBindingName,
		},
		Subjects: []rbacv1.Subject{
			{
				Kind:      rbacv1.ServiceAccountKind,
				Name:      serviceAccountName,
				Namespace: namespace,
			},
		},
		RoleRef: rbacv1.RoleRef{
			APIGroup: rbacv1.GroupName,
			Kind:     "Role",
			Name:     roleName,
		},
	}

	if get != nil {
		get = roleBinding
		return clientset.RbacV1().RoleBindings(namespace).Update(ctx, get, metav1.UpdateOptions{})
	}

	return clientset.RbacV1().RoleBindings(namespace).Create(ctx, roleBinding, metav1.CreateOptions{})
}

func updateOrCreateRole(clientset *kubernetes.Clientset, ctx context.Context, namespace, roleName string) (*rbacv1.Role, error) {
	get, err := clientset.RbacV1().Roles(namespace).Get(ctx, roleName, metav1.GetOptions{})
	if err != nil {
		if !errors.IsNotFound(err) {
			return nil, err
		}
	}

	rules := []rbacv1.PolicyRule{
		{
			APIGroups: []string{""},
			Resources: []string{"pods"},
			Verbs:     []string{"list", "get", "watch"},
		},
	}

	if get != nil {
		get.Rules = rules
		return clientset.RbacV1().Roles(namespace).Update(ctx, get, metav1.UpdateOptions{})
	}

	role := &rbacv1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      roleName,
		},
		Rules: rules,
	}

	return clientset.RbacV1().Roles(namespace).Create(ctx, role, metav1.CreateOptions{})
}

func updateOrCreateServiceAccount(clientset *kubernetes.Clientset, ctx context.Context, namespace, serviceAccountName string) (*corev1.ServiceAccount, error) {
	get, err := clientset.CoreV1().ServiceAccounts(namespace).Get(ctx, serviceAccountName, metav1.GetOptions{})
	if err != nil {
		if !errors.IsNotFound(err) {
			return nil, err
		}
	}

	labels := map[string]string{"app": "myapp"}

	if get != nil {
		get.ObjectMeta.Labels = labels
		return clientset.CoreV1().ServiceAccounts(namespace).Update(ctx, get, metav1.UpdateOptions{})
	}

	sa := &corev1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namespace,
			Name:      serviceAccountName,
			Labels:    labels,
		},
	}

	return clientset.CoreV1().ServiceAccounts(namespace).Create(ctx, sa, metav1.CreateOptions{})
}

References