Resource library

QA How-To

Use GitHub Actions OIDC for Test Environments

Follow this github actions oidc test environments tutorial to replace cloud secrets with short-lived AWS credentials and protected deployment workflows.

22 min read | 2,851 words

TL;DR

Create an AWS IAM OIDC provider for GitHub, trust only the repository's test GitHub Environment, and let the workflow exchange its GitHub identity token for temporary role credentials. Protect the job with environment rules, verify the AWS identity, deploy, test, and upload evidence without storing an AWS access key in GitHub.

Key Takeaways

  • Use GitHub's OIDC token to exchange repository identity for short-lived AWS credentials.
  • Bind the AWS trust policy to one organization, repository, audience, and GitHub Environment.
  • Grant id-token write only to the job that needs to assume the cloud role.
  • Keep the role permission policy narrower than its trust policy and limited to test resources.
  • Use a protected GitHub Environment to control approvals, branches, variables, and deployment history.
  • Verify the assumed identity and deployed endpoint without printing tokens or credentials.
  • Capture test evidence as artifacts and clean up temporary resources even after failures.

A github actions oidc test environments tutorial should end with one result: your CI job can deploy to and test a cloud environment without a long-lived cloud key in GitHub. GitHub issues a signed identity token for the job, AWS validates its claims, and AWS Security Token Service returns temporary credentials for a narrowly scoped IAM role.

You will build that flow for an AWS test environment and verify it from the workflow. This is one security-focused implementation inside the broader test automation CI/CD complete guide, which explains pipeline stages, test selection, evidence, and release gates.

The example uses a GitHub Environment named test, an AWS IAM role, an S3-hosted test fixture, and a smoke check. Replace the example bucket operation with your deployment tool after the identity path works. The important design stays the same: GitHub proves workload identity, AWS issues short-lived credentials, and neither system stores a permanent AWS access key.

What You Will Build

By the end of this tutorial, you will have:

  • An AWS IAM OIDC provider that accepts tokens from GitHub Actions.
  • A role trust policy restricted to one repository and its test environment.
  • A least-privilege role policy that can update one test bucket only.
  • A protected GitHub Environment with non-secret configuration variables.
  • A workflow that assumes the role, publishes a test fixture, runs a smoke check, and stores evidence.
  • Verification commands that prove which identity ran and whether the deployed content is reachable.

The trust path is separate from the permission path. The trust policy answers, Who may assume this role? The role permission policy answers, What may that assumed identity do? Treat both as required controls. A tightly restricted trust policy with an administrator permission policy is still dangerous, and a narrow permission policy with a broad trust policy can still expose test data or consume cloud resources.

Prerequisites

Use a GitHub repository where you can edit Actions workflows and repository environments. In AWS, use an account where you can create an IAM identity provider, role, and policy. You also need an existing, uniquely named S3 bucket for disposable test content. Do not point the tutorial at a production bucket.

Install these local tools:

aws --version
gh --version
git --version

Use AWS CLI v2 and a current GitHub CLI release. Authenticate locally with an administrator or infrastructure role only for setup:

aws sts get-caller-identity
gh auth status

Set shell variables for the examples. Substitute your real values before running commands:

export AWS_ACCOUNT_ID=123456789012
export AWS_REGION=us-east-1
export GITHUB_ORG=example-org
export GITHUB_REPO=web-app
export TEST_BUCKET=example-org-web-app-test

The repository must have GitHub Actions enabled. AWS IAM names are case-sensitive where shown, and the GitHub organization, repository, and environment in the trust policy must match exactly.

Credential approach Lifetime Rotation burden Repository scope Recommended use
Stored IAM access key Until rotated or revoked Manual Depends on secret access Legacy fallback only
GitHub OIDC role Minutes, set by AWS session No stored cloud key Claims restrict repo and environment Normal CI access
Self-hosted runner instance role Runner lifetime Managed by AWS Shared with workloads on runner Dedicated, isolated runners

