Saltar a contenido

Credenciales para GitHub Actions

Cómo consigue un workflow de GitHub Actions una credencial de kubelatch sin guardar ningún secreto en el repositorio.

Cómo funciona

GitHub Actions puede emitir un id_token OIDC firmado por GitHub, con el repositorio, la rama, el run_id y quien lanzó el job dentro. La action picaportelabs/kubelatch-action se lo pide a GitHub, se lo manda a kubelatch, y kubelatch lo canjea por una credencial de un bot, ya con su kubeconfig. No hay ningún secreto que guardar en GitHub ni que rotar.

La credencial dura 1 hora por defecto, suficiente para un job; un administrador puede cambiarlo con KUBELATCH_CI_TOKEN_TTL. Un job que dure más vuelve a usar la action.

sequenceDiagram
    participant W as Workflow
    participant G as GitHub
    participant K as kubelatch
    W->>G: la action pide un id_token (audiencia = url)
    G-->>W: id_token firmado
    W->>K: POST /v1/ci/github-actions/token {token}
    K->>K: verifica el token y busca la regla de confianza
    K-->>W: credencial del bot + kubeconfig (una vez)
    W->>K: kubectl a través del proxy

Antes de empezar

Un administrador tiene que haber preparado esto antes (CI: reglas de confianza):

  • Un bot dado de alta, con al menos un permiso concedido en el cluster que necesitas.
  • Una regla de confianza que haga coincidir tu repositorio (por sus ids numéricos, no por nombre) con ese bot, opcionalmente restringida a una rama (ref) o a un environment.
  • La URL de tu kubelatch, que el runner alcance por https, con un certificado en el que confíe (un kubelatch en un portátil o detrás de una red privada no sirve con los runners de GitHub).

El workflow

  1. Crea .github/workflows/kubelatch.yml en tu repositorio con este contenido. Es el ejemplo que trae el repositorio de kubelatch, sin los comentarios de cabecera. Un administrador también puede pasarte el «Fragmento del workflow», que ya lleva la URL de tu kubelatch: lo tiene rellenado para tu regla justo al crearla («Conecta el workflow»), o en «Workflow de ejemplo» de la pantalla «CI».

    name: kubectl via kubelatch
    
    on:
      push:
        branches: [main]
      workflow_dispatch:
    
    permissions:
      id-token: write   # required: lets the job request the OIDC token
      contents: read
    
    jobs:
      pods:
        runs-on: ubuntu-latest
        # environment: prod   # uncomment if the trust rule sets an environment
        steps:
          - uses: picaportelabs/kubelatch-action@v1
            with:
              url: https://kubelatch.example.com   # exactly KUBELATCH_BASE_URL: it is the token's audience
    
          - name: kubectl through kubelatch
            run: |
              kubectl auth whoami          # bot:<name> with the kubelatch:* groups of its grants
              kubectl get pods -n default
    
  2. Cambia url por la URL de tu kubelatch. Tiene que ser el KUBELATCH_BASE_URL del servidor, con el mismo esquema, host, puerto y ruta: es la audiencia que kubelatch exige en el token. La barra final da igual.

  3. Si la regla de confianza fija un environment, quita el # de la línea environment: y pon su nombre. Sin él, el id_token no lleva el claim que la regla de confianza espera y el canje responde 403.
  4. Ajusta el último paso a lo que necesites hacer en el cluster. Todos los pasos siguientes del job usan la credencial: kubectl, helm y cualquier herramienta que lea KUBECONFIG.

Qué hace la action

  • permissions: id-token: write es obligatorio. Sin él, el job no puede pedir el id_token.
  • Pide a GitHub un id_token con url como audiencia y lo enmascara en el log. Esa petición a GitHub falla de vez en cuando, así que la action la intenta hasta tres veces.
  • Canjea el token en POST /v1/ci/github-actions/token una sola vez: kubelatch nunca acepta dos veces el mismo token. Un 201 trae la credencial y su kubeconfig, una sola vez.
  • Enmascara la credencial antes de imprimir nada más, escribe el kubeconfig en un directorio propio bajo $RUNNER_TEMP, que en runners de Linux y macOS solo puede leer el usuario del job (nunca en ~/.kube/config del runner), y apunta KUBECONFIG a él.

Sus salidas son kubeconfig (la ruta del fichero), expires-at y clusters (los ids de los clusters a los que llega la credencial, como array JSON). Dale un id al paso, como id: kubelatch, para leerlas como ${{ steps.kubelatch.outputs.expires-at }}.

La action no tiene dependencias: dos ficheros cortos de JavaScript, src/main.js y src/action.js, que puedes leer en su repositorio antes de fiarte de ella. Si la política de tu organización lo pide, fíjala por commit (picaportelabs/kubelatch-action@<sha>) en lugar de @v1.

En un runner autogestionado, la action confía en el almacén de certificados del sistema, donde suele estar una CA privada, igual que curl, y además en el fichero de NODE_EXTRA_CA_CERTS si el job lo define. Pasa por HTTPS_PROXY (kubelatch y GitHub van los dos por https) salvo que NO_PROXY deje fuera a kubelatch.

Comprueba que funciona

Lanza el workflow (workflow_dispatch o un push a la rama configurada) y confirma:

  • El job imprime bot:<nombre-del-bot> en kubectl auth whoami.
  • La credencial aparece en «Credenciales» (para un administrador) con el nombre github-actions, marcada, por ejemplo, «token de CI · run 1234 · octocat»; su detalle trae la nota owner/repo@refs/heads/main · run <id> · <actor>.
  • Las peticiones quedan en la auditoría con ese bot como sujeto.

Relanzar el job pide un id_token nuevo (cada token tiene su propio jti, así que un token capturado no sirve dos veces).

Si algo falla

El error de la action dice la causa y enlaza aquí.

  • this job cannot ask GitHub for an OIDC token: al workflow o al job les falta permissions: id-token: write.
  • GitHub answered the OIDC token request with un código: GitHub no pudo emitir el token ni al tercer intento. Relanza el job; si sigue fallando, mira el estado de GitHub.
  • url must be https: kubelatch responde con una credencial, así que la action no acepta http sin cifrar fuera de la propia máquina del runner. Sirve kubelatch por https (Instalar).
  • 401 invalid token: el id_token no pasa la verificación (firma, emisor, audiencia distinta de KUBELATCH_BASE_URL, caducado, o ya canjeado antes). Revisa que url sea exactamente el KUBELATCH_BASE_URL del servidor; sin la action, revisa también que no estés reutilizando un token de un job anterior.
  • 403: no trust rule matches this workflow …: el mensaje trae los repository_owner_id, repository_id, ref y environment del token. Pide a un administrador que cree o ajuste la regla de confianza con esos valores, o que quite ref/environment para hacerla genérica.
  • 403 nombrando al bot: el bot está deshabilitado o sin permisos activos en ningún cluster.
  • 404, o answered 201 without a credential: url no es la dirección de tu kubelatch.
  • kubelatch answered 429: llegaron más de 60 canjes por minuto desde la dirección del runner. Relanza el job pasado un minuto.
  • could not reach kubelatch (sin la action, curl: (7) o (35)): el runner no llega a kubelatch por https, o no confía en su certificado. Con una CA privada, añádela al almacén del sistema del runner o apunta NODE_EXTRA_CA_CERTS a ella en el env del job; si no, es un problema de despliegue, no del workflow: pide a un administrador que revise la guía Instalar.

El listado completo de errores está en Errores.

Sin la action

Si tu organización solo permite sus propias actions, estos dos pasos en bash hacen lo mismo con curl y jq, que traen los runners alojados de GitHub. KUBELATCH_URL es la URL de tu kubelatch, igual letra por letra que el KUBELATCH_BASE_URL del servidor.

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
          # The token and kubeconfig are shown exactly once: keep them out of the log.
          printf '%s' "$BODY" | jq -r .token | xargs -I{} echo "::add-mask::{}"
          # Only this job's user may read it; never ~/.kube/config.
          (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
  • Request the OIDC token: pide a GitHub un id_token con la audiencia KUBELATCH_URL y lo enmascara en el log.
  • Exchange it: lo canjea en POST /v1/ci/github-actions/token. Un 201 trae credential, token, kubeconfig, clusters y expires_at, una sola vez. El paso enmascara el token y guarda el kubeconfig en $RUNNER_TEMP, nunca en ~/.kube/config del runner.