Lab Guide — ServiceAccounts & RBAC
Create a Helm chart with ServiceAccount and RBAC templates, deploy an app that reads ConfigMaps, and verify least-privilege enforcement.
Prerequisites
- A running Kubernetes cluster (minikube, kind, or cloud-based)
- Helm 3 installed
kubectlconfigured and connected to your cluster- Completed the Theory reading for this lesson
kubectl api-versions | grep rbac
Exercise 1 — Create the Chart Scaffold
Task 1.1 — Generate the chart
Create a new Helm chart and remove the default templates we will replace:
# Create the chart
helm create configreader
# Remove default templates we'll replace
rm configreader/templates/hpa.yaml
rm configreader/templates/ingress.yaml
Checkpoint: You should have a configreader/ directory with Chart.yaml, values.yaml, and a templates/ folder.
Task 1.2 — Configure values.yaml
Replace the contents of configreader/values.yaml with the following. Pay attention to the ServiceAccount and RBAC sections:
# configreader/values.yaml
replicaCount: 1
image:
repository: bitnami/kubectl
tag: latest
pullPolicy: IfNotPresent
serviceAccount:
create: true
name: ""
automount: true # This app needs API access
annotations: {}
imagePullSecrets: []
rbac:
create: true
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch"]
service:
enabled: false
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
Checkpoint: serviceAccount.automount is true because our app reads from the Kubernetes API. rbac.create is true so the chart generates Role and RoleBinding.
Task 1.3 — Create the ServiceAccount template
Create configreader/templates/serviceaccount.yaml:
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "configreader.serviceAccountName" . }}
labels:
{{- include "configreader.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- if .Values.serviceAccount.imagePullSecrets }}
imagePullSecrets:
{{- toYaml .Values.serviceAccount.imagePullSecrets | nindent 2 }}
{{- end }}
{{- end }}
Task 1.4 — Add the helper function
Open configreader/templates/_helpers.tpl and add the following at the end of the file:
{{- define "configreader.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "configreader.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
helm create scaffold already includes a serviceAccountName helper, replace it with the one above (or verify they match).
Task 1.5 — Create the Role template
Create configreader/templates/role.yaml:
{{- if .Values.rbac.create -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "configreader.fullname" . }}
labels:
{{- include "configreader.labels" . | nindent 4 }}
rules:
{{- toYaml .Values.rbac.rules | nindent 2 }}
{{- end }}
Task 1.6 — Create the RoleBinding template
Create configreader/templates/rolebinding.yaml:
{{- if .Values.rbac.create -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "configreader.fullname" . }}
labels:
{{- include "configreader.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "configreader.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "configreader.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
{{- end }}
Task 1.7 — Validate the templates
Use helm template to render and inspect the output without deploying:
# Render the chart locally
helm template my-release configreader/
# Check for errors
helm lint configreader/
Checkpoint: The output should include a ServiceAccount, Role, and RoleBinding. helm lint should report no errors.
Exercise 2 — Deploy and Test API Access
Task 2.1 — Create a test ConfigMap
Before deploying the chart, create a ConfigMap that our app will read:
# Create a namespace for this lab
kubectl create namespace rbac-lab
# Create a test ConfigMap
kubectl create configmap app-config \
--from-literal=DATABASE_HOST=postgres.svc \
--from-literal=LOG_LEVEL=info \
-n rbac-lab
Task 2.2 — Update the Deployment template
Modify configreader/templates/deployment.yaml to use a long-running command that we can exec into. Find the containers section and ensure serviceAccountName is set and the container runs a sleep command:
# In the pod spec (spec.template.spec), ensure:
serviceAccountName: {{ include "configreader.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["sleep", "3600"]
resources:
{{- toYaml .Values.resources | nindent 12 }}
livenessProbe and readinessProbe sections from the deployment template, since our sleep container does not serve HTTP.
Task 2.3 — Install the chart
# Install into the rbac-lab namespace
helm install configreader configreader/ -n rbac-lab
# Verify all resources were created
kubectl get serviceaccount,role,rolebinding -n rbac-lab
kubectl get pods -n rbac-lab
Checkpoint: You should see the ServiceAccount, Role, RoleBinding, and a running pod.
Task 2.4 — Test allowed operations
Exec into the pod and test reading ConfigMaps (which should be allowed):
# Get the pod name
POD=$(kubectl get pod -n rbac-lab -l app.kubernetes.io/name=configreader -o jsonpath='{.items[0].metadata.name}')
# Exec into the pod and try reading ConfigMaps
kubectl exec -n rbac-lab $POD -- kubectl get configmaps -n rbac-lab
kubectl exec -n rbac-lab $POD -- kubectl get configmap app-config -n rbac-lab -o yaml
Checkpoint: Both commands should succeed, showing the app-config ConfigMap and its data.
Task 2.5 — Test denied operations
Now test operations that should be denied by RBAC:
# Try to list pods (not in our Role rules) - should be DENIED
kubectl exec -n rbac-lab $POD -- kubectl get pods -n rbac-lab
# Try to delete a ConfigMap (we only have get/list/watch) - should be DENIED
kubectl exec -n rbac-lab $POD -- kubectl delete configmap app-config -n rbac-lab
# Try to access resources in a different namespace - should be DENIED
kubectl exec -n rbac-lab $POD -- kubectl get configmaps -n default
Checkpoint: All three commands should fail with a "Forbidden" error, confirming least-privilege enforcement.
Exercise 3 — Verify with kubectl auth can-i
Task 3.1 — Audit ServiceAccount permissions
Use kubectl auth can-i to verify exactly what the ServiceAccount is allowed to do:
# Check if the SA can get configmaps (should be "yes")
kubectl auth can-i get configmaps \
--as=system:serviceaccount:rbac-lab:configreader \
-n rbac-lab
# Check if the SA can delete configmaps (should be "no")
kubectl auth can-i delete configmaps \
--as=system:serviceaccount:rbac-lab:configreader \
-n rbac-lab
# Check if the SA can get pods (should be "no")
kubectl auth can-i get pods \
--as=system:serviceaccount:rbac-lab:configreader \
-n rbac-lab
# List all permissions for this ServiceAccount
kubectl auth can-i --list \
--as=system:serviceaccount:rbac-lab:configreader \
-n rbac-lab
Checkpoint: The --list output should show only configmaps and secrets with get, list, watch verbs.
Task 3.2 — Test with RBAC disabled
Upgrade the release with rbac.create=false and observe the difference:
# Upgrade with RBAC disabled
helm upgrade configreader configreader/ -n rbac-lab --set rbac.create=false
# Verify Role and RoleBinding are removed
kubectl get role,rolebinding -n rbac-lab
# The pod still runs but API calls should now fail
POD=$(kubectl get pod -n rbac-lab -l app.kubernetes.io/name=configreader -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n rbac-lab $POD -- kubectl get configmaps -n rbac-lab
Task 3.3 — Clean up
# Uninstall the release
helm uninstall configreader -n rbac-lab
# Delete the namespace
kubectl delete namespace rbac-lab
Completion Checklist
- Chart created with ServiceAccount, Role, and RoleBinding templates
- values.yaml configured with serviceAccount and rbac sections
- Helper template for serviceAccountName added
- Chart deployed and pod confirmed running
- ConfigMap read successfully from inside the pod
- Denied operations confirmed (pods, delete, cross-namespace)
- kubectl auth can-i used to audit permissions
- RBAC disable/enable toggle tested via Helm upgrade