Updates to Service Accounts in Kubernetes v1.22 and v1.24

Updates to Service Accounts in Kubernetes v1.22 and v1.24

What is a ServiceAccount?

There are two types of accounts in Kubernetes, user accounts and service accounts.

User accounts are for humans, for example, admins, or developers. They can use such accounts to gain access to the cluster or make changes to it. An example of this is whenever you run kubectl commands. The cluster authenticates the command through the user account that’s configured in the kubectl config file.

A service account is an account that is intended to be used by programs inside pods.

Why is it important?

Service accounts are important for security purposes. They allow us to monitor and identify what is accessing the cluster. They also allow us to control permissions. This ensures pods can access and change only what we specifically allow.

When you create a service account, it is associated with a token. That token can be used as a bearer token when you’re calling the Kubernetes API through a form of a web call like this:

curl http://localhost:8080/api/ -header "Authorization: Bearer <token>"

Output:

{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "192.168.49.2:8443"
    } 
  ]
}

Extracting the token from the service account can be tedious and repetitive. This is especially true in newer versions of Kubernetes as tokens may have a limited shelf life.

An easier way is to directly mount the token to the pod where the program is running. Here’s an example pod configuration below.

apiVersion: v1
kind: Pod
metadata:
 name: nginx
spec:
 containers:
 - image: nginx
   name: nginx
   volumeMounts:
   - mountPath: /var/run/secrets/tokens
     name: vault-token
 serviceAccountName: build-robot
 volumes:
 - name: vault-token
   projected:
     sources:
     - serviceAccountToken:
         path: vault-token
         expirationSeconds: 7200
         audience: vault

Here we see the service account build-robot is being referenced. Under volumes, we see the token of that service account being configured with expiry and audience (more on this later). Finally, the volume is mounted into /var/run/secrets/tokens and this will contain the token. This way the token can manifest as a file and can easily be read by whatever program is running in that pod.

How to create a service account?

Creating a service account is quite simple. Here, we are using Kubernetes v1.20. The below command will create a new service account with the name test-sa.

kubectl create serviceaccount test-sa

Output:

serviceaccount/test-sa created

You can also verify if it was created successfully by running this command:

kubectl get serviceaccount test-sa

Output:

NAME      SECRETS   AGE
test-sa   1         8s

Just as with any resource in Kubernetes, we can get more details about the service account with the describe command.

kubectl describe serviceaccount test-sa

Output:

Name:                test-sa
Namespace:           default
Labels:              <none>
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   test-sa-token-rdv56
Tokens:              test-sa-token-rdv56
Events:              <none>

Notice there’s a column that says Mountable secrets. This indicates that a secret resource has also been created automatically for that service account. I’ll tell you more about this later as there are some changes in the newer versions that affect how secrets are automatically generated whenever you create a new service account.

Let’s go ahead and check out the details of that secret.

[email protected]:~$ kubectl describe secret test-sa-token-rdv56

Output:

Name:         test-sa-token-rdv56
Namespace:    default
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: test-sa
              kubernetes.io/service-account.uid: c633f75f-6ad1-4473-8140-0fe1b0e40db7
Type:  kubernetes.io/service-account-token
Data
====
ca.crt:     1111 bytes
namespace:  7 bytes
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6IlRaVW5iTWZab1RBTjRubE9mQmZQVzl1V0JaeXVjS3RMa3R5WDJQNjdsbDAifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InRlc3Qtc2EtdG9rZW4tcmR2NTYiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoidGVzdC1zYSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6ImM2MzNmNzVmLTZhZDEtNDQ3My04MTQwLTBmZTFiMGU0MGRiNyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnRlc3Qtc2EifQ.KeFUs3c2YB2hUgI3RnKm3HKcf2Xtn9fgwulzMIcsB201uniqxwjigsHwayfUWNlrOP3VPLFltTwtQ2gBotaHBGcEI1hIvZ-rxejESPqwJSrF4t_LWzoemz1LxnhrCLeCXN2GrMtu1gVbyafxxLvmafnj2K8kgzGXhb9EtBUrqNGUWqHNn12FAhy3U5uQdgDn3Xb21VLr06mPbnSk9TJ_UrZTyMq0T3Ym-ZJ7usfEWIq7ddq9-3T3gzM4Zedgt-SCSkYJILvh1O4TtvAeCkzJ51fLawhLi5v3UrjNWrEkUmCmyc7Z5QhZu3FgEAs06fzTiQSUdh58wqS5HhfA7yqI3g