OIDC removes the stored AWS key, but it does not remove authorization work. You must still protect workflow changes, third-party actions, environments, and IAM permissions.

Step 1: Create the GitHub OIDC Provider in AWS

AWS needs an identity provider representing GitHub's token issuer. Create it once per AWS account, not once per repository. First check whether it already exists:

aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn \
  "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"

If AWS returns NoSuchEntity, create the provider:

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com

The issuer URL must be https://token.actions.githubusercontent.com, and the audience for the standard AWS credentials action is sts.amazonaws.com. AWS validates the TLS certificate through its current OIDC provider behavior, so do not paste a thumbprint copied from an old tutorial unless your organization's infrastructure policy explicitly requires management through another supported path.

Manage this resource with Terraform, CloudFormation, or your normal infrastructure repository after the proof of concept. Creating it by CLI is useful for learning, but reviewed infrastructure code prevents silent configuration drift.

Verify the step: list the configured provider and inspect its audience:

aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn \
  "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com" \
  --query '{Url:Url,ClientIDs:ClientIDList}'

Expect the URL without the https:// prefix and a client ID list containing sts.amazonaws.com. Do not continue if the audience is missing.

Step 2: Create a Trust Policy for the Test Environment

Create github-test-trust.json locally. This policy accepts GitHub's federated identity only when the audience and subject claims match your intended workload:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012: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:example-org/web-app:environment:test"
        }
      }
    }
  ]
}

Replace the account ID, organization, and repository. Keep environment:test if the workflow job will declare environment: test. When a job references a GitHub Environment, the OIDC subject uses the environment form instead of a branch form. This lets environment protection rules and the AWS claim reinforce each other.

Create the role:

aws iam create-role \
  --role-name GitHubActionsWebAppTest \
  --assume-role-policy-document file://github-test-trust.json \
  --description "GitHub Actions access to the web-app test environment"

