Skip to content

Repository files navigation

Gitlab Runner Operator

Kubernetes operator that manages GitLab CI runners using the kubernetes executor.

What is this operator for

It lets you run one or many GitLab runners, each with its own configuration expressed in YAML (no more hand-written config.toml), following an infrastructure-as-code approach. Every option exposed by the kubernetes executor is configurable through the CRD.

Status

Alpha. Breaking changes are possible and will be called out in the release notes. Please open an issue if you hit a bug.

API version: the current API group version is gitlab.k8s.alekc.dev/v1beta2. It replaces the older v1beta1 / v1alpha1 versions. The change is breaking: the authentication block was reworked (see below) and existing objects are not converted automatically.

Installation with helm

Once the CRDs are installed you can deploy the operator into your preferred namespace:

helm repo add alekc https://charts.alekc.dev/
helm repo update
helm install gitlab-runner-operator alekc/gitlab-runner-operator

Authentication

GitLab deprecated the registration-token workflow (in 16.0) and disabled it by default from 18.0 onward, so this operator uses runner authentication tokens (the glrt- tokens). There are two ways to authenticate, set under spec.authentication:

  1. Bring your own token. Create the runner in GitLab yourself (UI or the POST /user/runners API) and give the operator the resulting glrt- token. The operator makes no GitLab API calls; it just writes the token into the runner config.

  2. Operator-managed. Give the operator an access token (personal, group, or project) that holds the create_runner scope plus a create_options block. The operator creates the runner through POST /user/runners, stores the returned token, and deletes the runner from GitLab when the object is removed. Deletion first uses the runner's own authentication token (DELETE /runners by token), which needs no access-token scope, so create_runner alone is enough for the normal lifecycle. If that fails or the token is unavailable, the operator falls back to deleting by runner id with the access token (DELETE /runners/:id), which succeeds only when the access token also holds the api scope; otherwise the runner is logged as possibly orphaned.

Exactly one of the two modes must be configured; the CRD schema (CEL) rejects objects that set both or neither.

Configuration

Top-level spec fields (all optional unless noted):

Key Description
authentication Required. How the runner authenticates (see above).
concurrent Maximum number of jobs run concurrently across this runner. Minimum 1.
check_interval Seconds between checks for new jobs. Minimum 3.
log_level One of panic, fatal, error, warning, info, debug.
log_format One of runner, text, json.
gitlab_instance_url GitLab URL. Defaults to https://gitlab.com/.
caCertificate Optional PEM CA bundle to verify a private or self-signed GitLab endpoint, used for both the operator's API calls and the runner's own connection. Supply it inline (value) or from a secretKeyRef / configMapKeyRef (see below).
executor_config Kubernetes executor options, see the keywords reference.
environment Custom environment variables injected into the build environment.
runner_image Override the gitlab-runner image. Defaults to a recent gitlab/gitlab-runner:alpine-vX.Y.Z.

authentication fields

Key Description
token Bring-your-own mode: the pre-created glrt- token, as a token source (see below).
access_token Managed mode: an access token with the create_runner scope, as a token source.
create_options Managed mode: runner_type (instance_type/group_type/project_type), group_id, project_id, description, tag_list, run_untagged, locked, paused, access_level, maximum_timeout.

Both token and access_token are token sources with two mutually exclusive ways to supply the value:

Key Description
value The literal token, inline. Convenient for testing.
secret_key_ref Read the token from a Secret in the runner namespace: name (required), key (optional, defaults to token), optional (when true, a missing secret or key resolves to an empty token instead of failing).

caCertificate fields

Set at most one of the following. A referenced object must live in the runner namespace and hold a PEM CA bundle. The operator copies the resolved bundle into the runner's config Secret and points tls-ca-file at it.

Key Description
value The PEM CA bundle inline, supplied directly in the manifest.
secretKeyRef Read the CA from a Secret: name (required), key (optional, defaults to ca.crt).
configMapKeyRef Read the CA from a ConfigMap: name (required), key (optional, defaults to ca.crt).

Examples

Bring-your-own token

apiVersion: gitlab.k8s.alekc.dev/v1beta2
kind: Runner
metadata:
  name: runner-sample
spec:
  authentication:
    token:
      value: "glrt-XXXXXXXXXXXXXXXXXXXX"