We are presented with the details of that secret resource and also the token. This token will serve as identification as it is linked to the service account.

The Default Service Account

Each namespace cluster has its own default service account. You can easily identify the default service account and its secret based on its name, default, and default-token-<random-suffix>.

Try listing all service accounts with this command:

kubectl get serviceaccount
NAME      SECRETS   AGE
default   1         8m42s
test-sa   1         8m36s

You can also use the describe command to get more details.

kubectl describe serviceaccount default

Output:

Name:                default
Namespace:           default
Labels:              <none>
Annotations:         <none>
Image pull secrets:  <none>
Mountable secrets:   default-token-hcgns
Tokens:              default-token-hcgns
Events:              <none>

Do note that the permissions of the default service account are quite limited. If you need to work with a more customized set of permissions, you can create your own service account for that.

When you create a pod and don’t associate a specific service account, Kubernetes will automatically use the default service account when the pod is created.

Let’s see this in action. Let’s first create an nginx pod.

[email protected]:~$ kubectl run nginx --image=nginx

Output:

pod/nginx created

Then let’s execute the describe command to the pod to get its details.

[email protected]:~$ kubectl describe pod nginx

Output:

Name:             nginx
Namespace:        default
Priority:         0
Service Account:  default
Node:             aged/192.168.49.2
Start Time:       Tue, 18 Oct 2022 23:20:00 +0800
Labels:           run=nginx
Annotations:      <none>
Status:           Running
IP:               172.17.0.3
IPs:
  IP:  172.17.0.3
Containers:
  Nginx:
   …
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-hcgns (ro)

Since we did not set a specific service account to be used, notice that the pod that was created is using the default service account in the cluster, as shown in the Service Account field.

One thing I want to point out as well as the directory in the Mounts field. The directory /var/run/secrets/kubernetes.io/serviceaccount is the standard directory whenever a default service account is used. You can use the following command to see what’s inside.

[email protected]:~$ kubectl exec nginx -c nginx -- ls -l /var/run/secrets/kubernetes.io/serviceaccount

Output:

lrwxrwxrwx 1 root root 13 Oct 19 14:44 ca.crt -> ..data/ca.crt
lrwxrwxrwx 1 root root 16 Oct 19 14:44 namespace -> ..data/namespace
lrwxrwxrwx 1 root root 12 Oct 19 14:44 token -> ..data/token

The token file will contain the encoded JWT token(JSON Web Token) for the default service account. You can run this command if you’d like to take a look inside the file.

kubectl exec nginx -- cat /var/run/secrets/kubernetes.io/serviceaccount/token

Don’t worry if it looks gibberish, this is because it is encoded and it was intended to be this way.

eyJhbGciOiJSUzI1NiIsImtpZCI6IlhuUkZnQ01rbDdVZTd5dlVtZ3NwNVVLUzdBcWNkNXdRMGJoLVZWN011WGMifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjk3OTUyNDI3LCJpYXQiOjE2NjY0MTY0MjcsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0IiwicG9kIjp7Im5hbWUiOiJuZ2lueCIsInVpZCI6ImMzNzViMGYyLTYxMzYtNGNiZC1iNzNjLWUyZGFmYmVmYzdhNiJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoiZGVmYXVsdCIsInVpZCI6ImNlZGU1MjE1LWY1MzYtNDA4OS1iNGY0LWU5NDY2MjkyMDhiYyJ9LCJ3YXJuYWZ0ZXIiOjE2NjY0MjAwMzR9LCJuYmYiOjE2NjY0MTY0MjcsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.HAi8KrO_r6MtI21fyIMOPgT_OkswMJfKwxyJnd4-s0T1iMf3rIwQIOUEmmfXYO1J468IGQ4RHE6P7SSqIgrgsGOknzDcH91Ou0Z84xpVmwS8A5JsS6amZmeUERISMrMhOrnEa0t6k6-agjIwosoV0t2QvBPjJubtB6j81G4hIHUPkjVXJLs9i9OYKv54P6fp_ZAQvBjpyxVGNBmjD5sOJ7FU6sEgQCN7AJ6hL1L0MwpaHjBZzsDT164Tiujqg5zhhIwlE1gFeE2b1XUTTUp2iDS_LDRXBzMgEV6JaAgAbtavZ0PWBJqZ9iHRNpQFltfKUQEpc0XLFAq8pnzQhJ2HXg

