K8s Tips: Using a ServiceAccount

K8s Tips: Using a ServiceAccountLuc JuggeryBlockedUnblockFollowFollowingMay 30A ServiceAccount is used by containers running in a Pod to communicate with the API Server of the Kubernetes cluster.

In this post we will see how this is done and the Kubernetes resources involved in the process.

API Server — HTTP Rest APIIf you work with a Kubernetes cluster, chances are that you use the kubectl binary or the web interface to call the API Server.

Behind the hood, all the calls are done through the HTTP endpoints exposed by the API.

This API is well documented, all the endpoints are described in the official documentation https://kubernetes.

io/docs/reference/generated/kubernetes-api/v1.

14.

A simple example: the list of the Pods running in the default namespace can be retrieved from https://API_SERVER/api/v1/namespaces/default/pods/.

Of course the request needs to be authenticated and authorized to perform this action.

Accessing the API Server from a PodA lot of applications running in the cluster (read: running in Pods) need to communicate with the API Server.

Among them are the processes running within the Control Plane (scheduler, controller manager, proxy, …), and also all the applications that need to perform some administration of the cluster.

For example, some applications may need to know:the status of the cluster’s nodesthe namespaces availablethe Pods running in the cluster, or in a specific namespace…To communicate with the API Server, a Pod uses a ServiceAccount containing an authentication token.

Roles (eg: the right to list all the Pods within a given namespace) or ClusterRole (eg: the right to read all the Secrets within the entire cluster) can then be bound to this ServiceAccount, respectively with a RoleBinding or a ClusterRoleBinding, so the ServiceAccount is authorized to perform those actions.

From the outside of the cluster : the API Server can be accessed using the endpoint specified in the kube config file($HOME/.

kube/config by default).

As an example, if you use a DigitalOcean Managed Kubernetes, the endpoint is something like https://b703a4fd-0d56-4802-a354-ba2c2a767a77.

k8s.

ondigitalocean.

comFrom the inside of the cluster (read: from a Pod): the API Server can be accessed using the dedicated Service of type ClusterIP named kubernetes.

This service is there by default and automatically recreated if deleted by error (it happens).

$ kubectl get svcNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGEkubernetes ClusterIP 10.

96.

0.

1 <none> 443/TCP 23hWith the correct rights (more on that later) listing the Pods in the default namespace can be done from a Pod with this simple GET request https://kubernetes/api/v1/namespaces/default/pods/Using the namespace’s default ServiceAccountEach Namespace has a default ServiceAccount, named default.

We can verify this with the following command:$ kubectl get sa –all-namespaces | grep defaultdefault default 1 6m19skube-public default 1 6m19skube-system default 1 6m19sLet’s inspect the ServiceAccount named default of the default namespace (this will be pretty much the same for the default ServiceAccount of another namespace)$ kubeclt get sa default -o yamlapiVersion: v1kind: ServiceAccountmetadata: name: default namespace: default .

secrets:- name: default-token-dffkjWe can see here that a Secret is provided to this ServiceAccount.

Let’s have a closer look at this one:$ kubectl get secret default-token-dffkj -o yamlapiVersion: v1data: ca.

crt: LS0tLS1CRU.

0tLS0tCg== namespace: ZGVmYXVsdA== token: ZXlKaGJHY2.

RGMUlIX2c=kind: Secretmetadata: name: default-token-dffkj namespace: default .

type: kubernetes.

io/service-account-tokenThere are several key/value pairs under the data key of this Secret (for readability, I have shortened the value of the ca.

crt and token values), basically:ca.

crt is the base64 encoding of the cluster certificatenamespace is the base64 encoding of the current namespace (default)token is the base64 encoding of the JWT used to authenticate against the API ServerNote: JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object.

This information can be verified and trusted because it is digitally signed.

Let’s focus on the token: once decoded (using “base64 -d” on Linux, or “base64 -D” on macOS), we can easily get the payload of this JWT, from the command line or some online services like jwt.