Operator-managed runner

apiVersion: gitlab.k8s.alekc.dev/v1beta2
kind: Runner
metadata:
  name: runner-managed
spec:
  authentication:
    access_token:
      value: "glpat-XXXXXXXXXXXXXXXXXXXX"
    create_options:
      runner_type: project_type
      project_id: 1234567
      run_untagged: true
      tag_list:
        - test-gitlab-runner

Token from a secret

key defaults to token; set secret_key_ref.key to read a different key.

apiVersion: v1
kind: Secret
metadata:
  name: gitlab-runner-token
type: Opaque
stringData:
  token: "glrt-XXXXXXXXXXXXXXXXXXXX"
---
apiVersion: gitlab.k8s.alekc.dev/v1beta2
kind: Runner
metadata:
  name: runner-sample
spec:
  authentication:
    token:
      secret_key_ref:
        name: gitlab-runner-token
        # key omitted -> defaults to "token"

Mounting secrets or config maps as volumes

apiVersion: gitlab.k8s.alekc.dev/v1beta2
kind: Runner
metadata:
  name: runner-sample
spec:
  log_level: debug
  executor_config:
    image: "debian:slim"
    memory_limit: "150Mi"
    memory_request: "150Mi"
    volumes:
      config_map:
        - mount_path: /cm/
          name: test-config
      secret:
        - mount_path: /secrets/1/
          name: test-secret
  authentication:
    token:
      value: "glrt-XXXXXXXXXXXXXXXXXXXX"

Multiple runners in one object

See config/samples/gitlab_v1beta2_multirunner.yaml for a MultiRunner example that mixes both authentication modes across entries.

Custom CA for a self-signed GitLab

Set caCertificate to a PEM bundle, inline or from a Secret / ConfigMap. The operator uses it for its own API calls (fixing x509: certificate signed by unknown authority during registration) and copies it into the runner's config Secret so the runner trusts the endpoint too.

apiVersion: gitlab.k8s.alekc.dev/v1beta2
kind: Runner
metadata:
  name: runner-private-ca
spec:
  gitlab_instance_url: https://gitlab.internal.example.com/
  caCertificate:
    # one of secretKeyRef or configMapKeyRef; key defaults to ca.crt
    configMapKeyRef:
      name: gitlab-ca
  authentication:
    access_token:
      secret_key_ref:
        name: gitlab-access-token
    create_options:
      runner_type: project_type
      project_id: 1234567

Or supply the bundle inline with value:

spec:
  caCertificate:
    value: |
      -----BEGIN CERTIFICATE-----
      ...your CA here...
      -----END CERTIFICATE-----

RBAC and namespaces

The kubernetes executor permission set (pods and pods/exec, pods/attach, pods/log, services, secrets, configmaps, serviceaccounts, events) lives in one shared ClusterRole, gitlab-runner-operator-executor, reconciled by the operator. For each Runner or MultiRunner the operator then provisions its own ServiceAccount (a distinct identity for audit and revocation) and a RoleBinding that binds that ServiceAccount to the shared ClusterRole. A MultiRunner shares a single ServiceAccount across all its entries. Because the rules live in one ClusterRole, a permission change in a new operator version applies to every runner at once.

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 granted to a runner.

Job pods run in executor_config.namespace when set, otherwise in the runner's own namespace. By default a runner may only target its own namespace: a Runner author choosing an arbitrary namespace would otherwise have the operator bind their ServiceAccount (and run their jobs) in, say, kube-system, a privilege-escalation path. To permit specific build namespaces, start the operator with --allowed-build-namespaces=ns-a,ns-b (or =* to allow any). 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 (the ServiceAccount stays in the runner namespace) and removes it when the runner is deleted.

Because the operator pre-provisions RBAC for a known namespace, namespace_per_job and namespace_overwrite_allowed are rejected at admission by the CRD schema (CEL): both make the build namespace dynamic, which would require cluster-scoped RBAC.

Security note. The namespace 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 would be a privilege-escalation path: a Runner author could reach kube-system or another tenant's namespace. Keep --allowed-build-namespaces tight, and additionally restrict who can create Runner/MultiRunner objects by RBAC.

License

Apache License 2.0. See LICENSE.

About

Kubernetes operator to manage gitlab runners.

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages