Skip to main content

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​

terminal
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:

terminal
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.

FieldDescription
Project, environmentWhat a session can read. One environment.
KeysOptional. Without it, every key in the environment.
Repositoryowner/repo, for people to read. Matching uses the ids below.
Repository id, owner idFrom step 1. Both must match.
RefOptional. The exact ref allowed, such as refs/heads/main.
Workflow fileOptional. The file allowed, such as .github/workflows/deploy.yml.
GitHub environmentOptional. The environment the job must use. With required reviewers on it in GitHub, a person approves each run.
Session lengthUp to 60 minutes.
Set a ref for anything sensitive

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​

yaml
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
IssuerFixed 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.
KeysFrom GitHub's published key set, at a fixed URL.
AlgorithmRS256 only.
AudienceMust be this Envshed server's own URL. The CLI asks GitHub for a token with that audience.
Timeexp and nbf, with 30 seconds of clock tolerance.
Repository and ownerrepository_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 workflowExact 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 environmentExact match on environment, when the policy sets it.
EventOnly 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.
ReplayAn 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​

CodeMeaning
1No 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.