Tokens like this hold structured information and to see it in a much more human-readable format, you have to decode it first. For this purpose, you can use sites such as jwt.io.

Changes in newer versions

Now we know what service accounts are and their role in securing the cluster. Let’s now discuss some of the changes introduced for service accounts in the newer versions of Kubernetes.

Changes in v1.22

Mounted Secret

In previous versions of Kubernetes when you create a pod with a default service account, the secret of the default service account is added as a volume and then mounted in the standard directory.

Pod YAML configuration in v1.20:

volumes:
  - name: default-token-hcgns
    secret:
      defaultMode: 420
      secretName: default-token-hcgns
volumeMounts:
     - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
       name: default-token-hcgns
       readOnly: true

If you decode the contents of this token, you’ll notice that there’s no field to indicate when the token expires. Non-expiring tokens are less secure. The audience also does not exist. An audience in a JWT token specifies “who should be the recipient of the token” since this is missing in older versions, making it less secure. Basically, anyone who has the token can use it if no audience is specified.

Decoded v1.20 token:

{
  "iss": "kubernetes/serviceaccount",
  "kubernetes.io/serviceaccount/namespace": "default",
  "kubernetes.io/serviceaccount/secret.name": "default-token-hcgns",
  "kubernetes.io/serviceaccount/service-account.name": "default",
  "kubernetes.io/serviceaccount/service-account.uid": "f7c37f76-850d-4acd-b293-6182554f070c",
  "sub": "system:serviceaccount:default:default"
}

In version v1.22, pods created with a default service account don’t rely on the tokens from the secret but rather the token coming from the service account admission controller. These tokens have a standard naming format which is kube-api-access-<random-suffix>

Notice how in volumes the configuration is quite different in v1.22 as compared to the previous versions. Here we see that all three files namely token, ca.crt, and namespace are being referenced. You can also see the expirationSeconds field here indicating the expiry value in seconds.

Pod YAML configuration volume section in v1.22:

  volumes:
  - name: kube-api-access-l7tbh
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace

In the volumeMounts field, there’s no difference from previous versions. Here we still see the token being mounted in the standard directory.

Pod YAML configuration volumeMounts section in v1.22:

volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-l7tbh
      readOnly: true

TokenRequestAPI

Thanks to the TokenRequestAPI, tokens generated from it are now more secure as they now come with expiry and audience. Here’s an example of a decoded token in v1.22. It’s now quite different from what we’ve seen in previous versions. Notice the aud field which refers to the audience and the exp field which refers to the expiration/lifetime of the token.

Decoded v1.22 token:

