Helm’s templating engine can generate RBAC roles and ServiceAccounts, but the real magic isn’t just generating YAML; it’s dynamically scoping permissions based on your deployment’s needs.
Let’s see it in action. Imagine you have a Helm chart for a web application that needs to read secrets from Kubernetes.
# templates/rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "my-app.fullname" . }}-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "my-app.fullname" . }}-reader-binding
subjects:
- kind: ServiceAccount
name: {{ include "my-app.fullname" . }}
namespace: {{ .Release.Namespace }}
roleRef:
kind: ClusterRole
name: {{ include "my-app.fullname" . }}-reader
apiGroup: rbac.authorization.k8s.io
When you helm install my-release ./my-app-chart, Helm processes this template. If your chart’s name is my-app and the release name is my-release, the generated resources would look something like this:
# Generated YAML
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: my-release-my-app-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-release-my-app
labels:
app.kubernetes.io/name: my-app
app.kubernetes.io/instance: my-release
app.kubernetes.io/version: "1.16.0" # Example version
app.kubernetes.io/component: frontend
app.kubernetes.io/part-of: my-app
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: my-release-my-app-reader-binding
subjects:
- kind: ServiceAccount
name: my-release-my-app
namespace: default # Or whatever namespace you installed into
roleRef:
kind: ClusterRole
name: my-release-my-app-reader
apiGroup: rbac.authorization.k8s.io
This setup provides a dedicated ServiceAccount (my-release-my-app) for your application pods and binds it to a ClusterRole (my-release-my-app-reader) that grants read-only access to secrets cluster-wide. This is a common pattern for applications that need to fetch configuration or sensitive data securely.
The {{ include "my-app.fullname" . }} helper is crucial. It generates a unique name for your resources based on the release name and chart name, preventing naming collisions when deploying multiple instances of the same chart or different charts with similar resource names. The {{ .Release.Namespace }} ensures the ServiceAccount is correctly referenced within its own namespace.
You can make this even more dynamic. Suppose your application has different deployment tiers (e.g., read-only, read-write, admin). You can use values.yaml to control which RBAC resources are generated and what permissions they have.
# values.yaml
rbac:
create: true
readSecrets: true
writeSecrets: false
serviceAccount:
create: true
And in your templates/rbac.yaml:
{{- if .Values.rbac.create }}
{{- if .Values.rbac.readSecrets }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "my-app.fullname" . }}-secret-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "my-app.fullname" . }}-secret-reader-binding
subjects:
- kind: ServiceAccount
name: {{ include "my-app.fullname" . }}
namespace: {{ .Release.Namespace }}
roleRef:
kind: ClusterRole
name: {{ include "my-app.fullname" . }}-secret-reader
apiGroup: rbac.authorization.k8s.io
{{- end }}
{{- if .Values.rbac.writeSecrets }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "my-app.fullname" . }}-secret-writer
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "my-app.fullname" . }}-secret-writer-binding
subjects:
- kind: ServiceAccount
name: {{ include "my-app.fullname" . }}
namespace: {{ .Release.Namespace }}
roleRef:
kind: ClusterRole
name: {{ include "my-app.fullname" . }}-secret-writer
apiGroup: rbac.authorization.k8s.io
{{- end }}
{{- end }}
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
{{- end }}
With this, you can install with helm install my-release ./my-app-chart --set rbac.writeSecrets=true to grant write access, or omit rbac.create=true entirely if your application doesn’t need any special permissions and will use the default ServiceAccount.
The truly powerful aspect is how Helm’s templating allows you to define granular RBAC policies that adapt to your application’s runtime requirements. You’re not just writing static YAML; you’re defining a policy generation engine. This means you can create charts that automatically provision the exact permissions needed for a given deployment, adhering to the principle of least privilege without manual intervention. This is particularly useful in multi-tenant environments or for complex microservice architectures where precise control over inter-service communication and data access is paramount. You can even use Helm’s lookup function (though it’s often discouraged for production due to performance and dependency issues) to dynamically fetch information from the cluster and influence RBAC generation, though usually, this is handled by external controllers or admission webhooks.
When you define rules for a Role or ClusterRole, the apiGroups field is a list of API groups the rule applies to. An empty string "" in apiGroups signifies the core Kubernetes API group, which includes resources like Pods, Services, and Secrets. This is a common point of confusion: forgetting the "" means your rule won’t match core resources.