Skip to main content
mastering ckad certified kubernetes application developer

ServiceAccounts and RBAC Basics

9 min read Chapter 51 of 87
Summary

Covers ServiceAccount creation and assignment to Pods, the...

Covers ServiceAccount creation and assignment to Pods, the default ServiceAccount and its risks, disabling auto-mounted tokens, RBAC concepts within CKAD scope (Role vs ClusterRole, RoleBinding), imperative creation of Roles and RoleBindings, testing permissions with kubectl auth can-i, and a complete YAML example tying Role, RoleBinding, and Pod together.

ServiceAccounts and RBAC Basics

Every process that talks to the Kubernetes API server must authenticate. Humans authenticate with certificates, tokens, or OIDC. Pods authenticate with ServiceAccounts — Kubernetes-native identities that are automatically projected into containers as bearer tokens.

Understanding ServiceAccounts is not optional for the CKAD. The exam tests whether you can create a ServiceAccount, assign it to a Pod, and — critically — configure RBAC rules that grant or restrict what that ServiceAccount is authorized to do.

What Is a ServiceAccount?

A ServiceAccount is a namespaced Kubernetes object that provides an identity for Pods. When a Pod starts, the kubelet mounts a token file at /var/run/secrets/kubernetes.io/serviceaccount/token. This token identifies the Pod’s ServiceAccount to the API server.

kubectl exec <pod> -- cat /var/run/secrets/kubernetes.io/serviceaccount/token

This prints a JWT (JSON Web Token) that the API server validates on every request. The token is bound to a specific ServiceAccount in a specific namespace.

The Default ServiceAccount

Every namespace includes a default ServiceAccount, created automatically:

kubectl get serviceaccount -n default
NAME      SECRETS   AGE
default   0         14d

When you create a Pod without specifying serviceAccountName, Kubernetes assigns the default ServiceAccount. The kubelet mounts a token for this ServiceAccount into the container.

Why this matters: the default ServiceAccount has minimal permissions in a properly configured cluster. But in many environments, especially development clusters, it may have broader access than intended. More importantly, any Pod in the namespace shares the same identity — a compromised application Pod has the same API access as every other Pod using default.

The best practice is to create dedicated ServiceAccounts for each application and assign only the permissions it needs.

Creating a ServiceAccount

kubectl create serviceaccount my-sa

Verify:

kubectl get serviceaccount my-sa -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-sa
  namespace: default

ServiceAccounts are namespaced. A ServiceAccount named my-sa in namespace dev is entirely separate from one named my-sa in namespace prod.

Assigning a ServiceAccount to a Pod

Set the serviceAccountName field in the Pod spec:

apiVersion: v1
kind: Pod
metadata:
  name: sa-demo
spec:
  serviceAccountName: my-sa
  containers:
    - name: app
      image: curlimages/curl
      command: ["sh", "-c", "sleep 3600"]

The kubelet mounts a token for my-sa into the container. Verify:

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

You can also verify which ServiceAccount the Pod is using:

kubectl get pod sa-demo -o jsonpath='{.spec.serviceAccountName}'
# my-sa

Important: serviceAccountName is immutable — you cannot change it on a running Pod. To switch ServiceAccounts, delete and recreate the Pod.

Disabling Automatic Token Mounting

Some Pods never need to talk to the API server. A frontend web server, a batch processing job, a machine learning inference container — none of these need a Kubernetes API token. Mounting one creates an unnecessary attack surface.

Disable auto-mounting at the Pod level:

apiVersion: v1
kind: Pod
metadata:
  name: no-token-pod
spec:
  serviceAccountName: my-sa
  automountServiceAccountToken: false
  containers:
    - name: app
      image: nginx:1.25

Or at the ServiceAccount level (applies to all Pods using this SA):

apiVersion: v1
kind: ServiceAccount
metadata:
  name: no-api-sa
automountServiceAccountToken: false

When both are set, the Pod-level setting takes precedence. If you set automountServiceAccountToken: true on the Pod and false on the ServiceAccount, the token is mounted.

Verify no token is mounted:

kubectl exec no-token-pod -- ls /var/run/secrets/kubernetes.io/serviceaccount/
# ls: /var/run/secrets/kubernetes.io/serviceaccount/: No such file or directory

RBAC: Role-Based Access Control

RBAC controls what a ServiceAccount (or user) is authorized to do. The model has four components:

ObjectScopePurpose
RoleNamespaceDefines permissions within a single namespace
ClusterRoleClusterDefines permissions cluster-wide
RoleBindingNamespaceBinds a Role (or ClusterRole) to a subject in a namespace
ClusterRoleBindingClusterBinds a ClusterRole to a subject cluster-wide

For the CKAD, you primarily need Role and RoleBinding. ClusterRole appears when permissions need to span namespaces, but the exam scope focuses on namespace-level access control.

Creating a Role

A Role defines a set of permissions — which API resources can be accessed and which verbs (actions) are allowed:

kubectl create role pod-reader \
  --verb=get,list,watch \
  --resource=pods

This creates a Role that allows reading Pods (get, list, watch) in the current namespace.

Inspect the result:

kubectl get role pod-reader -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: default
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]

Key fields:

  • apiGroups: "" means the core API group (Pods, Services, ConfigMaps, Secrets). For Deployments, the apiGroup is apps. For Ingress, it is networking.k8s.io.
  • resources: Which resource types the Role applies to.
  • verbs: Which operations are allowed. Common verbs: get, list, watch, create, update, patch, delete.

A Role with multiple rules:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-manager
  namespace: default
rules:
  - apiGroups: [""]
    resources: ["pods", "services", "configmaps"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "create", "update"]

Creating a RoleBinding

