Skip to content

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 an environment.
  • A KUBELATCH_URL the 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

  1. Create .github/workflows/kubelatch.yml in your repository with this content. It's the example that ships in the kubelatch repository at examples/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
    
  2. Replace KUBELATCH_URL with your kubelatch's URL. It has to match letter for letter the server's KUBELATCH_BASE_URL: it's the audience kubelatch requires in the token.

  3. If the trust rule sets an environment, uncomment the environment: line and put its name. Without it, the id_token won't carry the claim the trust rule expects, and the exchange answers 403.
  4. Adjust the last step to whatever you need to do on the cluster.

What each step does

  • permissions: id-token: write is required. Without it, the job can't request the id_token.
  • Request the OIDC token: asks GitHub for an id_token with audience KUBELATCH_URL and masks it in the log.
  • Exchange it: exchanges it at POST /v1/ci/github-actions/token. A 201 returns credential, token, kubeconfig, clusters and expires_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> in kubectl auth whoami.
  • The credential shows up in Credenciales (Credentials) (for an administrator) with the name github-actions and a note owner/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): the id_token doesn't pass verification (signature, issuer, an audience different from KUBELATCH_BASE_URL, expired, or already exchanged before). Check that KUBELATCH_URL matches KUBELATCH_BASE_URL exactly 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's repository_owner_id, repository_id, ref and environment. Ask an administrator to create or adjust the trust rule with those values, or to remove ref/environment to make it generic.
  • 403 naming 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.