RBAC from Source Annotations
Automating Kubernetes RBAC generation from source code annotations ensures that a controller's permissions are the ground truth. This prevents drift between what developers request and what is actually deployed. This practice codifies permissions alongside the controller logic that requires them, creating a single, verifiable source for the operator's ClusterRole.
Manual RBAC Drift Causes Incidents
Manually maintaining RBAC policies in deployment charts separate from the controller's code is a common source of configuration drift. A controller's permission requirements, declared in source code via annotations, can easily diverge from the hand-edited YAML, leading to failed deployments or service outages when a permission is missing. A past incident involving a dependency-management controller was traced directly to this gap: a code change requiring new permissions was not manually mirrored in the deployment chart.
Defining Roles in Source¶
The source of truth for all RBAC permissions should be +kubebuilder:rbac
markers directly in the controller's source code. These annotations live next
to the code that needs the permissions, making them easy to review and maintain.
The controller-gen tool from the Kubebuilder framework parses these markers
to generate RBAC manifests.
Each marker specifies the API group, resource, and verbs required. The reconciler
for the StateRun custom resource, for example, requires permissions to interact
with multiple custom resources and core Kubernetes types.
A typical set of markers might look like this:
//+kubebuilder:rbac:groups=testing.acme.org,resources=testruns,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=testing.acme.org,resources=testruns/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=testing.acme.org,resources=testdefinitions,verbs=get;list;watch
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
The Generation Process¶
The process of turning annotations into a concrete RBAC manifest is handled
by a build-system target. By adding controller-gen to the standard
make manifests command, the RBAC role.yaml file is generated in the same
step as Custom Resource Definitions (CRDs).
The command controller-gen rbac:roleName=manager-role is configured to output
the generated rules into a temporary file, such as config/rbac/role.yaml. This
file serves as the canonical, machine-generated definition of the controller's
required permissions. It should not be edited by hand; instead, any changes must
be made to the +kubebuilder:rbac markers in the source code.
This generated file is an intermediate artifact, not the final deployment
manifest. It is typically excluded from Git tracking in the .gitignore file,
similar to other generated CRD bases.
Synchronizing with Deployment Manifests¶
To prevent drift, the generated RBAC rules must be synchronized with the
ClusterRole manifest used for deployment, which is often a Helm chart
template. A custom shell script handles this synchronization by replacing a
designated section within the chart's clusterrole.yaml template.
The script uses markers to identify the auto-managed block of rules:
# clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ .Release.Name }}-manager-role
rules:
# BEGIN GENERATED RBAC
# The content in this block is automatically generated. Do not edit manually.
- apiGroups:
- testing.acme.org
resources:
- testruns
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
# END GENERATED RBAC
The sync script reads the rules from the intermediate config/rbac/role.yaml
and injects them between the BEGIN and END markers. This approach allows
the ClusterRole to contain both generated rules and a small number of manually
managed rules that may exist outside the marked block for special-casing or
conditional logic.
Integrating into Pre-Commit Hooks¶
The entire process of generation and synchronization is automated and enforced
through a pre-commit hook. The same hook that checks for stale auto-generated Go
code (zz_generated.deepcopy.go) is configured to run make manifests.
If a developer adds or changes an RBAC annotation but forgets to run
make manifests, the pre-commit hook will detect that the clusterrole.yaml
in the Helm chart is out of sync with the generated role.yaml. The commit is
blocked until the developer runs the make target and stages the updated chart,
ensuring that RBAC changes are always atomically committed with the code that
requires them.
Handling Complex and Manual Rules¶
Initially, some rules may exist in the ClusterRole that are not backed by a
+kubebuilder:rbac marker. The synchronization script's marker-based approach
allows these to be preserved outside the generated block. However, the best
practice is to resolve these discrepancies by adding the corresponding
annotations to the source code, making the generated block the complete and
exclusive source of rules.
In some cases, rules may be conditional based on Helm values. For example, a
rule granting access to secrets and services was initially gated on a
targetNamespaces value. During an early "prove usage" phase, this conditional
logic was removed in favor of an unconditional cluster-wide grant to simplify
testing and validation. The annotation was made unconditional, and the sync
script absorbed the rule into the main generated block, demonstrating a
deliberate choice to trade least-privilege for implementation velocity, with the
intent to revisit later.
Verification and Testing¶
Automated RBAC generation impacts testing. controller-gen outputs resources
and verbs in alphabetical order. Any integration tests that assert on the exact
YAML content or rule order within the ClusterRole may need to be updated to
match the machine-generated format. Tests for RBAC should verify the presence
of required permissions rather than the exact textual representation of the rule
block. For example, a test that previously checked for get;list;watch might
fail if the generator outputs get;list;watch, but would pass if it correctly
checks for each verb individually.