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.
- The GitHub Actions runner asks GitHub for an OIDC token, scoped to the audience
https://api.cloudfleet.ai/v1/clusters/<cluster-id>. - GitHub issues a short-lived JWT signed by
https://token.actions.githubusercontent.com. kubectlsends the token to the cluster’s API server as a bearer token.- 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’ssubclaim (for example,github:repo:my-org/my-app:ref:refs/heads/main). - RBAC (and any
ValidatingAdmissionPolicyyou configure) decides what that user can do.
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 withcloudfleet 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.
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 nodesTwo repository variables (not secrets, because neither value is sensitive):
| Variable | Example |
|---|---|
CFKE_CLUSTER_ID | 95cc1ef4-2122-4b51-97d9-b35b531c3c45 |
CFKE_REGION | europe-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:
| Trigger | RBAC subject |
|---|---|
push to a branch | github:repo:OWNER/NAME:ref:refs/heads/BRANCH |
push of a tag | github:repo:OWNER/NAME:ref:refs/tags/TAG |
pull_request | github:repo:OWNER/NAME:pull_request |
| Job using a deployment environment | github:repo:OWNER/NAME:environment:ENV |
| Reusable workflow | github: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:
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.ioNamespace-scoped deploy access
Most deployments need write access to only one namespace. A namespaced RoleBinding keeps the scope of access small:
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.ioGranting 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 identitysystem: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 usesystem:authenticatedto grant permissions to your organization’s users. Bind theUsergroup 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.
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=5mThe 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 key | Source claim | Example |
|---|---|---|
cfke.io/github-repository | claims.repository | my-org/my-app |
cfke.io/github-ref | claims.ref | refs/heads/main |
cfke.io/github-actor | claims.actor | octocat |
cfke.io/github-run-id | claims.run_id | 8472103594 |
cfke.io/github-workflow | claims.workflow | Deploy production |
cfke.io/github-event | claims.event_name | push |
The example below denies any modification to resources in the production namespace unless the request comes from a workflow run on refs/heads/main:
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:
- name: Identity (proves authentication)
run: kubectl auth whoami
- name: Permissions (proves authorization)
run: kubectl auth can-i --listkubectl 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-adminto apull_requestsubject. 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 tomaincan 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
| Name | Required | Description |
|---|---|---|
cluster-id | yes | CFKE cluster ID (UUID). |
region | yes | Cluster region (for example, europe-central-1a). |
configure-kubectl outputs
| Name | Description |
|---|---|
kubeconfig | Path 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-idorregion. 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).
GitLab CI →