Lesson 05 ServiceAccounts & RBAC
Theme:

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

Cluster requirement: RBAC must be enabled on your cluster. All modern Kubernetes distributions enable RBAC by default. You can verify with: 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 }}
Tip: If the default 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 }}
Important: Remove or comment out any 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
Expected result: Without the Role and RoleBinding, the ServiceAccount has no permissions. The pod can still run, but any API call from inside the pod will be denied.

Task 3.3 — Clean up

# Uninstall the release
helm uninstall configreader -n rbac-lab

# Delete the namespace
kubectl delete namespace rbac-lab

Completion Checklist