A RoleBinding attaches a Role to a subject — a ServiceAccount, user, or group:

kubectl create rolebinding pod-reader-binding \
  --role=pod-reader \
  --serviceaccount=default:my-sa

The format for --serviceaccount is namespace:name. This binding grants the my-sa ServiceAccount in the default namespace the permissions defined in the pod-reader Role.

Inspect the result:

kubectl get rolebinding pod-reader-binding -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: pod-reader-binding
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: pod-reader
subjects:
  - kind: ServiceAccount
    name: my-sa
    namespace: default

Important: roleRef is immutable. You cannot change which Role a RoleBinding references after creation. To bind to a different Role, delete the RoleBinding and create a new one.

Testing Permissions

The kubectl auth can-i command checks whether a subject is authorized for a specific action:

# Can my-sa get pods?
kubectl auth can-i get pods \
  --as=system:serviceaccount:default:my-sa
# yes

# Can my-sa delete pods?
kubectl auth can-i delete pods \
  --as=system:serviceaccount:default:my-sa
# no

# Can my-sa create deployments?
kubectl auth can-i create deployments \
  --as=system:serviceaccount:default:my-sa
# no

# List all allowed actions for my-sa
kubectl auth can-i --list \
  --as=system:serviceaccount:default:my-sa

The --as flag impersonates the specified identity. The format for ServiceAccounts is system:serviceaccount:<namespace>:<name>.

This command is invaluable on the exam. After creating a Role and RoleBinding, verify with can-i before moving on. It takes five seconds and catches misconfiguration immediately.

Complete YAML: Role + RoleBinding + Pod with ServiceAccount

This example creates a pipeline: a ServiceAccount with read-only access to Pods and ConfigMaps, bound through RBAC, and assigned to a Pod.

ServiceAccount:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: monitoring-sa
  namespace: default

Role:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: monitoring-role
  namespace: default
rules:
  - apiGroups: [""]
    resources: ["pods", "configmaps"]
    verbs: ["get", "list", "watch"]

RoleBinding:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: monitoring-binding
  namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: monitoring-role
subjects:
  - kind: ServiceAccount
    name: monitoring-sa
    namespace: default

Pod:

apiVersion: v1
kind: Pod
metadata:
  name: monitoring-pod
spec:
  serviceAccountName: monitoring-sa
  containers:
    - name: monitor
      image: curlimages/curl
      command: ["sh", "-c", "sleep 3600"]

Apply all resources:

kubectl apply -f serviceaccount.yaml
kubectl apply -f role.yaml
kubectl apply -f rolebinding.yaml
kubectl apply -f pod.yaml

Or combine all four into a single file separated by ---:

kubectl apply -f monitoring-rbac.yaml

Verify permissions:

# From outside the Pod — impersonation
kubectl auth can-i get pods --as=system:serviceaccount:default:monitoring-sa
# yes

kubectl auth can-i delete pods --as=system:serviceaccount:default:monitoring-sa
# no

kubectl auth can-i get secrets --as=system:serviceaccount:default:monitoring-sa
# no

Verify from inside the Pod:

kubectl exec monitoring-pod -- sh -c '
  TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
  APISERVER=https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}
  
  # Should succeed — get pods is allowed
  curl -s --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
    -H "Authorization: Bearer ${TOKEN}" \
    ${APISERVER}/api/v1/namespaces/default/pods | head -c 200
  
  echo ""
  
  # Should fail — delete pods is not allowed
  curl -s --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
    -H "Authorization: Bearer ${TOKEN}" \
    -X DELETE \
    ${APISERVER}/api/v1/namespaces/default/pods/monitoring-pod | head -c 200
'

The first request succeeds (returns Pod JSON). The second returns a 403 Forbidden error.

ClusterRole and ClusterRoleBinding

A ClusterRole defines permissions across all namespaces (or for cluster-scoped resources like nodes):

kubectl create clusterrole node-reader \
  --verb=get,list \
  --resource=nodes

A ClusterRoleBinding binds a ClusterRole to a subject for the entire cluster:

kubectl create clusterrolebinding node-reader-binding \
  --clusterrole=node-reader \
  --serviceaccount=default:monitoring-sa

You can also use a RoleBinding to bind a ClusterRole within a single namespace — the ClusterRole’s permissions are then scoped to that namespace:

kubectl create rolebinding pod-reader-binding \
  --clusterrole=pod-reader-cluster \
  --serviceaccount=default:my-sa \
  -n dev

This grants my-sa the pod-reader-cluster ClusterRole’s permissions, but only in the dev namespace.

Exam Strategy

RBAC tasks on the CKAD follow a predictable pattern:

  1. Create a ServiceAccount: kubectl create serviceaccount <name>
  2. Create a Role: kubectl create role <name> --verb=<verbs> --resource=<resources>
  3. Create a RoleBinding: kubectl create rolebinding <name> --role=<role> --serviceaccount=<ns>:<sa>
  4. Assign the SA to a Pod: Set serviceAccountName in the Pod spec.
  5. Verify: kubectl auth can-i <verb> <resource> --as=system:serviceaccount:<ns>:<sa>

All five steps use imperative commands. Writing RBAC YAML from scratch is slow and error-prone on exam day. Use kubectl create to generate the resources, then modify the generated YAML only if necessary.

Common mistakes to avoid:

  • Forgetting the namespace:name format in --serviceaccount (e.g., default:my-sa, not my-sa)
  • Using --as=my-sa instead of --as=system:serviceaccount:default:my-sa in can-i
  • Placing serviceAccountName inside containers[] instead of at spec level
  • Confusing Role (namespace) and ClusterRole (cluster) scope
  • Creating the RoleBinding in the wrong namespace — it must be in the same namespace as the Role and the Pods you want to grant access to