{
  "aud": [
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1697952427,
  "iat": 1666416427,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "kubernetes.io": {
    "namespace": "default",
    "pod": {
      "name": "nginx",
      "uid": "c375b0f2-6136-4cbd-b73c-e2dafbefc7a6"
    },
    "serviceaccount": {
      "name": "default",
      "uid": "cede5215-f536-4089-b4f4-e946629208bc"
    },
    "warnafter": 1666420034
  },
  "nbf": 1666416427,
  "sub": "system:serviceaccount:default:default"
}

Changes in v1.24

In version v1.24 more enhancements have been introduced affecting the behavior of service accounts and their secrets.

There’s no longer a secret

In previous versions when you create a service account, it also automatically creates a secret resource associated with the service account. As shown below.

[email protected]:~$ kubectl create serviceaccount test-sa

Output:

serviceaccount/test-sa created

We can verify this by fetching existing secrets after creating the service account.

[email protected]:~$ kubectl get serviceaccount,secret

Output:

NAME                     SECRETS   AGE
serviceaccount/default   1         3h51m
serviceaccount/test-sa   1         9s
NAME                         TYPE                                  DATA   AGE
secret/default-token-rbw6m   kubernetes.io/service-account-token   3      3h51m
secret/test-sa-token-vtmwl   kubernetes.io/service-account-token   3      9s

In version v1.24 when you create a service account, secrets are no longer automatically created. To create a token, you can do it with the following command: kubectl create token <service account name>.

Let’s try to create one with this command:

kubectl create token test-sa

Running the command above will output the token in encoded format.

Output:

eyJhbGciOiJSUzI1NiIsImtpZCI6InJvTFJDQkdNQlRuMTBQQnJTdC1ISXJIOXdoZ19wTkhLLWlBUXpFYlJ2T0UifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNjY2NDQxMDE1LCJpYXQiOjE2NjY0Mzc0MTUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwic2VydmljZWFjY291bnQiOnsibmFtZSI6InRlc3Qtc2EiLCJ1aWQiOiI3Y2QyOGMzMC02ZjI2LTQ1NjAtYmE4Ny05NTU4NzRjOWQ3NDUifX0sIm5iZiI6MTY2NjQzNzQxNSwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6dGVzdC1zYSJ9.BmjtE-a1m-_5zKuiq5-yKZdcMdDhbpfDub4gYMiArYEFaQeOoloz5nA6J2QZAJSUW7DnEdrjOsWOZghsugF_yqSAAGuXFOL08-LSY43LxlXAZ_mtDg6pvwTDf8TjjwzQdRCHpV-cjJEvtGM-lQzOnt-rWcG2IYkM5LTnzFlCGz06QCUjyor32lMiyHcWCY9USehgJnzi1IBLPIPVq6qt4fRRKFXYQwVOrrGlaU0ULQwcc5kr6sFMmHw5IGpc8TL36csxiGb9Zzsq9njB_PsrknFK6-6-bWFIN9gUjZ6wqIdZe386w0qQ_6iuiz2W71vQWXCXnEbUJvO59Y7ywPtnsw

In the decoded format, you will see there’s an expiry, audience, and also the service account name.

{
  "aud": [
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1666441015,
  "iat": 1666437415,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "kubernetes.io": {
    "namespace": "default",
    "serviceaccount": {
      "name": "test-sa",
      "uid": "7cd28c30-6f26-4560-ba87-955874c9d745"
    }
  },
  "nbf": 1666437415,
  "sub": "system:serviceaccount:default:test-sa"
}

What if I want the old behavior?

If you would still prefer secrets with the non-expiring token, you can still do so. You just need to create a secret with the type, kubernetes.io/service-account-token and annotation of kubernetes.io/sevice-account.name with the value of the service account name. Here’s an example of the YAML file to create this type of secret.

apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  name: secret-name
  annotations:
    kubernetes.io/service-account.name: test-sa

Please take into account that this is no longer the preferred method. This method should only be considered if you are unable to use TokenRequestAPI to generate the tokens or the security concerns due to the non-expiring tokens are not an issue.

Conclusion

And there you have it. I hope this helped you gain a better understanding of what service accounts are, the role they play in the cluster, and finally, the changes it has gone through in different Kubernetes versions.

Learning how service accounts work is important especially when you’re managing a cluster in your organization or if you’re planning to take any of the Kubernetes certification exams.

If you’d like to learn more about Kubernetes, whether you are a beginner or already experienced, you can check out this amazing Kubernetes learning path! It has everything you need for you to become an expert in Kubernetes regardless of where you are in your learning journey.