GitHub Actions

This guide explains how to access a Cloudfleet Kubernetes Engine (CFKE) cluster from GitHub Actions with OpenID Connect (OIDC) workload identity federation. The workflow requests a short-lived OIDC token at runtime. The cluster trusts that token directly, and kubectl authenticates as the workflow itself. You do not store API keys, kubeconfigs, or service-account secrets in GitHub.

How it works

By default, the CFKE control plane trusts the GitHub Actions identity providers. When a workflow requests an OIDC token from GitHub with the cluster’s API URL as the audience, the cluster’s kube-apiserver accepts that token directly as a Kubernetes user.

  1. The GitHub Actions runner asks GitHub for an OIDC token, scoped to the audience https://api.cloudfleet.ai/v1/clusters/<cluster-id>.
  2. GitHub issues a short-lived JWT signed by https://token.actions.githubusercontent.com.
  3. kubectl sends the token to the cluster’s API server as a bearer token.
  4. The API server verifies the signature against GitHub’s JWKS and validates the audience. It then authenticates the request as the user github: + the token’s sub claim (for example, github:repo:my-org/my-app:ref:refs/heads/main).
  5. RBAC (and any ValidatingAdmissionPolicy you configure) decides what that user can do.

Left to right flow across a dashed boundary between your workflow and your cluster: the Actions runner requests an OIDC token, the GitHub OIDC issuer issues a short-lived JWT, the kube-apiserver verifies the token, and RBAC decides what it may do. No stored secrets cross the boundary.

The token lifetime is five minutes. If a job runs longer than that window, later kubectl calls fail authentication, and the job must re-run the configuration step. You have no secrets or kubeconfigs to rotate.

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 with cloudfleet clusters list.
  • A GitHub repository where you can edit workflow files and configure repository variables.
  • Administrator access to the cluster (or someone who has it) to bind RBAC to the GitHub identity.

Quick start

The Cloudfleet configure-kubectl action handles the OIDC token request and writes a kubeconfig that subsequent steps can use. The cluster API server presents a publicly trusted TLS certificate, so the kubeconfig needs no CA bundle.

yaml
name: Deploy

on:
  push:
    branches: [main]

permissions:
  id-token: write   # required: lets the workflow request a GitHub OIDC token
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: azure/setup-kubectl@v5
      - uses: cloudfleet-actions/configure-kubectl@v1
        with:
          cluster-id: ${{ vars.CFKE_CLUSTER_ID }}
          region: ${{ vars.CFKE_REGION }}
      - run: kubectl get nodes

Two repository variables (not secrets, because neither value is sensitive):

VariableExample
CFKE_CLUSTER_ID95cc1ef4-2122-4b51-97d9-b35b531c3c45
CFKE_REGIONeurope-central-1a

To set up variables, see the GitHub documentation on storing information in variables.

The first run passes authentication but fails authorization, because no RBAC is bound to the workflow’s identity yet. The error message prints the exact User the cluster authenticated. The next section explains how to grant permissions to that user.

Granting permissions with RBAC

Authentication and authorization are independent. An authenticated workflow has zero permissions until the cluster admin creates a RoleBinding or ClusterRoleBinding that names the workflow’s identity.

Identifying the workflow’s user

CFKE constructs the Kubernetes username by prefixing the GitHub OIDC token’s sub claim with github:. The shape of the sub depends on what triggered the workflow:

TriggerRBAC subject
push to a branchgithub:repo:OWNER/NAME:ref:refs/heads/BRANCH
push of a taggithub:repo:OWNER/NAME:ref:refs/tags/TAG
pull_requestgithub:repo:OWNER/NAME:pull_request
Job using a deployment environmentgithub:repo:OWNER/NAME:environment:ENV
Reusable workflowgithub:repo:OWNER/NAME:job_workflow_ref:OTHER/REPO/.github/workflows/X.yml@REF

GitHub’s OIDC documentation lists the full set of sub formats and how to customize them per repository.

Repositories created, renamed, or transferred after July 15, 2026 use GitHub’s immutable subject format, which adds the owner and repository IDs. For these repositories, the RBAC subject looks like github:repo:my-org@123456/my-app@456789:ref:refs/heads/main.

If you are not sure which subject your workflow produces, run kubectl auth whoami from the workflow once and read the value from the logs. Until a binding names the identity, the command fails with a Forbidden error that still shows the username.

Cluster-wide read access

Use a ClusterRoleBinding when the workflow needs to read resources across all namespaces:

yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: gha-myapp-view
subjects:
  - kind: User
    name: "github:repo:my-org/my-app:ref:refs/heads/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 need write access to only one namespace. A namespaced RoleBinding keeps the scope of access small:

yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: gha-myapp-deployer
  namespace: production
subjects:
  - kind: User
    name: "github:repo:my-org/my-app:ref:refs/heads/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 GitHub workflows

Every authenticated GitHub workflow is also a member of these groups:

  • cfke.io:third-party-idp: every CI/CD identity (GitHub or GitLab)
  • cfke.io:third-party-idp:github: every GitHub Actions identity
  • system:authenticated: every authenticated identity, added by Kubernetes

A group binding alone grants nothing to a CI identity. CFKE denies every request from a github: or gitlab: user unless at least one RoleBinding or ClusterRoleBinding names that user in a kind: User subject. A workflow from a repository you have not bound by name gets no access, even if it targets your cluster ID.