io.

This payload has the following format:{ “iss”: “kubernetes/serviceaccount”, “kubernetes.

io/serviceaccount/namespace”: “default”, “kubernetes.

io/serviceaccount/secret.

name”: “default-token-dffkj”, “kubernetes.

io/serviceaccount/service-account.

name”: “default”, “kubernetes.

io/serviceaccount/service-account.

uid”: “ac5aa972–80ae-11e9–854d-0800278b691f”, “sub”: “system:serviceaccount:default:default”}We can see the ServiceAccount it is linked to, the namespace it exists in, and some other internal information.

We will see below how to use this token from within a simple Pod based on the following specification:apiVersion: v1kind: Podmetadata: name: pod-defaultspec: containers: – name: alpine image: alpine:3.

9 command: – "sleep" – "10000"Assuming this specification is in the pod-default.

yaml file, the creation of the Pod is done with the following (and standard) command:$ kubectl apply -f pod-default.

yamlAs no serviceAccountName key is specified, the default ServiceAccount of the Pod’s namespace is used.

We can confirm this by checking the specification of this Pod once created (Kubernetes adds a lot of things for us during the creation process).

$ kubectl get po/pod-default -o yamlapiVersion: v1kind: Podmetadata: name: pod-default namespace: default .

spec: serviceAccountName: default containers: — image: alpine:3.

9 command: — sleep — "10000" volumeMounts: — mountPath: /var/run/secrets/kubernetes.

io/serviceaccount name: default-token-dffkj volumes: — name: default-token-dffkj secret: secretName: default-token-dffkj.

An important thing to note here:the serviceAccountName key is set with the name of the default ServiceAccountthe information of the ServiceAccount is mounted inside the container of the Pod, through the usage of volume, in /var/run/secrets/kubernetes.

io/serviceaccount (more on that in a bit)Anonymous call of the API ServerLet’s run a shell within this container and install the curl utility:$ kubectl exec -ti pod-default — sh/ # apk add –update curlFrom this shell, we can try to get information from the API Server without authentication.

/ # curl https://kubernetes/api/v1 –insecureNote: as said above, from a Pod running in the cluster, the API Server can be reached using the kubernetes ClusterIP service.

We then get an error message as an unauthenticated user is not allowed to perform this request.

/ # curl https://kubernetes/api –insecure{ “kind”: “Status”, “apiVersion”: “v1”, “metadata”: {}, “status”: “Failure”, “message”: “forbidden: User ”system:anonymous” cannot get path ”/api””, “reason”: “Forbidden”, “details”: {}, “code”: 403}Let’s go one step further and try to issue the same query using the token of the default ServiceAccount.

Call using the ServiceAccount tokenFrom the alpine container, the token of the default ServiceAccount can be retrieved from /run/secrets/kubernetes.

io/serviceaccount/token (remember the volume / volumeMounts instructions we saw above).

Using this token, we can use it as a Bearer token to authenticate against the API Server:/ # TOKEN=$(cat /run/secrets/kubernetes.

io/serviceaccount/token)/ # curl -H “Authorization: Bearer $TOKEN” https://kubernetes/api/v1/ –insecureThis time the request goes fine, no more error querying this endpoint, the list of resources is returned.

{ "kind": "APIResourceList", "groupVersion": "v1", "resources": [ { "name": "bindings", "singularName": "", "namespaced": true, "kind": "Binding", "verbs": [ "create" ] }, .

]}Let’s now try something more ambitious, and use this token to list all the Pods within the default namespace:/ # curl -H “Authorization: Bearer $TOKEN” https://kubernetes/api/v1/namespaces/default/pods/ –insecure{ “kind”: “Status”, “apiVersion”: “v1”, “metadata”: {}, “status”: “Failure”, “message”: “pods is forbidden: User ”system:serviceaccount:default:default” cannot list resource ”pods” in API group ”” in the namespace ”default””, “reason”: “Forbidden”, “details”: { “kind”: “pods” }, “code”: 403}The default ServiceAccount do not have enough rights to perform this query.

