Skip to content

Sign in from GitHub Actions with a trust - #41

Merged
camilb merged 4 commits into
mainfrom
feature/github-actions-oidc
Oct 23, 2026
Merged

camilb merged 4 commits into
mainfrom
feature/github-actions-oidc

Conversation

@camilb

@camilb camilb commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

In a GitHub Actions job, the CLI can now sign in with a trust instead of a stored token. Backend: edkaio/edka#854.

permissions:
  contents: read
  id-token: write
env:
  EDKA_TRUST: <trust id>
steps:
  - run: edka apps publish ./edka
  - run: edka apps update jude --cluster orion --yes --wait

Change

  • internal/auth/actions.go: with EDKA_TRUST or --trust set, the CLI requests an OIDC token from ACTIONS_ID_TOKEN_REQUEST_URL with the API origin as audience, and exchanges it at POST /api/cli/github-actions/token for a 15-minute access token. It exchanges a new GitHub token two minutes before expiry, so a long --wait keeps working, and saves nothing. The runner's request token goes only to its own URL, over HTTPS (HTTP on loopback for a simulated runner), never through a redirect. A trust ID that is not a UUID, a job without id-token: write, and EDKA_TOKEN together with EDKA_TRUST fail before any request; edka login refuses while EDKA_TRUST is set.
  • api.Error carries the trust, so a 401/403 hints at the trust in the console instead of edka login; a refused sign-in names what Edka compares (repository, ref, environment, workflow file).
  • edka trusts list|get|create|delete. create resolves --clusters names to IDs, builds the grants from --app and --allow, and prints the job settings for the new trust.
  • internal/catalog/operations.json regenerated from the backend branch with bin/cli-catalog.py. It adds the five /api/trusts routes, and also the routes that landed on the backend since the last regeneration (mobile devices and step-up, notification channels, node disk and local-disk routes, Postgres collation and image move).
  • docs/security.md, and the agent skill via make skill.

Verification

  • make test and make check pass.
  • internal/auth/actions_test.go: the audience sent is the API origin, the exchange body names the trust and the GitHub token, a token is reused until two minutes before expiry and then exchanged with a fresh GitHub token, HTTP runner URLs off loopback are refused, a redirect does not carry the request token, a refusal shows Edka's message with the sign-in hint, and the audience matches URL.origin (case, default ports).
  • internal/cli/trusts_test.go: apps update --cluster sinaia --yes --wait through a simulated runner and exchange: one exchange when the token outlasts the command, and a fresh exchange per request when every token is about to expire, with the newest token on the last request. Plus refusal messages, the id-token: write check, EDKA_TOKEN with EDKA_TRUST, login, and trusts create/delete.
  • End to end, this binary against the backend branch on a local port with a simulated runner signing ID tokens: whoami, apps publish, apps update --wait, and refusals for another app's package, deployments list, trusts list and a job on another branch.
  • Not run: a job on GitHub's real runners; that comes with the prezly/jude workflow once the backend ships.

🤖 Generated with Claude Code


Note

Medium Risk
Introduces a new authentication path (OIDC token exchange and short-lived trust tokens) with broad CLI integration; behavior is heavily tested but misconfigured trusts or grants could block or over-limit CI deploys.

Overview
Adds GitHub Actions trust sign-in so CI jobs can call Edka without storing EDKA_TOKEN. With EDKA_TRUST or --trust, the CLI fetches a runner OIDC token (API origin as audience), exchanges it at POST /api/cli/github-actions/token for a 15-minute in-memory access token, and re-exchanges ~2 minutes before expiry so long --wait runs stay authenticated. Runner request tokens are sent only to the Actions URL over HTTPS (loopback HTTP for tests), with validation that blocks mixing EDKA_TOKEN and EDKA_TRUST and blocks edka login when a trust is set.

New edka trusts commands (list, get, create, delete) manage workflow trusts (repo, refs/environments, workflow file, publish/update grants) and print suggested job permissions / env after create. api.Client / api.Error carry trust context so 401/403 messages point at console trust settings instead of edka login, with richer hints on exchange refusal.

internal/catalog/operations.json is regenerated (trust routes plus other backend additions such as mobile, node disk, Postgres collation). Security docs and the agent skill document the trust flow and global --trust flag.

Reviewed by Cursor Bugbot for commit 58016b5. Configure here.

With EDKA_TRUST or --trust set in a GitHub Actions job that has
`id-token: write`, the CLI asks the runner for an OIDC token with the API
origin as its audience and exchanges it at
POST /api/cli/github-actions/token for a 15-minute access token of the
trust. It exchanges a new GitHub token two minutes before expiry, so a
long --wait keeps working, and saves nothing. The runner's request token
goes only to its own URL, over HTTPS, and never through a redirect.

`edka trusts list|get|create|delete` manage trusts, and the catalog is
regenerated from the backend that adds the /api/trusts routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 58016b5. Configure here.

Comment thread internal/cli/trusts.go Resolved
Comment thread internal/auth/actions.go Resolved
camilb and others added 3 commits October 23, 2026 14:37
…enewal

`ui.Clean` drops newlines, so `trusts create` and `trusts get` printed the
job settings on one line. They go through `ui.CleanText` now.

A renewal starts two minutes before the token expires. When it fails, the
token still serves while it has more than 30 seconds left, and GitHub's
refusal of an OIDC token request is an API error, so a wait retries a 5xx
or 429 like any other read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trust ref pattern now takes +, as Git does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A trust's update grant refuses an empty cluster list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@camilb
camilb merged commit a0832a0 into main Oct 23, 2026
9 checks passed
@camilb
camilb deleted the feature/github-actions-oidc branch October 23, 2026 12:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant