Skip to content

Dedicated build namespace

Keeping job pods out of the namespace the runner lives in is worth doing: jobs are arbitrary code, and a separate namespace gives you a quota boundary, a NetworkPolicy boundary, and a Pod Security Admission level of their own.

The short version

Allow the namespace on the operator first, or the runner will refuse it:

# Helm values
allowedBuildNamespaces:
  - gitlab-ci-builds

Then point the runner at it:

apiVersion: gitlab.k8s.alekc.dev/v1beta2
kind: Runner
metadata:
  name: runner-sample
  namespace: gitlab-runners
spec:
  authentication:
    token:
      secret_key_ref:
        name: gitlab-runner-token
  executor_config:
    namespace: gitlab-ci-builds

The operator creates the RoleBinding in gitlab-ci-builds while the ServiceAccount stays in gitlab-runners, and removes that binding when the runner is deleted.

Why the allow-list exists

Without it, whoever can create a Runner could name any namespace and have the operator bind a ServiceAccount, and run pods, there. kube-system included. The reconciler enforces the list, so it cannot be bypassed by a flag on the object. A namespace that is not allowed leaves the runner NotReady with an error and provisions no RBAC at all.

allowedBuildNamespaces: ["*"] allows any, and is a reasonable choice on a single-tenant cluster and a poor one anywhere else. Read RBAC and namespaces before choosing.

What else the namespace needs

Secrets move with the jobs. Everything the job pod reads is read from this namespace: image pull secrets, mounted Secrets and ConfigMaps, the service account. Moving the build namespace and leaving the Secrets behind is the single most common way this setup breaks.

Pod Security Admission. Label it for the strictest level your jobs can live with:

kubectl label ns gitlab-ci-builds \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/warn=restricted

restricted forbids privileged pods, so DinD will not run under it. That is the trade: baseline if you need DinD, restricted with rootless builds if you do not.

A ResourceQuota. A runaway pipeline can otherwise consume the cluster. Size it above concurrent multiplied by your per-job requests, or jobs fail to schedule for reasons that look nothing like a quota.

A NetworkPolicy that still lets jobs out. A default-deny namespace breaks CI in a confusing way, because the job pod needs to reach GitLab, your registry, and usually the public internet for dependencies. Allow egress to those explicitly and remember DNS.

Gotchas

namespace_per_job and namespace_overwrite_allowed are rejected. The CRD refuses both at admission. RBAC is provisioned ahead of time for a namespace the operator knows about, and a dynamic namespace would mean granting runners cluster-scoped permissions. See limitations.

Turning the allow-list off later revokes bindings. Remove a namespace from the flag and the operator prunes the RoleBinding it created there on the next reconcile. Jobs starting after that fail with a forbidden error.

One namespace per trust level, not per project. Every runner sharing a build namespace shares its blast radius. Splitting by team or trust level is worth it; splitting per project multiplies the allow-list without buying much.