In the following, we will create our own ServiceAccount and provide it the additional rights it needs for this action.

Using a custom ServiceAccountCreation of a ServiceAccountLet’s create a new ServiceAccount in the default namespace and call it demo-sa.

This ServiceAccount is defined in the following specification and created with the standard kubectl apply -f command.

apiVersion: v1kind: ServiceAccountmetadata: name: demo-saCreation of a RoleA ServiceAccount is not that useful unless some rights are bound to it.

Rights are known as Role or ClusterRole in Kubernetes.

They are associated to a ServiceAccount with RoleBinding and ClusterRoleBinding respectively.

A Role (the same applies to a ClusterRole) contains a list of rules.

Each rule defines some actions that can be performed (eg: list, get, watch, …) against a list of resources (eg: Pod, Service, Secret) within apiGroups (eg: core, apps/v1, …).

While a Role defines rights for a specific namespace, the scope of a ClusterRole is the entire clusterThe following specification defines a Role allowing to list all the Pods in the default namespace.

apiVersion: rbac.

authorization.

k8s.

io/v1beta1kind: Rolemetadata: name: list-pods namespace: defaultrules: — apiGroups: — ‘’ resources: — pods verbs: — listBinding the Role with the ServiceAccountIn the last step, we bind the Role and the ServiceAccount created above.

In order to do so, we define a RoleBinding with the following specification:apiVersion: rbac.

authorization.

k8s.

io/v1beta1kind: RoleBindingmetadata: name: list-pods_demo-sa namespace: defaultroleRef: kind: Role name: list-pods apiGroup: rbac.

authorization.

k8s.

iosubjects: — kind: ServiceAccount name: demo-sa namespace: defaultOnce the RoleBinding is created, the demo-sa ServiceAccount can list the Pods in the default namespace (this is the action defined under the rules key within the specification of the Role).

Let’s check this.

Using the ServiceAccount within a PodWe create a simple Pod from the following specification:apiVersion: v1kind: Podmetadata: name: pod-demo-saspec: serviceAccountName: demo-sa containers: — name: alpine image: alpine:3.

9 command: — "sleep" — "10000"The serviceAccountName key is specified and contains the name of the ServiceAccount used by that Pod, demo-sa.

As we saw above, if the serviceAccountName is not specified in the Pod specification, the default ServiceAccount of the namespace is used.

As we did with the pod-default Pod, we now run a shell within the alpine container of the Pod pod-demo-sa, get the token belonging to the demo-sa ServiceAccount and use it to query the list of Pods within the default namespace.

# Get the ServiceAccount token from within the Pod's container/ # TOKEN=$(cat /run/secrets/kubernetes.

io/serviceaccount/token)# Call an API Server's endpoint (using the ClusterIP kubernetes service) to get all the Pods running in the default namespace/ # curl -H “Authorization: Bearer $TOKEN” https://kubernetes/api/v1/namespaces/default/pods/ –insecureNo more error this time as the ServiceAccount has the rights to perform this action.

We get a list of Pod running in the default namespace.

{ "kind": "PodList", "apiVersion": "v1", "metadata": { "selfLink": "/api/v1/namespaces/default/pods/", "resourceVersion": "96296" }, "items": [ .

]}Main takeawaysBy default, each Pod can communicate with the API Server of the cluster it is running on.

If no ServiceAccount is specified, it uses the default ServiceAccount of its namespace.

As the default ServiceAccounts only have limited rights, it’s generally a best practice to create a ServiceAccount for each application, giving this one the rights it needs (no more).

To authenticate against the API Server a Pod uses the token of the attached ServiceAccount.

This token, is available in the filesystem of each container of the Pod.

In this example we used curl to query the HTTP endpoints of the API Server.

Real applications would obviously use dedicated libraries, several of them are available in different languages.

.

. More details

Leave a Reply