Skip to content

CI: trust rules

A trust rule lets a repository's GitHub Actions workflows get a bot's credential, with no secret stored in GitHub. Here's how to create one in CI and how to scope it well.

How it works

The workflow asks GitHub for an OIDC token with audience KUBELATCH_BASE_URL and exchanges it at POST /v1/ci/github-actions/token. kubelatch verifies the token, looks up the repository's trust rule and, if it matches, responds once with the bot's credential and its kubeconfig. What exactly is verified is in Security model.

That token is GitHub Actions' own and has nothing to do with OIDC login, where people sign in through your company's identity provider: trust rules work the same with either login, or with none.

sequenceDiagram
  participant W as Workflow
  participant G as GitHub
  participant K as kubelatch
  W->>G: request OIDC token (audience = KUBELATCH_BASE_URL)
  G-->>W: signed id_token
  W->>K: POST /v1/ci/github-actions/token
  K->>K: verify signature, aud, jti and trust rule
  K-->>W: bot credential + kubeconfig

The credential lasts KUBELATCH_CI_TOKEN_TTL (1 hour by default; can't exceed KUBELATCH_MAX_TTL_BOT). It shows up in Credentials with the name github-actions and a note with the repository, ref, run and actor; its row reads CI token · run 1234 · alice. Every kubectl from the workflow is logged in the audit log as bot:<name>.

Before you start

  • A bot with permissions: create it in Users and grant it a tier in Permissions (Users and bots).
  • The runners reach kubelatch over https with a certificate they trust. A kubelatch on a laptop or a private network won't do. With a private CA, use KUBELATCH_KUBECONFIG_CA (Install).

Create a trust rule

  1. In CI, click New trust rule. The form opens in a panel on the right (from a bot's page, the CI rules card has the same button with the bot already chosen); fill it in:

    Field What to put
    Repository owner/repo, for example acme/app. With the GitHub App (GitHub login), kubelatch asks GitHub for the numeric ids of the repository and its owner and keeps the repository's current name. For a repository the App can't see, give the ids yourself (below).
    Condition What the workflow's ref must be: Branch… (type main: the rule keeps refs/heads/main), Tag… (v1.0 is refs/tags/v1.0), Any workflow (no ref) or Another ref… (a full ref, such as refs/heads/main).
    Environment (optional) The GitHub environment, for example prod.
    Bot The bot whose credential the workflow will receive. Only enabled ones show up, each with what it holds; under the field, The workflow gets: repeats it, for example viewer on kind-local.
    Name A name to recognize it by, for example app deploy.
  2. Read the warnings under the field they concern: the missing ref under Any workflow, pull_request_target under a branch, and the unprotected environment under Environment (optional). Click Create rule.

  3. The panel becomes Connect the workflow (The workflow side). Click Done once you have copied the snippet.

Without the GitHub App kubelatch can't ask GitHub, so you give it the ids. With the App you can give them too, for a repository it can't see (a private one outside its installation, or one of an organization where it isn't installed). Look up the numeric ids of the repository and its owner:

gh api repos/<owner>/<repo> --jq '[.owner.id, .id]'
# or without gh:
curl -s https://api.github.com/repos/<owner>/<repo> | jq '.owner.id, .id'

and click I have the ids, under Repository: Owner ID is the first number (repository_owner_id) and Repository ID the second (repository_id). The form shows that gh command under Command that prints the ids, filled in with the repository you typed. Without the App, that name is optional and only labels the rule. With the App, Repository becomes optional too and the name only fills the command: kubelatch doesn't ask GitHub, trusts the repository by those ids alone, and the rule is listed by them, as owner_id / repo_id, with no name.

kubelatch compares by numeric ids, not by names: repositories and organizations get renamed and their names get recycled. There can only be one rule per repository, ref and environment. If GitHub doesn't know the repository, or the App can't see it (for a private one, the App's installation must include it), or it was renamed or moved, the panel says so and keeps what you typed.

How the rule is chosen

Any workflow leaves the ref empty, and an empty Environment (optional) leaves the environment empty: the rule then applies to any ref, or any environment, of the repository. With them set, only to that exact ref and that environment. If several match, the most specific one wins: one with an environment beats one with a ref, and both beat the generic one.

Pin the ref for anything that isn't read-only

A rule with Any workflow also applies to workflows from a pull request from a branch in the same repository (refs/pull/N/merge). Anyone with push permission could get the bot's credential by editing a workflow in a PR. PRs from a fork don't get an id_token on pull_request.

Two more limits:

  • Don't combine a rule with pull_request_target workflows that check out the PR's code. They run with the base branch's ref, not the ref of the code they execute.
  • A rule with an environment only protects if that environment exists in GitHub with protection rules (reviewers, branch policy). If it doesn't exist, GitHub creates it the first time, unprotected.

The workflow side

After Create rule, the panel becomes Connect the workflow: a Workflow snippet filled in for that rule, with your URL, the trigger of its branch or tag and its environment, ready to paste into the repository. Example workflow, in the header of CI (and in the empty list), has the generic one. The full example is examples/github-actions/kubelatch.yml in the repository. The essentials:

  • permissions: id-token: write.
  • The step uses: picaportelabs/kubelatch-action@v1 with url set to KUBELATCH_BASE_URL: it's the token's audience.
  • If the rule sets an environment, the job declares environment: "<name>". The snippet writes the name between quotes, so a colon or a # in it can't change which environment it names.

The guide for whoever writes the workflow is in Credentials for GitHub Actions.

Check that it works

  1. Run the workflow (workflow_dispatch or a push to the rule's ref).
  2. The job prints bot:<name> in kubectl auth whoami and the pod list.
  3. A github-actions credential with the run's note shows up in Credentials, as CI token · run … · actor.
  4. In CI, the rule's Last exchange stops reading Never exchanged, the exchanged today chip counts the rule, and the sentence at the top counts the credential.
  5. In Audit, the requests have the bot as subject. In Control plane there's a credential.issue with via: github-actions.

Each run gets a new token: relaunching the job works. If it fails, see Troubleshooting.

Read the list

CI reads each rule as a sentence: the repository, the condition, the environment and the bot it hands, for example acme/app · refs/heads/main · environment prod → deploy-bot. A rule with no ref says any workflow on an amber chip. The rule's name sits under it, and Last exchange says when a workflow last exchanged a token through it (Never exchanged until one does). Hover the repository to read its numeric ids; a rule written with ids only shows them as 65 / 74. A sentence at the top counts the rules and the bots they hand, and the CI tokens issued since midnight (4 rules for 2 bots. 12 credentials exchanged today.). Two chips count rules and filter the list: 1 with no ref (the rules that take any workflow) and 3 exchanged today (the rules a workflow exchanged a token through since midnight); press one to see them, press both to combine them, and share the page's address, which keeps them. A chip with nothing to count isn't drawn. Filter searches the repository or its ids, the ref, the environment, the bot and the name. The row menu has Requests (the bot's requests in Audit), Credentials (its CI tokens) and Delete.

Delete a rule

In CI, open the actions menu at the end of the rule's row, choose Delete and confirm. That repository's workflows stop getting credentials, but the ones already issued keep working until they expire (under KUBELATCH_CI_TOKEN_TTL). Revoke them in Credentials if needed: Credentials in the rule's row menu opens that bot's CI tokens. Disabling the bot does revoke all its credentials instantly.

GitHub Enterprise Server

Point KUBELATCH_GITHUB_ACTIONS_ISSUER at https://<server>/_services/token. The rest doesn't change. See Configuration.