Skip to main content

Service Accounts

Review cka service accounts.

What we need to include:

It's not possible to create a pod with a service account that doesn't exist.

It's a best practice that each application creates a service account even if it doesn't make a binding with any role or cluster role. This SA can be used in applications that don't need cluster access.

If we don't define the serviceAccountName in the pod, the default SA will be used. Imagine that all pods in the same namespace are using the default service account; if one of them needs some higher permission in this service account, all the other pods will have the same permission unnecessarily.

Another detail we should pay attention to is that most applications don't need access to the Kubernetes API, and the SA is exactly for that. In this case, we can prevent the token from being mounted, completely cutting off access.

kubectl run nginx --image=nginx -o yaml --dry-run=client
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: nginx
name: nginx
spec:
containers:
- image: nginx
name: nginx
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}

kubectl run nginx --image=nginx -o yaml --dry-run=client > pod-nginx.yaml

k apply -f pod-nginx.yaml

# Let's go inside the pod to check some details
k exec -it nginx -- bash

root@nginx:/# cd /run/secrets/kubernetes.io/serviceaccount/
root@nginx:/run/secrets/kubernetes.io/serviceaccount# ls
ca.crt namespace token
root@nginx:/run/secrets/kubernetes.io/serviceaccount# cat namespace
default

root@nginx:/run/secrets/kubernetes.io/serviceaccount# cat token
eyJhbGciOiJSUzI1NiIsImtpZCI6InNOUENuTnQxUVVoYjBrb0NTWXJMdE14ZW4xSzd1VHYtVGpwYWNRV1JYcncifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzU1ODA4NDA0LCJpYXQiOjE3MjQyNzI0MDQsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiZjI3NzUzM2EtYWRhNi00M2EzLWI2YWUtYjY5YTk1MTJjYWU0Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoicGVyc29uYWwtY2x1c3Rlci13b3JrZXIzIiwidWlkIjoiYWIwOWU2YzAtMDg5NC00ZjkwLWJlMGQtYzIzYTc5NzA5N2FkIn0sInBvZCI6eyJuYW1lIjoibmdpbngiLCJ1aWQiOiI2Njk2Y2IzMy01YzlmLTQzYmUtYjRhYS1kYjAxZmIwOTg2YjUifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiJhYjAyODA5ZS0yNDAxLTQ2ZmYtYTNjNi0wMzc4OTdkMGQxZmUifSwid2FybmFmdGVyIjoxNzI0Mjc2MDExfSwibmJmIjoxNzI0MjcyNDA0LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0.WgmNnF2dblSDjnlbH9j0MuzRu4yX1XKj4Q3wTlkQi-r482HNT-vsyHmmtJwy_6XLEFgMeNe0NSizhrRpgluYPdERwQcyaX8Y_GX0GIM0eGaiGm0qHS31oK115Ch_7SaGHPRdNv-cNQutQz1R-8ptTXEnxRNK2a8furX-mWPBXMdQkTrgObOBWnmP-O0lsiCBqCFDQmw6qXTIUby-7ipg3M82xBp8JP_ml-nOpFI5cMoq2SKcaryOk2t8mWT1KdsrROmtuQrEqcK_eBD6KY2Sm84VrB_XWHdUJ3jikn5utpIkiqzWjFntHoos5asVxNwvH-R_Y2_xKqGzxYsCU9b1pg

root@nginx:/run/secrets/kubernetes.io/serviceaccount# env | grep KUBE
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT_443_TCP=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
KUBERNETES_SERVICE_HOST=10.96.0.1
KUBERNETES_PORT=tcp://10.96.0.1:443
KUBERNETES_PORT_443_TCP_PORT=443

# Let's try to reach the API, but we have a problem, we're authenticating as system:anonymous.
root@nginx:/run/secrets/kubernetes.io/serviceaccount# curl https://$KUBERNETES_SERVICE_HOST -k
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {},
"code": 403
}

# Let's pass the token. This token being the default service account, we'll authenticate as system:serviceaccount:default:default
root@nginx:/run/secrets/kubernetes.io/serviceaccount# curl https://$KUBERNETES_SERVICE_HOST -k -H "Authorization: Bearer $(cat token)"
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:serviceaccount:default:default\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {},
"code": 403
}
root@nginx:/run/secrets/kubernetes.io/serviceaccount# curl https://$KUBERNETES_SERVICE_HOST/version -k -H "Authorization: Bearer $(cat token)"
{
"major": "1",
"minor": "30",
"gitVersion": "v1.30.0",
"gitCommit": "7c48c2bd72b9bf5c44d21d7338cc7bea77d0ad2a",
"gitTreeState": "clean",
"buildDate": "2024-05-13T22:00:36Z",
"goVersion": "go1.22.2",
"compiler": "gc",
"platform": "linux/amd64"
}
exit

# And just to show that the volumes were mounted

❯ k get pod nginx -o yaml
apiVersion: v1
kind: Pod
metadata:
...
namespace: default
resourceVersion: "11254882"
uid: 08243b7c-1208-49a7-affc-58348a481cb6
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: nginx
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount #<<<<
name: kube-api-access-mmcdn
readOnly: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
nodeName: personal-cluster-worker3
preemptionPolicy: PreemptLowerPriority
priority: 0
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: default
serviceAccountName: default
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
# Here we have the volumes with the files
volumes:
- name: kube-api-access-mmcdn
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 #<<<<

Usually a pod doesn't need to access the API unless it's part of an operator that will work with cluster resources.

Now let's create the same pod with a specific service account and remove the token automount. Edit the pod-nginx.yaml file.

apiVersion: v1
kind: Pod
metadata:
labels:
run: nginx
name: nginx
spec:
# We define the service account and disable automount
serviceAccountName: accessor # a service account that already exists in the namespace
automountServiceAccountToken: false
containers:
- image: nginx
name: nginx
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
❯ k replace -f pod-nginx.yaml --force
pod "nginx" deleted
pod/nginx replaced

# Didn't mount even the secrets folder
k exec -it nginx -- bash
root@nginx:/# ls /run/
lock/ nginx.pid
exit

# And we can verify that it didn't mount any volume

❯ k get pod nginx -o yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: "2024-08-21T20:55:24Z"
labels:
run: nginx
name: nginx
namespace: default
resourceVersion: "11253497"
uid: 362c77fb-e4dc-4195-9287-0fa3fcabf4d7
spec:
automountServiceAccountToken: false
containers:
- image: nginx
imagePullPolicy: Always
name: nginx
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
enableServiceLinks: true
nodeName: personal-cluster-worker3
preemptionPolicy: PreemptLowerPriority
priority: 0
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: accessor # Service account used
serviceAccountName: accessor
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2024-08-21T20:55:26Z"
status: "True"
type: PodReadyToStartContainers
- lastProbeTime: null
lastTransitionTime: "2024-08-21T20:55:24Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2024-08-21T20:55:26Z"
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2024-08-21T20:55:26Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2024-08-21T20:55:24Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://747371ca8c09824d1689d89cee6c494431eb6bf6cbaa68bfb11daae12b8c8c70
image: docker.io/library/nginx:latest
imageID: docker.io/library/nginx@sha256:447a8665cc1dab95b1ca778e162215839ccbb9189104c79d7ec3a81e14577add
lastState: {}
name: nginx
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2024-08-21T20:55:25Z"
hostIP: 172.18.0.4
hostIPs:
- ip: 172.18.0.4
phase: Running
podIP: 10.32.0.11
podIPs:
- ip: 10.32.0.11
qosClass: BestEffort
startTime: "2024-08-21T20:55:24Z"