Credentials for GitHub Actions¶
How a GitHub Actions workflow gets a kubelatch credential without storing any secret in the repository.
How it works¶
GitHub Actions can issue an OIDC id_token signed by GitHub, with the repository, the branch, the run_id and who triggered the job inside it. The workflow asks GitHub for it, sends it to kubelatch, and kubelatch exchanges it for a bot credential, complete with its kubeconfig. There's no secret to store in GitHub or to rotate.
sequenceDiagram
participant W as Workflow
participant G as GitHub
participant K as kubelatch
W->>G: requests an id_token (audience = KUBELATCH_URL)
G-->>W: signed id_token
W->>K: POST /v1/ci/github-actions/token {token}
K->>K: checks the token and looks up the trust rule
K-->>W: bot credential + kubeconfig (once)
W->>K: kubectl through the proxy
Before you start¶
An administrator has to have prepared this beforehand (CI: trust rules):
- A registered bot, with at least one permission granted on the cluster you need.
- A trust rule that matches your repository (by its numeric ids, not by name) to that bot, optionally restricted to a branch (
ref) or anenvironment. - A
KUBELATCH_URLthe runner can reach over https, with a certificate it trusts (a kubelatch on a laptop or behind a private network won't work).
The workflow¶
-
Create
.github/workflows/kubelatch.ymlin your repository with this content. It's the example that ships in the kubelatch repository atexamples/github-actions/kubelatch.yml, without the comments. An administrator can also give you the Fragmento del workflow (Workflow snippet) from the CI screen, which already carries your kubelatch's URL.name: kubectl via kubelatch on: push: branches: [main] workflow_dispatch: permissions: id-token: write # required: lets the job request the OIDC token contents: read env: KUBELATCH_URL: https://kubelatch.example.com # exactly KUBELATCH_BASE_URL: it is the token's audience jobs: pods: runs-on: ubuntu-latest # environment: prod # uncomment if the trust rule sets an environment steps: - name: Request the OIDC token for kubelatch run: | ID_TOKEN=$(curl -sSf -G \ -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ --data-urlencode "audience=$KUBELATCH_URL" \ "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -r .value) echo "::add-mask::$ID_TOKEN" echo "ID_TOKEN=$ID_TOKEN" >> "$GITHUB_ENV" - name: Exchange it for a kubelatch credential run: | RESPONSE=$(curl -sS -w '\n%{http_code}' -H 'Content-Type: application/json' \ -d "$(jq -n --arg token "$ID_TOKEN" '{token:$token}')" "$KUBELATCH_URL/v1/ci/github-actions/token") STATUS=$(printf '%s' "$RESPONSE" | tail -n1) BODY=$(printf '%s' "$RESPONSE" | sed '$d') if [ "$STATUS" != "201" ]; then echo "kubelatch answered $STATUS: $(printf '%s' "$BODY" | jq -r '.error // .')" >&2 exit 1 fi printf '%s' "$BODY" | jq -r .token | xargs -I{} echo "::add-mask::{}" (umask 077 && printf '%s' "$BODY" | jq -r .kubeconfig > "$RUNNER_TEMP/kubeconfig") chmod 600 "$RUNNER_TEMP/kubeconfig" echo "KUBECONFIG=$RUNNER_TEMP/kubeconfig" >> "$GITHUB_ENV" echo "credential expires at $(printf '%s' "$BODY" | jq -r .expires_at)" - name: kubectl through kubelatch run: | kubectl auth whoami # bot:<name> with the kubelatch:* groups of its grants kubectl get pods -n default -
Replace
KUBELATCH_URLwith your kubelatch's URL. It has to match letter for letter the server'sKUBELATCH_BASE_URL: it's the audience kubelatch requires in the token. - If the trust rule sets an
environment, uncomment theenvironment:line and put its name. Without it, theid_tokenwon't carry the claim the trust rule expects, and the exchange answers403. - Adjust the last step to whatever you need to do on the cluster.
What each step does¶
permissions:id-token: writeis required. Without it, the job can't request theid_token.- Request the OIDC token: asks GitHub for an
id_tokenwith audienceKUBELATCH_URLand masks it in the log. - Exchange it: exchanges it at
POST /v1/ci/github-actions/token. A201returnscredential,token,kubeconfig,clustersandexpires_at, once. The step masks the token and saves the kubeconfig to$RUNNER_TEMP, never to the runner's~/.kube/config. - kubectl through kubelatch: from here on, kubectl uses that kubeconfig through the proxy.
Check that it works¶
Trigger the workflow (workflow_dispatch or a push to the configured branch) and confirm:
- The job prints
bot:<bot-name>inkubectl auth whoami. - The credential shows up in Credenciales (Credentials) (for an administrator) with the name
github-actionsand a noteowner/repo@refs/heads/main · run <id> · <actor>. - The requests show up in the audit log with that bot as the subject.
Re-running the job requests a new id_token (each run has its own jti, so a captured token can't be used twice).
If something fails¶
401 token no válido(401 invalid token): theid_tokendoesn't pass verification (signature, issuer, an audience different fromKUBELATCH_BASE_URL, expired, or already exchanged before). Check thatKUBELATCH_URLmatchesKUBELATCH_BASE_URLexactly and that you're not reusing a token from a previous job.403 ninguna trust rule coincide ...(403 no trust rule matches ...): the message carries the token'srepository_owner_id,repository_id,refandenvironment. Ask an administrator to create or adjust the trust rule with those values, or to removeref/environmentto make it generic.403naming the bot: the bot is disabled or has no active permissions on any cluster.- The runner can't reach kubelatch (
curl: (7)or(35)): missing network reachability over https, or the certificate isn't trusted. It's a deployment problem, not the workflow's: ask an administrator to check the Install guide.
The full list of errors is in Errors.