Once a workflow has its own binding, RBAC also applies the bindings of every group it belongs to. To give all your bound GitHub workflows the same baseline permission, add a kind: Group subject with the name cfke.io:third-party-idp:github to a binding, for example one that grants view in a shared namespace.

Warning: Every bound workflow also inherits all bindings to system:authenticated. Never use system:authenticated to grant permissions to your organization’s users. Bind the User group instead, as described in user management.

Example: deploying to production from main

The workflow below pushes a manifest update on every commit to main. Through the RoleBinding above, the cluster trusts pushes to main as a deployer in the production namespace.

yaml
name: Deploy production

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: azure/setup-kubectl@v5

      - name: Configure kubectl for Cloudfleet
        uses: cloudfleet-actions/configure-kubectl@v1
        with:
          cluster-id: ${{ vars.CFKE_CLUSTER_ID }}
          region: ${{ vars.CFKE_REGION }}

      - name: Confirm identity
        run: kubectl auth whoami

      - name: Apply manifests
        run: kubectl apply -n production -f k8s/

      - name: Wait for rollout
        run: kubectl rollout status -n production deployment/myapp --timeout=5m

The permissions: id-token: write block at the workflow (or job) level lets GitHub issue an OIDC token for this run. Without it, the action fails at the token request step with a permission error.

Token claims available for admission policies

In addition to the username, CFKE attaches the most useful claims from the GitHub OIDC token to the authenticated user as Kubernetes extras. ValidatingAdmissionPolicy can read these extras and express checks that RBAC cannot:

Extra keySource claimExample
cfke.io/github-repositoryclaims.repositorymy-org/my-app
cfke.io/github-refclaims.refrefs/heads/main
cfke.io/github-actorclaims.actoroctocat
cfke.io/github-run-idclaims.run_id8472103594
cfke.io/github-workflowclaims.workflowDeploy production
cfke.io/github-eventclaims.event_namepush

The example below denies any modification to resources in the production namespace unless the request comes from a workflow run on refs/heads/main:

yaml
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('github:') ||
        ('cfke.io/github-ref' in request.userInfo.extra &&
         request.userInfo.extra['cfke.io/github-ref'][0] == 'refs/heads/main')        
      message: "Production resources may only be modified from refs/heads/main"

Bind this policy to the production namespace with a ValidatingAdmissionPolicyBinding to enforce it.

Verifying the integration

Add these steps to a workflow during initial setup to confirm authentication and authorization separately:

yaml
- name: Identity (proves authentication)
  run: kubectl auth whoami

- name: Permissions (proves authorization)
  run: kubectl auth can-i --list

kubectl auth whoami prints the username and any extras the cluster sees. If the command fails with a TLS or token error, authentication is broken. Possible causes are a wrong cluster ID, a wrong region, or a missing id-token: write permission.

kubectl auth can-i --list prints the verbs and resources the current user can act on. If both commands fail with Forbidden, no binding names this identity yet.

Security considerations

Bind RBAC to the most specific subject your workflow produces. The general rule is “what triggered this run is also what the cluster trusts.”

  • Never bind cluster-admin to a pull_request subject. Every pull request runs with that subject, including pull requests from forks if you allow them.
  • Branch-scoped subjects (ref:refs/heads/main) are the safest production target. Only commits already merged to main can act with that identity.
  • Tag-scoped subjects (ref:refs/tags/*) suit release pipelines, but anyone who can push tags to the repo can produce that identity. Restrict tag pushes if the role is privileged.
  • Environment-scoped subjects (environment:production) work well with GitHub deployment environment protection rules (required reviewers, wait timers, branch filters). These rules add an approval gate before the cluster trusts the run.
  • Reusable workflow subjects let many repositories share one centrally-reviewed deployment workflow without each repository getting its own RBAC binding.

If you accept pull requests from forks and grant any cluster permissions to the pull_request subject, treat the cluster as compromised by anyone who can open a PR.

CFKE rejects OIDC tokens from your organization’s identity provider if their preferred_username collides with the github: or gitlab: prefixes. As a result, a regular Cloudfleet user cannot impersonate a CI identity.

Reference

configure-kubectl inputs

NameRequiredDescription
cluster-idyesCFKE cluster ID (UUID).
regionyesCluster region (for example, europe-central-1a).

configure-kubectl outputs

NameDescription
kubeconfigPath to the kubeconfig file written by the action. Also exported as the KUBECONFIG env var.

The action does not install kubectl. Add an installer step (such as azure/setup-kubectl@v5) before it.

Source code

The action is open source. Browse or fork it at github.com/cloudfleet-actions/configure-kubectl.

Troubleshooting

Error: Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable

The workflow or job is missing permissions: id-token: write. GitHub only issues OIDC tokens when the workflow explicitly requests them.

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 action requests must match the cluster’s own issuer URL exactly.
  • The cluster is in a different region than configured. Compare the region string with the output of cloudfleet clusters list.

Error from server (Forbidden): … User “github:repo:…” 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

GitHub OIDC tokens expire after five minutes. Before any kubectl call that may run past the token’s lifetime, re-run the cloudfleet-actions/configure-kubectl step. Alternatively, split long-running operations into separate jobs.

Next steps

  • Configure a GitLab CI integration for projects hosted on GitLab.
  • 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).
On this page