GitHub Actions with OIDC
Run a GitHub Actions job with its secrets and no stored credential. The job proves which repository it runs in, using an ID token from GitHub, and Envshed returns a short-lived, read-only session. There is no ENVSHED_TOKEN to store, rotate or leak.
For other platforms, use a machine token. The order of preference is in Hosted Agents and Routines.
Usage
envshed run --oidc <policy> [-o <org>] [-p <project>] [-e <env>] -- <command...>
Or set ENVSHED_OIDC_POLICY instead of the flag. The flag wins when both are set.
This needs a CLI release that includes --oidc.
Set it up
1. Find the repository's ids
Envshed matches numeric ids rather than names, so a repository deleted and created again under the same name does not match. With the GitHub CLI:
gh api repos/OWNER/REPO --jq '.id, .owner.id'
This prints the repository id, then the owner id.
2. Create a trust policy
Open your organization, choose Hosted agents, then New trust policy. Only owners and admins can manage policies.
| Field | Description |
|---|---|
| Project, environment | What a session can read. One environment. |
| Keys | Optional. Without it, every key in the environment. |
| Repository | owner/repo, for people to read. Matching uses the ids below. |
| Repository id, owner id | From step 1. Both must match. |
| Ref | Optional. The exact ref allowed, such as refs/heads/main. |
| Workflow file | Optional. The file allowed, such as .github/workflows/deploy.yml. |
| GitHub environment | Optional. The environment the job must use. With required reviewers on it in GitHub, a person approves each run. |
| Session length | Up to 60 minutes. |
Without a ref or a GitHub environment, any branch in the repository can sign in. A workflow condition does not prevent that: it checks the file's path, and someone who can push a branch can edit that file on their branch. Use a protected branch as the ref.
A policy must have one of the two unless the environment's name marks it as non-production, such as development or staging. Names that are unknown are treated as production.
The policy's conditions cannot be edited. To change them, create a new policy and delete the old one.
3. Use it in the workflow
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
- name: Run with secrets
run: npx envshed@0 run --oidc 11111111-1111-4111-8111-111111111111 -- npm test
The policy id is not a secret. Without id-token: write, GitHub does not give the job an ID token, and the command stops and says so.
The policy decides the organization, project and environment, so you do not need .envshed.json or flags. A flag that names something else stops the command with an error. A default in .envshed.json is ignored.
What the session can do
It is read-only and limited to the policy's environment and keys, and it answers on the same routes as an agent session. Everything else answers 403.
Disabling or deleting the policy ends its live sessions on their next request, and enabling it again does not bring them back. There is no way to revoke a single session.
The token stays out of sight
The CLI keeps the session token in memory. It does not print it, store it or pass it to your command. The command's environment also leaves out the variables used to ask GitHub for another ID token, but this is not a boundary: on Linux a process can read them from its parent's /proc entry, and a later step gets its own copy from the runner. What decides whether a job can sign in is id-token: write on that job, so give it only to jobs that need it.
Secret values in your command's output are masked with [ENVSHED:KEY_NAME] under --oidc, because GitHub's own log masking only knows values from secrets.X. --no-redact turns this off. A machine token in GitHub Actions needs --redact, as the snippet on the Hosted agents screen shows.
The CLI does not emit an ::add-mask:: command for the token. Doing so would write the token to stdout, where anything wrapping the CLI could read it, to guard a value that nothing else in the job can see.
--watch is refused, because a session lasts at most an hour. Use a machine token for a long-running process.
If ENVSHED_TOKEN is also set, the command stops with an error rather than choosing one.
What the server checks
| Check | |
|---|---|
| Issuer | Fixed to https://token.actions.githubusercontent.com. Nothing in a token changes where keys come from, so a token cannot make the server fetch a URL. |
| Keys | From GitHub's published key set, at a fixed URL. |
| Algorithm | RS256 only. |
| Audience | Must be this Envshed server's own URL. The CLI asks GitHub for a token with that audience. |
| Time | exp and nbf, with 30 seconds of clock tolerance. |
| Repository and owner | repository_id and repository_owner_id, compared exactly. A renamed repository keeps working. One recreated under the same name does not match, and neither does one transferred to another owner. |
| Ref and workflow | Exact match on ref, and on the file path in workflow_ref, when the policy sets them. workflow_ref names the calling workflow, not a reusable workflow it calls. |
| GitHub environment | Exact match on environment, when the policy sets it. |
| Event | Only push, pull_request, merge_group, schedule, workflow_dispatch, repository_dispatch, release and deployment. Others, such as pull_request_target, issue_comment and workflow_run, can be started by outsiders and are refused. |
| Replay | An ID token works once. A job that needs another session asks GitHub for another ID token, which the CLI does on every run. |
Every refusal answers the same 401, so a caller learns nothing about why. Requests are rate limited per IP. The reason goes to the audit log: your organization's log when the job is in the repository the policy names, otherwise the global log. Any GitHub user can get a validly signed ID token from their own repository, so a signature alone does not earn a place in your log.
GitHub Enterprise Server and enterprises with a customized issuer are not supported.
Exit codes
| Code | Meaning |
|---|---|
| 1 | No ID token available, the exchange was refused, or the options conflict |
Otherwise run exits with your command's exit code.
What this does not protect against
This reduces accidental leaks. It is not a barrier against an agent with shell access.
Your command receives the values it asked for, and anything running in that command can read them. Anyone who can change the workflow on a ref the policy allows can read everything the policy allows. Keep the keys to what the job needs, and use a protected branch as the ref.
A ref condition does not vet the code a job runs. Events that run in the context of the default branch, such as pull_request_target, are refused for this reason. A workflow on an allowed event that runs code it did not review still hands that code the session.