GitLab CI
This guide explains how to access a Cloudfleet Kubernetes Engine (CFKE) cluster from GitLab CI/CD jobs using OpenID Connect (OIDC) workload identity federation. The job mints a short-lived OIDC ID token at runtime, the cluster trusts that token directly, and kubectl authenticates as the job itself. No API keys, kubeconfigs, or service-account secrets need to be stored as GitLab variables.
How it works
The CFKE control plane is configured to trust GitLab CI’s CI/CD identity provider out of the box. When a GitLab job declares an id_tokens block with the cluster’s API URL as the audience, the cluster’s kube-apiserver accepts the resulting JWT directly as a Kubernetes user.
- The GitLab job declares
id_tokenswithaud: https://api.cloudfleet.ai/v1/clusters/<cluster-id>. GitLab sets that token as an environment variable for the job. - The runner uses the token as a bearer token when calling the cluster’s API server.
- The API server verifies the signature against GitLab’s JWKS at the issuer URL (
https://gitlab.comby default, or your self-hosted GitLab URL), validates the audience, and authenticates the request as the usergitlab:+ the token’ssubclaim (for example,gitlab:project_path:my-group/my-app:ref_type:branch:ref:main). - RBAC (and any
ValidatingAdmissionPolicyyou configure) decides what that user is allowed to do.
The token lifetime is short (GitLab’s default is five minutes). Long-running jobs that span that window will see authentication failures on later kubectl calls. There are no secrets or kubeconfigs to rotate.
For self-hosted GitLab instances, the cluster’s trusted issuer URL must match the GitLab installation. CFKE supports overriding the default https://gitlab.com issuer. Contact Cloudfleet support if you need this configured.
Prerequisites
- A running CFKE cluster. If you do not have one yet, follow the getting started guide.
- The cluster ID (a UUID) and region (for example,
europe-central-1a). You can read both from the Cloudfleet console or withcloudfleet clusters list. - A GitLab project where you can edit
.gitlab-ci.ymland configure CI/CD variables. - Administrator access to the cluster (or someone who has it) so RBAC can be bound to the GitLab identity.
Quick start
The snippet below installs kubectl, builds a kubeconfig from the OIDC token, and runs a kubectl command. The cluster API server presents a publicly trusted TLS certificate, so no CA bundle needs to be fetched.
.cfke:
image: debian:13-slim
variables:
DEBIAN_FRONTEND: noninteractive
KUBECTL_VERSION: v1.34
KUBECONFIG: ${CI_PROJECT_DIR}/kubeconfig
CFKE_CLUSTER_ID: 95cc1ef4-2122-4b51-97d9-b35b531c3c45
CFKE_REGION: europe-central-1a
CFKE_API_SERVER: https://${CFKE_CLUSTER_ID}.${CFKE_REGION}.cfke.io
CFKE_API_AUD: https://api.cloudfleet.ai/v1/clusters/${CFKE_CLUSTER_ID}
id_tokens:
CFKE_TOKEN:
aud: ${CFKE_API_AUD}
before_script:
- apt-get update -qq && apt-get install -y --no-install-recommends apt-transport-https ca-certificates curl gnupg
- install -d /etc/apt/keyrings
- curl -fsSL "https://pkgs.k8s.io/core:/stable:/${KUBECTL_VERSION}/deb/Release.key" | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
- echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/${KUBECTL_VERSION}/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
- apt-get update -qq && apt-get install -y --no-install-recommends kubectl
- kubectl config set-cluster cfke --server="${CFKE_API_SERVER}"
- kubectl config set-credentials gitlab --token="${CFKE_TOKEN}"
- kubectl config set-context ctx --cluster=cfke --user=gitlab
- kubectl config use-context ctx
deploy:
extends: .cfke
script:
- kubectl auth whoami
- kubectl get nodes
The .cfke hidden job is reusable. Any job that extends: .cfke runs before_script and inherits a working KUBECONFIG, so its script block can call kubectl directly.
The first run will succeed at authentication but fail at authorization, because no RBAC has been bound to the job’s identity yet. The error message will print the exact User the cluster authenticated. The next section explains how to grant permissions to that user.
Project-level CI/CD variables
The cluster ID and region are not sensitive — they identify the cluster but cannot be used to authenticate against it. Configure them as GitLab CI/CD variables so they can be reused across .gitlab-ci.yml and overridden per environment:
- In your GitLab project, go to Settings > CI/CD > Variables.
- Add two variables:
CFKE_CLUSTER_ID— the cluster UUIDCFKE_REGION— the cluster region
Then remove the hard-coded values from the snippet above. The defaults exposed by GitLab via id_tokens and your project variables are enough.
Granting permissions with RBAC
Authentication and authorization are independent. A job that has been authenticated still has zero permissions until the cluster admin creates a RoleBinding or ClusterRoleBinding that names the job’s identity.
Identifying the job’s user
CFKE constructs the Kubernetes username by prefixing the GitLab OIDC token’s sub claim with gitlab:. The shape of the sub depends on how the job was triggered. The most common forms:
| Trigger | RBAC subject |
|---|---|
| Pipeline on a branch | gitlab:project_path:GROUP/PROJECT:ref_type:branch:ref:BRANCH |
| Pipeline on a tag | gitlab:project_path:GROUP/PROJECT:ref_type:tag:ref:TAG |
| Merge request pipeline | gitlab:project_path:GROUP/PROJECT:ref_type:branch:ref:SOURCE_BRANCH |
| Job using a deployment environment | gitlab:project_path:GROUP/PROJECT:environment:ENV |
GitLab’s ID token documentation lists the full set of claims and how the sub is composed. If you are unsure what subject your job produces, run kubectl auth whoami from the job once and read the value from the logs.
Cluster-wide read access
Use a ClusterRoleBinding when the job needs to read resources across all namespaces:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gitlab-myapp-view
subjects:
- kind: User
name: "gitlab:project_path:my-group/my-app:ref_type:branch:ref:main"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view # built-in read-only role
apiGroup: rbac.authorization.k8s.io
Namespace-scoped deploy access
Most deployments only need write access to a single namespace. A namespaced RoleBinding keeps the blast radius small:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: gitlab-myapp-deployer
namespace: production
subjects:
- kind: User
name: "gitlab:project_path:my-group/my-app:ref_type:branch:ref:main"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit # built-in read/write role
apiGroup: rbac.authorization.k8s.io
Granting permissions to all GitLab jobs
Every authenticated GitLab job is also a member of two groups:
cfke.io:third-party-idp— every CI/CD identity (GitLab or GitHub)cfke.io:third-party-idp:gitlab— every GitLab CI identity
Bind a role to one of these groups to grant a baseline permission to all CI jobs. For example, to let every GitLab job read its own job’s events, replace kind: User with kind: Group and the group name in the binding above. Use this with care: any project on gitlab.com (or your self-hosted GitLab) that points its jobs at your cluster ID will pick up these permissions.
Example: deploying to production from main
The pipeline below applies a manifest update on every commit to main. The cluster trusts pushes to main as a deployer in the production namespace via the RoleBinding above.
.cfke:
image: debian:13-slim
variables:
DEBIAN_FRONTEND: noninteractive
KUBECTL_VERSION: v1.34
KUBECONFIG: ${CI_PROJECT_DIR}/kubeconfig
CFKE_API_SERVER: https://${CFKE_CLUSTER_ID}.${CFKE_REGION}.cfke.io
CFKE_API_AUD: https://api.cloudfleet.ai/v1/clusters/${CFKE_CLUSTER_ID}
id_tokens:
CFKE_TOKEN:
aud: ${CFKE_API_AUD}
before_script:
- apt-get update -qq && apt-get install -y --no-install-recommends apt-transport-https ca-certificates curl gnupg
- install -d /etc/apt/keyrings
- curl -fsSL "https://pkgs.k8s.io/core:/stable:/${KUBECTL_VERSION}/deb/Release.key" | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
- echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/${KUBECTL_VERSION}/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
- apt-get update -qq && apt-get install -y --no-install-recommends kubectl
- kubectl config set-cluster cfke --server="${CFKE_API_SERVER}"
- kubectl config set-credentials gitlab --token="${CFKE_TOKEN}"
- kubectl config set-context ctx --cluster=cfke --user=gitlab
- kubectl config use-context ctx
deploy:
extends: .cfke
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- kubectl auth whoami
- kubectl apply -n production -f k8s/
- kubectl rollout status -n production deployment/myapp --timeout=5m
CFKE_CLUSTER_ID and CFKE_REGION are inherited from project-level CI/CD variables and substituted into CFKE_API_SERVER and CFKE_API_AUD.
Using a pre-built kubectl image
If you prefer not to install kubectl in every job, swap the image: to one that already has it (for example, bitnami/kubectl) and shorten before_script to just the kubeconfig assembly:
.cfke:
image:
name: bitnami/kubectl:1.34
entrypoint: [""]
variables:
KUBECONFIG: ${CI_PROJECT_DIR}/kubeconfig
CFKE_API_SERVER: https://${CFKE_CLUSTER_ID}.${CFKE_REGION}.cfke.io
CFKE_API_AUD: https://api.cloudfleet.ai/v1/clusters/${CFKE_CLUSTER_ID}
id_tokens:
CFKE_TOKEN:
aud: ${CFKE_API_AUD}
before_script:
- kubectl config set-cluster cfke --server="${CFKE_API_SERVER}"
- kubectl config set-credentials gitlab --token="${CFKE_TOKEN}"
- kubectl config set-context ctx --cluster=cfke --user=gitlab
- kubectl config use-context ctx
Token claims available for admission policies
In addition to the username, CFKE attaches the most useful claims from the GitLab OIDC token to the authenticated user as Kubernetes extras. These are visible to ValidatingAdmissionPolicy and can express checks RBAC cannot:
| Extra key | Source claim | Example |
|---|---|---|
cfke.io/gitlab-project | claims.project_path | my-group/my-app |
cfke.io/gitlab-ref | claims.ref | main |
cfke.io/gitlab-user | claims.user_login | alice |
cfke.io/gitlab-pipeline-id | claims.pipeline_id | 1834201 |
The example below denies any modification to resources in the production namespace unless the request comes from a job triggered by a commit to the main branch:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: prod-only-from-main
spec:
matchConstraints:
resourceRules:
- apiGroups: ["*"]
apiVersions: ["*"]
operations: ["CREATE", "UPDATE", "DELETE"]
resources: ["*"]
validations:
- expression: |
!request.userInfo.username.startsWith('gitlab:') ||
('cfke.io/gitlab-ref' in request.userInfo.extra &&
request.userInfo.extra['cfke.io/gitlab-ref'][0] == 'main')
message: "Production resources may only be modified from the main branch"
Bind this policy to the production namespace with a ValidatingAdmissionPolicyBinding to enforce it.
Verifying the integration
Add these commands to a job during initial setup to confirm authentication and authorization separately:
verify:
extends: .cfke
script:
- kubectl auth whoami # proves authentication
- kubectl auth can-i --list # proves authorization
kubectl auth whoami prints the username and any extras the cluster sees. If this fails with a TLS or token error, authentication itself is broken (wrong cluster ID, wrong region, missing id_tokens block, or — for self-hosted GitLab — an issuer mismatch).
kubectl auth can-i --list prints the verbs and resources the current user is allowed to act on. An empty or near-empty list means RBAC has not been bound yet to this identity.
Security considerations
Bind RBAC to the most specific subject your pipeline produces. The general rule is “what triggered this job is also what the cluster trusts.”
- Branch-scoped subjects (
ref_type:branch:ref:main) are the safest production target. Only commits already merged to the protected branch can act with that identity. Combine with GitLab protected branches to restrict who can push there. - Tag-scoped subjects (
ref_type:tag:ref:*) suit release pipelines, but anyone who can push tags to the repo can produce that identity. Restrict tag protection if the role is privileged. - Environment-scoped subjects (
environment:production) compose well with GitLab deployment approvals for an extra approval gate before the cluster trusts the run. - Merge request pipelines run with the source branch in their
sub. Treat the cluster as compromised by anyone who can open a merge request if you grant cluster permissions to merge request subjects.
CFKE rejects OIDC tokens (from your organization’s identity provider) whose preferred_username would collide with the github: or gitlab: prefixes, so a regular Cloudfleet user cannot impersonate a CI identity.
Reference
Required job configuration
| Field | Value | Purpose |
|---|---|---|
id_tokens.<NAME>.aud | https://api.cloudfleet.ai/v1/clusters/<cluster-id> | Tells GitLab to mint an ID token scoped to this cluster. |
| Cluster API URL | https://<cluster-id>.<region>.cfke.io | API server endpoint kubectl should target. Served with a publicly trusted TLS certificate. |
The id_tokens keyword exposes the JWT as the environment variable named on the left-hand side (in the snippets above, CFKE_TOKEN). Subsequent shell commands can read that variable to assemble the kubeconfig.
Self-hosted GitLab
If your GitLab instance runs at a URL other than https://gitlab.com, the cluster needs to be configured with that issuer URL at provisioning time. The default trusted issuer is https://gitlab.com. Contact Cloudfleet support to override it.
Troubleshooting
error: You must be logged in to the server (Unauthorized)
The token reached the API server but was rejected. Common causes:
- Wrong cluster ID or region. The audience the job declares must match the cluster’s own issuer URL exactly.
- Self-hosted GitLab whose issuer URL has not been registered with the cluster. Verify with
cloudfleet clusters describeor contact support. - The
id_tokensblock was omitted, soCFKE_TOKENis empty.
Error from server (Forbidden): ... User "gitlab:project_path:..." cannot ...
Authentication succeeded; authorization failed. The error message contains the exact User the cluster saw. Bind a RoleBinding or ClusterRoleBinding to that user (see Granting permissions with RBAC).
kubectl succeeds early in the job but fails later
GitLab ID tokens are short-lived. Restart the job’s authentication step (re-run before_script) before any kubectl call that may run past the token’s lifetime, or split long-running operations into separate jobs.
Next steps
- Configure a GitHub Actions integration for projects hosted on GitHub.
- Review API tokens for cases where OIDC federation is not an option (for example, calling the Cloudfleet API to manage clusters or fleets from CI).
← GitHub Actions