Skip to content
← All labs
DevOps / Hands-on lab

Deploy to AWS from GitHub without access keys

Restrict GitHub OIDC trust to your repository's main branch and upload a verified artifact into one private S3 prefix.

Intermediate75 minutesUpdated 2026-09-23Use your own AWS learning account

Overview

A student team needs its automation to upload a build artifact to AWS. Configure GitHub Actions to obtain temporary AWS credentials using OpenID Connect, then prove that the permitted branch succeeds and another branch cannot assume the role.

The result is a private artifact in S3, not a public website. You will create no IAM user or long-lived access key. Authentication answers which workflow is running; the role's permissions separately restrict what that workflow can do.

Architecture

A workflow in the learner's repository main branch obtains a GitHub OIDC token, exchanges it through AWS STS for a restricted role session, and uploads into a private S3 site prefix.

Open the full-size architecture diagram

GitHub issues a signed identity token. AWS STS checks the IAM trust policy and returns short-lived role credentials. The workflow uses those credentials to list, read, and write only the dedicated bucket's site/ prefix. S3 stores the artifact independently of the runner.

OIDC avoids distributing static AWS keys to a CI system. Repository review and branch protection still matter: code that reaches the trusted branch can exercise its deployment permissions. GitHub OIDC guidance

Prerequisites

  • Your own disposable GitHub repository with a main branch, Actions enabled, and permission to edit workflow files and repository variables. Use GitHub-hosted Ubuntu runners.
  • An AWS learning account and a setup role allowed to create/delete a dedicated S3 bucket, manage its objects, create an IAM OIDC provider if necessary, and create/manage/delete the lab role and its inline policy.
  • Use us-east-1, the standard AWS partition, and a private bucket containing only this lab's data.
  • Use GitHub's default OIDC subject format. This workflow has no GitHub environment; environment-based subjects require a different trust condition. Organization Actions restrictions may require approval of the pinned actions.

Steps

1. Prepare the private artifact store

In S3, create a globally unique general-purpose bucket such as cloudadhar-oidc-YOUR-SUFFIX in us-east-1. Keep all Block Public Access settings enabled, ACLs disabled, and SSE-S3 encryption. Leave versioning disabled for this disposable exercise. Do not enable website hosting.

Record the bucket name, AWS account ID, and exact GitHub OWNER/REPO spelling. In your repository, create site/index.html:

html
<!doctype html>
<html lang="en"><meta charset="utf-8"><title>OIDC lab</title>
<body><h1>CloudAdhar deployment proof</h1><p>Build 1</p></body></html>

Commit it to main. The file contains no secrets or personal information.

2. Register GitHub's identity provider

In IAM, open Identity providers. If your account already has an approved provider for token.actions.githubusercontent.com, reuse it after verifying that its audience includes sts.amazonaws.com.

Otherwise choose Add provider, OpenID Connect, provider URL https://token.actions.githubusercontent.com, and audience sts.amazonaws.com. Record whether you created or reused it. IAM can retrieve the certificate thumbprint; do not paste a historical thumbprint from an old tutorial. AWS OIDC provider verification

3. Create an exact trust boundary

In IAM Roles, choose Create role and Custom trust policy. Replace ACCOUNT_ID and OWNER/REPO below, then name the role cloudadhar-github-deploy. Do not attach broad managed permissions.

json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::ACCOUNT_ID: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:OWNER/REPO:ref:refs/heads/main"
      }
    }
  }]
}

The exact subject excludes other repositories, pull-request subjects, and other branches. The audience limits the intended token recipient. Neither condition should use *. AWS GitHub trust configuration

Add this inline permissions policy, named LabSitePrefix, replacing YOUR_BUCKET:

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::YOUR_BUCKET",
      "Condition": {"StringLike": {"s3:prefix": ["site/", "site/*"]}}
    },
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject"],
      "Resource": "arn:aws:s3:::YOUR_BUCKET/site/*"
    }
  ]
}

Listing uses the bucket ARN; object access uses the prefix ARN. This role cannot delete objects, change bucket access, or deploy outside site/. The setup role, not this deployment role, performs cleanup. AWS S3 policy examples

4. Add variables and the workflow

In repository Settings, Secrets and variables, Actions, Variables, create AWS_ROLE_ARN, AWS_ACCOUNT_ID, and DEPLOY_BUCKET using your recorded values. These identifiers are configuration, not AWS credentials. Do not create access-key secrets.

Create .github/workflows/deploy.yml on main:

yaml
name: Private S3 artifact
on:
  push:
    branches: [main]
  workflow_dispatch:
permissions:
  contents: read
  id-token: write
jobs:
  deploy:
    runs-on: ubuntu-24.04
    timeout-minutes: 10
    env:
      AWS_REGION: us-east-1
      DEPLOY_BUCKET: ${{ vars.DEPLOY_BUCKET }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
        with:
          role-to-assume: ${{ vars.AWS_ROLE_ARN }}
          allowed-account-ids: ${{ vars.AWS_ACCOUNT_ID }}
          aws-region: us-east-1
          audience: sts.amazonaws.com
          role-duration-seconds: 900
          role-session-name: GitHub-${{ github.run_id }}
          retry-max-attempts: 3
      - name: Verify identity and upload
        shell: bash
        run: |
          set -euo pipefail
          aws sts get-caller-identity
          aws s3 sync site/ "s3://${DEPLOY_BUCKET}/site/" --no-follow-symlinks --only-show-errors
          aws s3 cp "s3://${DEPLOY_BUCKET}/site/index.html" deployed-index.html --only-show-errors
          cmp site/index.html deployed-index.html
          echo "Verified identical source and S3 artifact"

The action commits above were checked against their official release tags on 2026-09-23. Review updates before changing pins. id-token: write permits token issuance, not arbitrary AWS access. The sync command deliberately omits --delete. GitHub action security and AWS sync behavior

5. Verify success and rejection

Open Actions and inspect the run. Expect an STS ARN containing assumed-role/cloudadhar-github-deploy/GitHub-, followed by Verified identical source and S3 artifact. In S3, download site/index.html through your authenticated console session and inspect it.

Change Build 1 to Build 2 on main; the next run should replace the same object. Then create a temporary branch named oidc-denied containing the workflow. Use Run workflow and select that branch. Expect role assumption to fail before upload because its subject is not the trusted main-branch subject. Do not loosen the trust policy to make this negative test pass.

If the main run fails, compare owner/repository case, branch, audience, role ARN, and variables. For S3 AccessDenied, inspect bucket/prefix spelling and account restrictions. IAM changes may require brief propagation time.

Verification

  • Main obtains the intended temporary role session and verifies matching file contents.
  • A second main run updates the existing site/index.html artifact.
  • The other branch is denied at role assumption.
  • The bucket remains private; no AWS access-key secrets exist.

Save redacted run IDs, branch names, expected outcomes, and an explanation of trust versus permissions. Never print or archive the OIDC token or temporary credentials.

Cost and cleanup

GitHub Actions minutes and S3 storage/requests may incur charges. Use tiny files and a few runs; check your account allowances before running. OIDC does not remove the cost of the resources used by a workflow.

Disable or remove the lab workflow first. With your setup role, empty only the dedicated bucket and delete it; remove all versions if you enabled versioning. Delete LabSitePrefix and the dedicated IAM role. Delete the OIDC provider only if this lab created it and no other role uses it. Remove the three repository variables and temporary test branch. Confirm the bucket and role are absent, retaining your evidence locally.

References