How do you configure a GitHub Actions job to assume a cloud role through OIDC?
answer
- one permission scope makes it possible at all
- the runner gets two request environment variables
- the cloud must trust one issuer URL
- two claims carry the policy decision
- one claim form ties it to an approval gate
basics
~20 sGrant the job permissions: id-token: write, use the provider's login action to exchange the GitHub-issued token for temporary credentials, and on the cloud side trust GitHub's issuer while pinning the sub claim to the specific repository, branch, or environment.
solid answer
~40 sTwo halves have to line up. In the workflow, the job needs `permissions: id-token: write` — without it the OIDC request environment variables are absent and the login step fails. A login action such as `aws-actions/configure-aws-credentials` then requests a signed JSON Web Token from GitHub with a given audience and trades it for short-lived credentials, which it exports for subsequent steps. On the cloud side you register GitHub's issuer, `https://token.actions.githubusercontent.com`, as an identity provider and write a trust policy that checks two claims: `aud`, which must match the audience the action requests, and `sub`, which encodes the repository and the context — `repo:my-org/my-repo:ref:refs/heads/main` or `repo:my-org/my-repo:environment:production`. The `sub` condition is the security control; a wildcard such as `repo:my-org/*:*` lets any workflow in the org assume the role.
code
yaml · 14 linesjobs:
deploy:
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write # required to mint the OIDC token
contents: read # restated, because the map is exhaustive
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
aws-region: eu-central-1
- run: aws s3 sync ./dist s3://my-bucket --deletego deeper
Recognise the shape: a permissions block with id-token: write, then a cloud login action that names a role instead of a key. Know that no long-lived credential is stored in the repository.
Explain the exchange — GitHub mints a signed token for the job, the action trades it for temporary cloud credentials — and name the claims the cloud checks, especially aud and sub.
Design the trust: precise sub patterns per role, environment-scoped subjects for production, aud verified, and a diagnosis path for the missing-request-URL failure. Explain why a repository-only match is inadequate.
Own federation across the estate: which roles exist, how sub patterns are generated and reviewed as repositories change, how renames and forks are handled, and what the migration plan is for remaining stored keys.
## The two sides OIDC federation replaces a stored cloud key with a token GitHub mints per job. Getting it working is always the same two-sided exercise: the workflow must be allowed to ask for a token, and the cloud must be configured to believe it. ## Workflow side The job requires the `id-token` scope: permissions: id-token: write contents: read Remember that the permissions map is exhaustive, so `contents: read` has to be restated for `actions/checkout` to work. With the scope granted, the runner receives `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN` in its environment; a login action calls that endpoint (in a JavaScript action, via `core.getIDToken(audience)`) and receives a signed JWT. The absence of those variables is exactly the failure mode when `id-token: write` was forgotten, and the error text names the missing URL rather than the missing permission — worth recognising. A typical AWS job: jobs: deploy: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::123456789012:role/gha-deploy aws-region: eu-central-1 - run: aws s3 sync ./dist s3://my-bucket Google Cloud and Azure have their own login actions with the same shape. In every case no long-lived key appears anywhere in the repository — only a role identifier, which is not a secret. ## The token's claims The JWT GitHub issues carries claims describing the run. The ones you build policy on: - `iss` — always `https://token.actions.githubusercontent.com`. - `aud` — the audience the action requested. The AWS action defaults to `sts.amazonaws.com`; other providers use their own value. - `sub` — the subject, encoding what ran. Its shape depends on context: `repo:OWNER/REPO:ref:refs/heads/main` for a branch push, `repo:OWNER/REPO:pull_request` for a pull-request run, and `repo:OWNER/REPO:environment:production` when the job declares an environment. - Additional claims such as `repository`, `repository_owner`, `job_workflow_ref`, and `workflow_ref` are present and can be matched by providers that support richer conditions. ## Cloud side Register GitHub as an OIDC identity provider using that issuer URL, then attach a trust policy to the role that constrains both `aud` and `sub`. The whole security of the arrangement lives in the `sub` condition. Three common mistakes: 1. **Wildcarding the repository** (`repo:my-org/*:*`) — now every workflow in the organization, including one added by any repository admin, can assume the role. 2. **Matching only the repository, not the ref** — a pull-request run from a branch in that repository can then assume a production role, which turns a normal contribution into a production credential. 3. **Omitting the `aud` check** — leaves the role reachable by a token minted for a different audience. The environment form of `sub` is the strongest common pattern: pin the role to `repo:my-org/my-repo:environment:production` and only a job that declares `environment: production` — and therefore only a job that has passed that environment's reviewers and branch policy — produces a token the role will accept. Credential issuance and deployment approval become the same gate. ## What this buys, operationally The credentials the job holds are minted per run and expire quickly, so there is nothing to rotate, nothing to leak from a repository settings page, and nothing that survives the job. Revocation is a policy edit rather than a key rotation. The cost is setup complexity and the fact that `sub` patterns must be maintained as repositories are renamed or branches are restructured — a rename silently breaks the trust, which is a good failure mode but one worth documenting. ## Answering well Walk the interviewer through both halves in order: `id-token: write`, login action with an audience, issuer registered in the cloud, trust policy conditioned on `aud` and a precise `sub`. Then volunteer the pinning discussion — that is the part that separates someone who has copied a snippet from someone who has designed the trust.
- A job's OIDC login fails with a missing ACTIONS_ID_TOKEN_REQUEST_URL. What is the cause?The job does not have `permissions: id-token: write`. Those request variables are injected only when the scope is granted, and because the permissions map is exhaustive, adding some other scope elsewhere in the workflow can remove it. Add `id-token: write` to that job, keeping `contents: read` alongside it if the job checks out code.
- How do you scope a cloud role so only approved production deploys can assume it?Condition the trust policy's `sub` claim on the environment form, `repo:my-org/my-repo:environment:production`, and put the deploy job in that environment with required reviewers. The claim only takes that shape when the job declares the environment, and the job only starts after the environment's protection rules pass, so approval and credential issuance become one gate.
- Why is a trust policy matching only the repository considered too loose?Because every run in that repository — including a pull-request run proposing a workflow change, or a scheduled job on a stale branch — produces a token whose `sub` matches. Anyone who can cause a workflow to run in the repository can then obtain the role's credentials. Pin the ref or the environment as well, and check the `aud` claim.
saying these in an interview costs you the question
- Forgets id-token: write and blames the login action
- Wildcards the sub claim across the whole organization
- Trusts the repository without pinning ref or environment
- Thinks OIDC still needs a stored key as fallback
- Believes the token is the GITHUB_TOKEN reused