Do not use a wildcard such as repo:example-org/* for convenience. Do not trust every branch if the job represents an environment. Separate test and production roles so a test workflow can never inherit production permissions.

Verify the step: read back the decoded trust policy:

aws iam get-role \
  --role-name GitHubActionsWebAppTest \
  --query 'Role.AssumeRolePolicyDocument.Statement[0].Condition'

Expect both exact claims. Confirm the subject reads repo:example-org/web-app:environment:test, including capitalization.

Step 3: Attach Least-Privilege Test Permissions

The role can now be assumed, but it has no AWS service permissions. Create github-test-permissions.json for one bucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListTestBucket",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::example-org-web-app-test"
    },
    {
      "Sid": "ManageTestPrefix",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::example-org-web-app-test/oidc-smoke/*"
    }
  ]
}

Replace the bucket name. The object statement allows only the oidc-smoke/ prefix. It cannot modify bucket policy, encryption, public access settings, or unrelated objects. Your real deployment may need ECR, ECS, Lambda, Kubernetes, or CloudFormation permissions. Add only operations observed in a successful test deployment, then remove broad bootstrap permissions.

Create and attach the managed policy:

aws iam create-policy \
  --policy-name GitHubActionsWebAppTestS3 \
  --policy-document file://github-test-permissions.json

aws iam attach-role-policy \
  --role-name GitHubActionsWebAppTest \
  --policy-arn \
  "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/GitHubActionsWebAppTestS3"

A customer-managed policy makes review and reuse clear. An inline policy is also possible, but do not attach AWS managed administrator policies for a CI shortcut.

Verify the step: list attached policies and simulate an allowed and denied action if your setup identity has simulation permission:

aws iam list-attached-role-policies --role-name GitHubActionsWebAppTest
aws iam simulate-principal-policy \
  --policy-source-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:role/GitHubActionsWebAppTest" \
  --action-names s3:PutObject iam:CreateUser \
  --resource-arns "arn:aws:s3:::${TEST_BUCKET}/oidc-smoke/index.html"

Expect s3:PutObject to be allowed for the prefix and iam:CreateUser to be implicitly denied.

Step 4: Configure GitHub Actions OIDC Test Environments Tutorial Controls

Create a GitHub Environment named test in Repository Settings, Environments, New environment. Add an environment variable named AWS_ROLE_ARN with this value:

arn:aws:iam::123456789012:role/GitHubActionsWebAppTest

Add AWS_REGION and TEST_BUCKET as environment variables too. These values are identifiers, not credentials, so variables communicate their purpose better than secrets. The workflow will not need AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY.

Configure deployment branches so only your default branch can use test, or select named branches according to your release model. Add required reviewers if a test deployment can modify shared data, trigger paid infrastructure, or affect external integrations. Remember that environment reviewers approve a job before it can access environment variables and request credentials.

Protect .github/workflows/ and infrastructure paths with CODEOWNERS and branch protection. An attacker who can merge arbitrary workflow code may request the same token and use any permission the role grants. OIDC limits credential lifetime and identity scope, but review controls still protect what trusted workflow code does.

You can also create the environment with the GitHub API, but protection rule capabilities depend on repository and organization features. The settings interface is the clearest portable starting point.

Verify the step: open the environment page and confirm its name is exactly test. Confirm all three variables exist, no long-lived AWS keys exist at repository or environment scope, and the allowed branch rule matches the branch you will run.

Step 5: Add the OIDC Deployment and Smoke Test Workflow

Create .github/workflows/test-environment.yml in your example repository:

name: Test environment OIDC smoke

on:
  workflow_dispatch:
  push:
    branches: [main]
    paths:
      - 'test-fixture/**'
      - '.github/workflows/test-environment.yml'

permissions:
  contents: read

concurrency:
  group: test-environment
  cancel-in-progress: false

jobs:
  deploy-and-test:
    runs-on: ubuntu-latest
    environment: test
    timeout-minutes: 15
    permissions:
      contents: read
      id-token: write
    env:
      AWS_REGION: ${{ vars.AWS_REGION }}
      TEST_BUCKET: ${{ vars.TEST_BUCKET }}
    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Configure short-lived AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ vars.AWS_ROLE_ARN }}
          aws-region: ${{ vars.AWS_REGION }}
          role-session-name: gha-test-${{ github.run_id }}
          mask-aws-account-id: true

      - name: Verify assumed identity
        run: |
          set -euo pipefail
          aws sts get-caller-identity --query Arn --output text | tee assumed-role.txt
          grep -q 'assumed-role/GitHubActionsWebAppTest/' assumed-role.txt

      - name: Publish smoke fixture
        run: |
          set -euo pipefail
          mkdir -p test-fixture
          printf '<h1>OIDC test %s</h1>\n' "${GITHUB_SHA}" > test-fixture/index.html
          aws s3 cp test-fixture/index.html \
            "s3://${TEST_BUCKET}/oidc-smoke/index.html" \
            --content-type text/html

      - name: Verify uploaded fixture
        run: |
          set -euo pipefail
          aws s3 cp "s3://${TEST_BUCKET}/oidc-smoke/index.html" downloaded.html
          grep -Fq "${GITHUB_SHA}" downloaded.html
          aws s3api head-object \
            --bucket "${TEST_BUCKET}" \
            --key oidc-smoke/index.html \
            --query '{Length:ContentLength,Type:ContentType}' \
            --output json | tee smoke-result.json

      - name: Upload test evidence
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: oidc-test-evidence-${{ github.run_id }}
          path: |
            assumed-role.txt
            smoke-result.json
          if-no-files-found: warn
          retention-days: 14

The workflow-level permissions default to read-only content access. The deployment job adds id-token: write, which permits requesting an OIDC token but does not itself grant AWS access. The credentials action requests the token, calls AssumeRoleWithWebIdentity, and exports temporary AWS environment variables for later steps.

The job declares environment: test, so the token subject matches the trust policy. Concurrency prevents two shared-environment deployments from overlapping. cancel-in-progress: false avoids interrupting cleanup or verification halfway through a deployment.

Verify the step: run a YAML-aware editor check, commit the file to an allowed branch, then open the Actions tab. The workflow should be visible as Test environment OIDC smoke. If it does not appear, confirm the file is on the default branch and Actions is enabled.

Step 6: Run and Verify the Short-Lived Credential Flow

Trigger the workflow from the Actions interface, or use GitHub CLI:

gh workflow run test-environment.yml --ref main
gh run list --workflow test-environment.yml --limit 1

If the environment requires approval, open the pending run, review the target environment, and approve it. Watch the job:

gh run watch --exit-status

Open the Verify assumed identity log. It should show an STS assumed-role ARN containing assumed-role/GitHubActionsWebAppTest/gha-test-, not an IAM user ARN. AWS access keys created for the session are masked and expire automatically. Never add a debug step that prints the OIDC token, AWS environment variables, or the output of credential files.

Download and inspect evidence after the run:

RUN_ID=$(gh run list --workflow test-environment.yml --limit 1 --json databaseId --jq '.[0].databaseId')
gh run download "${RUN_ID}" --pattern 'oidc-test-evidence-*' --dir evidence
find evidence -type f -maxdepth 2 -print

You should find assumed-role.txt and smoke-result.json. The JSON should report a positive content length and text/html. This artifact proves the identity check and object verification ran, but it intentionally contains no credentials. For a richer test suite, follow the same evidence discipline described in publishing test evidence as CI artifacts.

Verify the step: confirm the GitHub run is green, the artifact is downloadable, and this command returns object metadata:

aws s3api head-object --bucket "${TEST_BUCKET}" --key oidc-smoke/index.html

Use your local setup identity only for this independent check. The workflow itself must rely on OIDC.

Step 7: Add Negative Tests, Cleanup, and Release Safety

A successful run proves the happy path. Now confirm your controls reject the wrong identity. Create a temporary branch that is not allowed by the environment rule and try to dispatch the workflow. GitHub should block or skip access to the protected environment. Do not weaken the AWS trust policy to make this negative test pass.

You can also temporarily change the workflow job to environment: staging on a disposable branch. The job should fail during role assumption because the token subject no longer equals environment:test. Restore the file immediately after observing the expected denial. A failure such as Not authorized to perform sts:AssumeRoleWithWebIdentity is the correct security result.

Add cleanup as a separate step if the fixture is temporary:

      - name: Remove temporary fixture
        if: always()
        run: |
          set -euo pipefail
          aws s3 rm "s3://${TEST_BUCKET}/oidc-smoke/index.html"

If cleanup runs after a failed credential step, AWS commands will fail because no credentials exist. For production workflows, place deployment and cleanup in separate jobs with explicit conditions, or make cleanup tolerate a missing session while still reporting real deletion failures. Keep shared environments serialized, and use unique prefixes for parallel preview environments.

Flaky tests should not be allowed to obscure whether identity, deployment, or application behavior failed. Apply a documented process for quarantining flaky tests in a CI pipeline, while keeping the credential and deployment verification as required gates.

Verify the step: confirm the unauthorized environment cannot assume the role, the authorized test job still passes, and the temporary object is absent after cleanup:

aws s3api head-object --bucket "${TEST_BUCKET}" --key oidc-smoke/index.html

After cleanup, expect a not-found response.

Troubleshooting

Problem: Could not load credentials from any providers. -> Confirm the job has permissions: id-token: write, declares environment: test, and runs the credentials action before AWS commands. Job-level permissions replace inherited workflow permissions, so include contents: read there too.

Problem: AWS returns Not authorized to perform sts:AssumeRoleWithWebIdentity. -> Compare the trust policy organization, repository, environment, provider ARN, and audience with the workflow. Case and punctuation matter. Check that the role ARN variable points to the role in the same account as the provider.

Problem: The subject claim uses a branch instead of an environment. -> Add environment: test to the job, not merely an ENVIRONMENT variable. GitHub changes the subject format only when the job references a GitHub Environment.

Problem: The environment job waits indefinitely. -> Open the deployment review panel and check required reviewers, allowed branches, and wait timers. A reviewer who triggered the run may be unable to self-approve when self-review prevention is enabled.

Problem: Role assumption succeeds but S3 returns AccessDenied. -> The trust path is working. Check the attached role policy, bucket policy, encryption key policy, exact bucket name, and object prefix. If the bucket uses a customer-managed KMS key, add only the required KMS permissions to both identity and key policies.

Problem: Evidence upload has no files after an early failure. -> Keep if: always() and if-no-files-found: warn. Create a small diagnostic status file before risky steps if every run must produce an artifact, but never copy token or credential values into it.

Best Practices

  • Pin third-party actions to reviewed commit SHAs when your supply-chain policy requires immutable references. Keep an automated process for updating those pins.
  • Use one role per environment and application boundary. Never let a test role deploy to production resources.
  • Restrict the OIDC sub claim and always validate the aud claim. Avoid organization-wide wildcards.
  • Grant id-token: write at the smallest practical job scope. Other jobs do not need it.
  • Put identifiers in environment variables and reserve secrets for values that truly remain secret.
  • Limit session duration, service actions, resources, regions, and object prefixes.
  • Protect workflow and infrastructure changes with review ownership and branch rules.
  • Record CloudTrail events and GitHub deployment history so an incident can connect a role session to a workflow run.
  • Upload useful failure evidence, but never upload tokens, credentials, or unredacted sensitive configuration.
  • Test denial paths after trust-policy or environment-rule changes. Security controls need regression coverage too.

A common mistake is treating OIDC as a replacement for least privilege. It replaces stored credential distribution, not IAM design. Another mistake is allowing pull requests from untrusted forks to reach a privileged environment. Keep privileged jobs on reviewed code and understand the security boundary of each GitHub event before enabling it.

Interview Questions and Answers

Q: Why is OIDC safer than storing an AWS access key in GitHub?

OIDC lets the workflow exchange a signed, short-lived workload identity for temporary AWS credentials. There is no permanent AWS key to copy, forget to rotate, or recover from repository secret exposure. Safety still depends on a narrow trust policy, role permissions, and protected workflow changes.

Q: What does id-token: write permit?

It permits the job to request a GitHub OIDC token. It does not grant write access to repository contents or automatically authorize AWS actions. AWS access begins only if the token claims satisfy the role trust policy.

Q: Why validate both aud and sub?

The audience identifies the intended token recipient, while the subject identifies the GitHub workload. Checking both prevents a token intended for another service or an unrelated repository context from assuming the role.

Q: How does a GitHub Environment affect the OIDC subject?

When a job references an environment, its subject uses repo:ORG/REPO:environment:NAME. The IAM trust policy should match that exact form. The environment can also enforce reviewers, branch rules, wait timers, variables, and deployment history.

Q: Where should least privilege be enforced?

Enforce caller identity in the role trust policy and permitted AWS operations in the role permission policies. Resource policies, KMS key policies, organization service control policies, and GitHub protection rules provide additional boundaries.

Q: How would you debug a failed role assumption?

First verify id-token: write and the job environment. Then compare the provider ARN, audience, subject, role ARN, organization, repository, and capitalization. Separate role-assumption failures from later service authorization failures because they involve different policies.

Q: Should forked pull requests receive cloud credentials?

Normally no. Keep privileged deployment jobs on trusted, reviewed code and protected events. If pull request validation needs cloud resources, use isolated infrastructure and an explicit threat model rather than exposing a shared environment role.

Where To Go Next

Place this identity flow inside the test automation CI/CD complete guide. Then strengthen the surrounding pipeline:

Next, convert the AWS setup commands into reviewed infrastructure as code. Add a separate role for each environment, exercise explicit denial tests, and connect CloudTrail role sessions to GitHub run IDs. Once identity and authorization are stable, replace the S3 fixture with your application's real test deployment and keep the same verification discipline.

Conclusion

This github actions oidc test environments tutorial replaces stored AWS credentials with a verifiable workload identity and short-lived role session. The secure path requires all of its parts: a GitHub OIDC provider, exact trust claims, least-privilege service permissions, a protected environment, a narrowly permissioned workflow job, and evidence that the expected identity performed the test.

Start with the disposable S3 smoke fixture. Prove success and denial paths, move the IAM resources into infrastructure as code, then adapt the deployment step to your test stack without broadening the role beyond what that stack actually needs.

Interview Questions and Answers

Explain the GitHub Actions to AWS OIDC authentication flow.

The job receives permission to request a GitHub OIDC token. GitHub signs a token containing claims such as issuer, audience, and subject. AWS validates those claims against the IAM provider and role trust policy, then STS returns temporary credentials governed by the role's permission policies.

What is the difference between an IAM trust policy and a permission policy?

A trust policy defines which principal may assume the role and under what conditions. A permission policy defines what the assumed role may do in AWS. A secure CI role needs both a narrowly matched workload identity and least-privilege service permissions.

Why should an OIDC trust policy check the audience claim?

The audience indicates which service the token was intended for. Checking `sts.amazonaws.com` helps ensure AWS STS accepts only a token minted for that exchange, alongside an exact subject restriction for the repository context.

How would you isolate test and production deployments with OIDC?

I would create separate GitHub Environments and separate AWS roles. Each trust policy would match only its environment subject, and each permission policy would reference only that environment's resources. Production would have stronger reviewers, branch restrictions, and monitoring.

Does id-token write give a workflow cloud access?

No. It only permits the job to request a GitHub OIDC token. The cloud provider still validates the token, and the IAM role trust policy decides whether assumption is allowed. Service actions are then limited by the role permission policy and other AWS controls.

How do you test an OIDC configuration safely?

Start with a disposable test resource and a minimal allowed action. Verify the assumed-role ARN and the service result, then run negative cases from a disallowed environment or branch. Store sanitized evidence, and never log the identity token or temporary credentials.

What risks remain after replacing access keys with OIDC?

A malicious change to a trusted workflow can use the role's permissions, and an overbroad trust or permission policy expands impact. Third-party action compromise, weak branch protection, unsafe events, untrusted runners, and leaked test data also remain relevant. OIDC reduces credential exposure but does not replace defense in depth.

Frequently Asked Questions

What is GitHub Actions OIDC?

GitHub Actions OIDC is a workload identity mechanism that lets a job request a signed token containing repository and job context. A cloud provider validates that token and can issue temporary credentials without a stored cloud access key.

Do I need to store AWS access keys when using GitHub OIDC?

No. The workflow requests a GitHub identity token and exchanges it for temporary AWS role credentials. Store the role ARN and region as variables, and remove obsolete long-lived AWS keys after verifying that no other workflow depends on them.

What permissions does a GitHub Actions OIDC workflow need?

The job needs `id-token: write` to request an OIDC token and usually `contents: read` to check out code. AWS permissions come from the assumed role, whose trust and service policies should be restricted independently.

How do I restrict an AWS OIDC role to a GitHub Environment?

Set the trust-policy subject to `repo:ORG/REPO:environment:ENVIRONMENT` and make the workflow job reference that environment. Also validate the audience as `sts.amazonaws.com` and configure GitHub environment branch and reviewer rules.

Why does AssumeRoleWithWebIdentity return access denied?

The token claims probably do not match the role trust policy, or the provider and role ARNs point to different configuration. Compare the audience, organization, repository, environment name, capitalization, and the job's `id-token` permission.

Can one GitHub OIDC role serve test and production?

It can technically trust multiple subjects, but separate roles are safer and easier to audit. Give test and production distinct trust policies, permissions, environment protection rules, and approval paths.

How long do GitHub OIDC AWS credentials last?

AWS issues a temporary role session whose duration is bounded by role and request settings. Treat the credentials as short-lived, avoid printing them, and keep the workflow timeout and role session duration no longer than the deployment requires.

Related Guides