Skip to content

RBAC and namespaces

What every runner gets

The permissions every kubernetes executor needs (pods and pods/exec, pods/attach, pods/log, services, secrets, configmaps, serviceaccounts and events) live in one shared ClusterRole, gitlab-runner-operator-executor, reconciled by the operator.

For each Runner or MultiRunner the operator then provisions:

  • its own ServiceAccount, so each runner is a distinct identity for audit and revocation;
  • a RoleBinding tying that ServiceAccount to the shared ClusterRole.

A MultiRunner shares a single ServiceAccount across all of its entries. Because the rules live in one ClusterRole, a permission change in a new operator version applies to every runner at once.

Optional grants

Permissions only some runners need are not in that role. Each optional grant has its own ClusterRole and its own RoleBinding, created only where a spec asks for it, so enabling one does not hand out the others.

Today there is one: pod_disruption_budget needs policy/poddisruptionbudgets, held in gitlab-runner-operator-executor-pdb and bound by a RoleBinding named pdb-<child-name>. The grant is per build namespace, so a MultiRunner that sets the flag on one entry does not widen it into the namespaces its other entries target. Turning the flag off deletes the binding on the next reconcile.

Upgrading from a version that granted this unconditionally

Upgrading from an operator version that granted poddisruptionbudgets to everything revokes it fleet-wide as soon as the first runner reconciles. Each runner that still wants it regains it on its own next reconcile. A job starting in that gap fails with poddisruptionbudgets is forbidden.

The ceiling

The operator can only grant a runner what the operator itself holds: it has no RBAC escalate verb. So the manager ClusterRole is the explicit ceiling for runner permissions.

RoleBindings to the ClusterRole are namespaced, so the effective grant is confined to the build namespace. Nothing cluster-scoped is ever granted to a runner.

Where jobs run

Job pods run in executor_config.namespace when it is set, otherwise in the runner's own namespace.

By default a runner may only target its own namespace. A Runner author picking an arbitrary namespace would otherwise have the operator bind their ServiceAccount, and run their jobs, in somewhere like kube-system.

To permit specific build namespaces, start the operator with:

--allowed-build-namespaces=ns-a,ns-b

or --allowed-build-namespaces=* to allow any. With the Helm chart, set allowedBuildNamespaces.

The reconciler refuses any other executor_config.namespace: the runner goes NotReady with an error, no RBAC is provisioned, and any binding previously created for a now-disallowed namespace is revoked. When an allowed build namespace differs from the runner's, the operator creates the RoleBinding there too, while the ServiceAccount stays in the runner's namespace, and removes it when the runner is deleted.

Keep the allow-list tight

The allow-list is enforced by the reconciler, the component that actually provisions the RBAC, so it cannot be turned off by a flag. The operator holds the executor permission set cluster-wide, so on a shared cluster an unrestricted namespace is a privilege-escalation path: a Runner author could reach kube-system or another tenant's namespace.

Restrict who can create Runner and MultiRunner objects by RBAC as well. Creating one of these is, in effect, requesting pod-create rights in the target namespace.

Rejected by design

namespace_per_job and namespace_overwrite_allowed are rejected at admission by the CRD schema. Both make the build namespace dynamic, which would require cluster-scoped RBAC for runners, and the operator pre-provisions RBAC for a namespace it knows in advance. See limitations.