skip to content

A GitHub Actions workflow must deploy to AWS with no access keys stored as repository secrets. How do you set that up with an IAM OIDC identity provider, and which condition must the role's trust policy contain?

level: seniorimportance: should knowfreq 55%

answer

  1. no secret in the repository at all
  2. a token GitHub signs per job
  3. the claim that says which repo
  4. aud proves nothing about ownership
  5. pin to branch, tag or environment

basics

~20 s

Register token.actions.githubusercontent.com as an IAM OIDC identity provider, create a role that trusts it for sts:AssumeRoleWithWebIdentity, and condition the trust policy on both the aud claim and the sub claim so only your repository and branch can assume it.

solid answer

~30 s

You create an IAM OIDC identity provider in the account for the issuer `token.actions.githubusercontent.com` with audience `sts.amazonaws.com`, then a deployment role whose trust policy names that provider as a `Federated` principal for the action `sts:AssumeRoleWithWebIdentity`. The workflow job declares `permissions: id-token: write`, and `aws-actions/configure-aws-credentials` fetches a signed OIDC token from GitHub and exchanges it for temporary AWS credentials. The load-bearing part is the condition block: check `token.actions.githubusercontent.com:aud` equals `sts.amazonaws.com`, and — critically — check the `:sub` claim, which encodes `repo:<org>/<repo>:ref:refs/heads/main` or `repo:<org>/<repo>:environment:production`. Without a `sub` condition the role trusts *any* GitHub Actions workflow on the internet.

code

json · 18 lines
json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:environment:production"
        }
      }
    }
  ]
}

go deeper

for a junior

Know that AWS credentials do not belong in repository secrets, and that GitHub Actions can instead present a signed token AWS trades for a temporary session.

for a middle

Be able to name the three pieces — the IAM OIDC provider, the role trust policy with sts:AssumeRoleWithWebIdentity, and the id-token: write permission in the workflow — and describe the exchange.

for a senior

Show that you understand the sub claim is the actual trust boundary: describe its format, the exposure from omitting it, and why an unanchored wildcard silently widens it to sibling repositories.

for a principal

Own the fleet-level design — one narrow role per account and per pipeline stage, environment-scoped subjects that ride on approval gates, session naming for audit, and a review rule that catches over-broad trust conditions before they merge.

## What problem this solves The traditional pattern is an IAM user with access keys pasted into GitHub repository secrets. Those keys never expire, they are readable by anyone who can add a workflow to the repository, and they survive long after the person who created them leaves. OIDC federation removes the stored secret entirely: GitHub signs a short-lived JSON Web Token asserting facts about the running job, and AWS trades that token for a temporary session. ## The three pieces **1. The identity provider object.** In IAM you register the issuer `https://token.actions.githubusercontent.com` as an OIDC identity provider with the client ID (audience) `sts.amazonaws.com`. This is a one-off per account. It tells IAM that tokens signed by GitHub's keys, for that audience, are worth evaluating. **2. The role and its trust policy.** The role holds the deployment permissions in its identity policy, and its trust policy decides who may assume it: ```json { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", "token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main" } } } ``` The condition keys are the OIDC issuer's hostname followed by a colon and the claim name — that syntax is how AWS exposes web-identity token claims to policy. **3. The workflow side.** The job needs `permissions: id-token: write` (this is a GitHub permission to *request* a token, nothing to do with AWS), and typically uses the `aws-actions/configure-aws-credentials` action with `role-to-assume` and `aws-region`. The action requests the token, calls `AssumeRoleWithWebIdentity`, and exports temporary credentials into the job's environment for every later step. ## The sub claim is where the security lives The `aud` claim proves the token was minted for AWS. It proves nothing about *whose* workflow produced it, because every GitHub OIDC token for AWS carries the same audience. A trust policy that checks only `aud` is trusted by every repository on GitHub — anyone can push a workflow that assumes your role and takes your deployment permissions. The `sub` claim carries the identity that matters, in a structured form: - `repo:my-org/my-repo:ref:refs/heads/main` — a push or run on that branch - `repo:my-org/my-repo:ref:refs/tags/v1.2.3` — a tag - `repo:my-org/my-repo:pull_request` — a pull-request event - `repo:my-org/my-repo:environment:production` — a job targeting that GitHub environment The scoping mistakes, in descending order of frequency: 1. **No `sub` condition at all.** Global exposure. 2. **`repo:my-org/*`** with `StringLike`. Any repository in your organisation deploys to production, including a new one created by anyone with repo-create rights. 3. **A trailing wildcard in the wrong place.** `repo:my-org/my-repo*` also matches `my-org/my-repo-fork` and `my-org/my-repo-scratch`. Wildcards belong *after* a colon-delimited segment boundary, as in `repo:my-org/my-repo:*`. 4. **`repo:my-org/my-repo:*`** for a production role. Legitimate for a read-only or plan role; for a deploy role it means a pull request from a fork-triggered workflow, or any branch, can assume it. Pin to the branch, tag pattern, or environment. The strongest pattern for production is the **environment** subject, because GitHub environments carry their own protection rules — required reviewers, branch restrictions, wait timers — so approval and the AWS trust condition reinforce each other. ## Operational notes - **Scope the role's permissions too.** Federation gets rid of the stored key; it does not excuse `AdministratorAccess` on the deploy role. Separate plan/read roles from apply/write roles. - **Set a short `Duration`.** The action supports a role session duration; a deploy rarely needs hours. - **`role-session-name`** shows up in CloudTrail, so set it to something that identifies the workflow run and makes an audit trail readable. - **Multi-account deploys** are cleanest as one role per target account, each with its own narrow trust condition, rather than one hub role that chains outward. - **Self-hosted runners change nothing here** — the token comes from GitHub's issuer regardless of where the job runs. ## The shape of a good answer Describe the three pieces, then spend most of the answer on the `sub` condition and the exposure created by omitting it. An interviewer asking this question is almost always listening for whether you know that `aud` alone is worthless.

  • Why is `repo:my-org/my-repo*` a dangerous value for the sub condition?
    Because the wildcard is not anchored at a segment boundary, so it also matches repositories whose names merely start the same way — `my-repo-fork`, `my-repo-sandbox`. Anyone able to create such a repository in the organisation inherits the deploy role. Wildcards belong after a colon, as in `repo:my-org/my-repo:*`.
  • What does pinning the sub to a GitHub environment buy over pinning to a branch?
    Environments carry protection rules — required reviewers, allowed branches, wait timers — enforced by GitHub before the job starts. The AWS trust condition then only accepts tokens from jobs that passed those gates, so human approval and the cloud trust boundary reinforce each other instead of being two independent controls.
  • The workflow fails with an error about not being able to request an ID token. What is missing?
    The job or workflow has not been granted `permissions: id-token: write`. Without it GitHub refuses to mint the OIDC token, so nothing reaches AWS at all. It is a GitHub-side permission and unrelated to the AWS trust policy — the giveaway is that the failure happens before any AWS API call.
  • Does federating remove the need to scope the deploy role's permissions?
    No. It removes the stored long-lived secret and shortens the credential's life, but whatever the role can do is exactly what a successful assumption gets. Keep the identity policy narrow, split read/plan from write/apply roles, and prefer one role per target account so a compromise of one pipeline does not reach the others.

saying these in an interview costs you the question

  • Checking the aud claim is enough to scope the role
  • Repository secrets are safe because only admins see them
  • Any repo in the org is a fine trust boundary for production
  • OIDC removes the need to limit the role's permissions
  • Self-hosted runners make the trust condition unnecessary

context