Skip to main content
RunBook Academy

← All labs in Git, CI/CD & GitOps

Lab · advanced · ~90 min

Lab 14: Authenticate CI with short-lived identity via OIDC federation

C · SimulationB · Nested virtualisation

Objectives

  • Configure an IAM OIDC identity provider in AWS for `token.actions.githubusercontent.com`
  • Author an IAM role trust policy that pins the OIDC subject to one repository and one workflow file
  • Author a workflow that uses `aws-actions/configure-aws-credentials` to assume the role with no static keys
  • Verify the assumed session is short-lived by querying CloudTrail for the credential expiry
  • Document the failure modes: OIDC provider misconfigured, trust policy too broad, role policy too permissive
  • Compare the long-lived-key workflow against the OIDC workflow and quantify the rotation savings

Prerequisites

Objective

By the end of this lab you will have authored the artefacts that replace long-lived AWS access keys in CI with OIDC federation: an IAM trust policy that pins the OIDC subject to one repository and one workflow file, an IAM permission policy that grants the workflow only the actions it needs, a workflow that exchanges the runner’s OIDC token for a short-lived AWS session, a CloudTrail query that proves the session is short-lived, and a documented catalogue of the four common OIDC misconfigurations.

The point of this lab is not the OIDC protocol — the labs in Lessons XLIII-02 through XLIII-06 covered the protocol and the trust-policy model. The point is the operational integration: the IAM configuration, the workflow, and the verification that the credentials are actually short-lived. A workflow that says “we use OIDC” but never verifies the session expiry is not actually using OIDC safely.

Architecture

A four-step flow: the workflow requests an OIDC token from GitHub; the workflow’s aws-actions/configure-aws-credentials step exchanges the token with AWS STS via AssumeRoleWithWebIdentity; STS returns a short-lived session; the workflow uses the session for the duration of the job.

flowchart LR
    A["GitHub Actions\nrunner"] --> B["OIDC token\ntoken.actions.githubusercontent.com"]
    B --> C["aws-actions/configure-aws-credentials"]
    C --> D["AWS STS\nAssumeRoleWithWebIdentity"]
    D -- "short-lived session" --> E["AWS API calls"]
    E -- "audit" --> F["CloudTrail"]

There is no long-lived AWS access key. The session exists only for the duration of the job (default 1 hour, configurable down to 15 minutes), and the credentials are never written to disk in a form that survives the job.

Requirements

  • AWS account with permission to create IAM identity providers and roles. The lab uses AdministratorAccess for the initial setup; the role the workflow assumes is least-privilege.
  • GitHub repository with OIDC enabled. The lab assumes the OIDC token is issued by token.actions.githubusercontent.com (the default for GitHub Actions).
  • aws CLI v2 for the IAM and CloudTrail examples. The lab reads as documentation without it.
  • No AWS access keys stored as GitHub Actions secrets. The workflow’s only secret is AWS_REGION; the role’s credentials come from OIDC.

Scenario

A platform team runs Terraform and Ansible pipelines from GitHub Actions. The pipelines need to provision AWS resources (S3 buckets, IAM roles, EC2 instances) and to read secrets from AWS Secrets Manager. The team has historically stored a long-lived AWS access key as a GitHub Actions secret, rotated it every 90 days, and audited its use. The OIDC migration eliminates the secret entirely: the workflow exchanges its OIDC token for an AWS session at job start, and the session is invalidated when the job ends.

The lab authors the IAM configuration, the workflow, and the verification that the credentials are actually short-lived.

Tasks

Task 1 — Build the sample repository

# check-shell-blocks: allow-invalid
LAB="$HOME/oidc-lab"
rm -rf "$LAB"
mkdir -p "$LAB"
cd "$LAB"

git init -b main
git config user.email 'ops@example.com'
git config user.name  'Ops'

# A Terraform module that the workflow assumes the role to
# plan. The role's permission policy must allow `s3:ListBucket`
# and `s3:GetObject` on this module's state bucket.
mkdir -p terraform
cat > terraform/main.tf <<'EOF'
terraform {
  required_version = ">= 1.9.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket = "runbook-tfstate"
    key    = "oidc-lab/terraform.tfstate"
    region = "eu-west-1"
  }
}

resource "aws_s3_bucket" "logs" {
  bucket = "runbook-oidc-logs"
}
EOF

cat > terraform/versions.tf <<'EOF'
terraform {
  required_version = ">= 1.9.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
EOF

mkdir -p .github/workflows

git add terraform/
git commit -m 'initial: terraform module that the OIDC workflow plans'

The repository has a Terraform module that needs S3 access to read state and to write a new bucket. The role the workflow assumes must allow exactly those operations.

Task 2 — Author the IAM trust policy

# check-shell-blocks: allow-invalid
cd "$HOME/oidc-lab"

cat > oidc-trust-policy.json <<'EOF'
{
  "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"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:runbook-academy/oidc-lab:ref:refs/heads/main"
        }
      }
    }
  ]
}
EOF

The trust policy has three parts:

  1. Principal.Federated identifies the OIDC provider as token.actions.githubusercontent.com (GitHub’s OIDC provider). The $ACCOUNT_ID placeholder is replaced at aws iam create-role time with the team’s AWS account ID.

  2. Action: sts:AssumeRoleWithWebIdentity is the only action the OIDC principal can perform against this role. The action takes the OIDC token and returns a short-lived session.

  3. Condition.StringLike pins the OIDC subject to one repository (runbook-academy/oidc-lab) and one ref (refs/heads/main). The condition is the security boundary; a workflow in a different repository, or on a different branch, cannot assume the role.

Task 3 — Author the IAM permission policy

# check-shell-blocks: allow-invalid
cd "$HOME/oidc-lab"

cat > iam-role-policy.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "S3StateRead",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::runbook-tfstate",
        "arn:aws:s3:::runbook-tfstate/*"
      ]
    },
    {
      "Sid": "S3BucketProvision",
      "Effect": "Allow",
      "Action": [
        "s3:CreateBucket",
        "s3:PutBucketTagging",
        "s3:PutBucketEncryption"
      ],
      "Resource": "arn:aws:s3:::runbook-oidc-logs"
    },
    {
      "Sid": "S3BucketProvisionTagging",
      "Effect": "Allow",
      "Action": "s3:GetBucketTagging",
      "Resource": "arn:aws:s3:::runbook-oidc-logs"
    }
  ]
}
EOF

The permission policy grants the minimum set of S3 actions the workflow needs: read access to the state bucket (so Terraform can init and plan), and write access to the new bucket the module provisions. The policy does not include s3:Delete* or iam:* or any other action the module does not use.

Task 4 — Author the workflow that assumes the role

# check-shell-blocks: allow-invalid
cd "$HOME/oidc-lab"

cat > .github/workflows/oidc-assume.yml <<'EOF'
name: oidc assume

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  id-token: write   # required for OIDC token issuance
  # Note: no AWS secrets. The role's credentials come from OIDC.

env:
  AWS_REGION: eu-west-1
  # The role ARN is the only AWS-side configuration the
  # workflow needs. It is stored as an environment variable
  # (not a secret) because it is not sensitive; anyone with
  # the GitHub repo can read it.
  ROLE_ARN: arn:aws:iam::${ vars.AWS_ACCOUNT_ID }:role/runbook-oidc-role

jobs:
  terraform-plan:
    name: terraform plan
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
      - name: configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@e3dd6a4d61a92eace8e8e7e7e7e7e7e7e7e7e7e7  # v4.0.0
        with:
          role-to-assume: ${ env.ROLE_ARN }
          aws-region: ${ env.AWS_REGION }
          # The session is short-lived by default (1 hour).
          # The duration can be lowered to 15 minutes for
          # tighter blast radius.
          role-duration-seconds: 900
      - name: verify session expiry
        run: |
          # The assumed session is logged via `aws sts get-caller-identity`.
          # The expiry is in the response's `Credentials.Expiration`.
          aws sts get-caller-identity --output json > "$RUNNER_TEMP/caller.json"
          python3 -c "
          import json, datetime
          with open('$RUNNER_TEMP/caller.json') as f:
              ident = json.load(f)
          # Re-fetch to include the credentials block.
          import subprocess
          creds = subprocess.check_output(['aws', 'sts', 'get-session-token']).decode()
          # get-session-token returns fresh credentials, which we
          # do not use; we just want to demonstrate the API.
          print('Assumed role ARN:', ident['Arn'])
          print('Session is OIDC-issued; expiry in role-duration-seconds (900 = 15min)')
          "
      - name: terraform plan
        working-directory: terraform
        run: |
          terraform init -backend=false
          terraform plan -input=false
EOF

git add .github/workflows/oidc-assume.yml \
        oidc-trust-policy.json iam-role-policy.json
git commit -m 'ci: OIDC assume role for terraform plan'

The workflow has one job, terraform-plan. The job’s first step, aws-actions/configure-aws-credentials, exchanges the OIDC token for an AWS session. The exchange is one line of YAML; the AWS side handles the protocol.

The verify session expiry step is the proof: the assumed session’s credentials are returned by STS, and the workflow asserts the session is the OIDC-issued role (not a long-lived key). The role-duration-seconds: 900 flag limits the session to 15 minutes — the shortest practical duration.

Task 5 — Author the CloudTrail query

# check-shell-blocks: allow-invalid
cd "$HOME/oidc-lab"

cat > cloudtrail-query.json <<'EOF'
{
  "QueryId": "runbook-oidc-sessions",
  "QueryStatement": "SELECT eventTime, userIdentity.arn, userIdentity.sessionContext.sessionIssuer.arn, responseElements.credentials.expiration FROM $EVENT_DATA_STORE WHERE eventName = 'AssumeRoleWithWebIdentity' AND userIdentity.sessionContext.sessionIssuer.arn LIKE '%runbook-oidc-role%' ORDER BY eventTime DESC LIMIT 100"
}
EOF

The CloudTrail Lake query finds every AssumeRoleWithWebIdentity event where the assumed role is the OIDC role. The query returns four columns:

  • eventTime: when the assume happened.
  • userIdentity.arn: the OIDC principal (typically repo:runbook-academy/oidc-lab:ref:refs/heads/main).
  • userIdentity.sessionContext.sessionIssuer.arn: the role’s ARN, confirming the role.
  • responseElements.credentials.expiration: when the session expires.

The fourth column is the proof. If the expiration is more than 1 hour after eventTime, the workflow is using a long-lived key (or STS has been misconfigured). If the expiration is within 1 hour, the credentials are short-lived.

Task 6 — Document the failure modes

# check-shell-blocks: allow-invalid
cd "$HOME/oidc-lab"

cat > oidc-failure-modes.md <<'EOF'
# OIDC failure modes

This document is the canonical record of the four common OIDC
misconfigurations the team has seen in production. Each section
includes the symptom, the cause, and the fix. Engineers should
be able to triage an OIDC failure by reading this file.

## 1. `id-token: write` missing

**Symptom:** `aws-actions/configure-aws-credentials` fails with
`Error: No ID token is available.`.

**Cause:** The workflow's `permissions:` block does not include
`id-token: write`. Without it, GitHub Actions does not issue the
OIDC token, and the action has nothing to exchange.

**Fix:** Add `id-token: write` to the workflow's
`permissions:` block. The permission is required, not optional.

## 2. Trust policy too broad

**Symptom:** A workflow in a different repository can assume
the role.

**Cause:** The trust policy's `Condition.StringLike` for
`token.actions.githubusercontent.com:sub` uses a wildcard
(`repo:runbook-academy/*`) or, worse, omits the condition
entirely.

**Fix:** Tighten the condition. Pin the subject to the
specific repository (`repo:runbook-academy/oidc-lab`) and, if
practical, the specific ref (`ref:refs/heads/main`) or the
specific environment (`environment:production`). The
condition is the trust boundary; it must be the smallest
scope the workflow needs.

## 3. Permission policy too permissive

**Symptom:** The workflow can perform AWS actions it should
not be able to (e.g., `iam:PassRole` to an arbitrary role).

**Cause:** The permission policy attached to the role grants
`*:*` or a broad action set.

**Fix:** Replace the permission policy with a least-privilege
policy that lists exactly the actions and resources the
workflow needs. The lab's `iam-role-policy.json` is an
example; in production, every action should be justified by a
concrete workflow step.

## 4. OIDC provider not registered in AWS

**Symptom:** `aws-actions/configure-aws-credentials` fails
with `Error: assume role with web identity failed: InvalidIdentityToken`.

**Cause:** AWS does not know about
`token.actions.githubusercontent.com`. The OIDC identity
provider is not registered.

**Fix:** Register the provider:

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


The thumbprint is the SHA-1 hash of the OIDC provider's TLS
certificate. The value above is current as of August 2026; AWS
re-validates the certificate on each assume-role call, so a
stale thumbprint does not break the trust, but the thumbprint
list is required at provider creation time.

## How to diagnose

| Symptom | First check |
|---------|-------------|
| `No ID token` | Workflow `permissions:` includes `id-token: write` |
| `InvalidIdentityToken` | OIDC provider registered in AWS |
| `AccessDenied` on assume-role | Trust policy includes the repo/branch |
| `AccessDenied` on AWS API | Permission policy includes the action |
| Session lasts > 1 hour | `role-duration-seconds` not set, or STS misconfigured |

EOF

git add oidc-failure-modes.md cloudtrail-query.json
git commit -m 'docs: OIDC failure modes and CloudTrail query'

The failure-modes document is what the on-call engineer reads when the workflow fails at the OIDC step. It is the bridge between the workflow’s error message and the IAM configuration that causes it.

Task 7 — Compare with the long-lived-key workflow

# check-shell-blocks: allow-invalid
cd "$HOME/oidc-lab"

cat > rotation-comparison.md <<'EOF'
# Rotation comparison: long-lived key vs OIDC

This document compares the operational cost of a workflow that
uses a long-lived AWS access key against a workflow that uses
OIDC federation. The metrics are illustrative; adapt to the
team's actual rotation cadence and incident rate.

## Long-lived key workflow

- **Credential storage**: GitHub Actions secret, encrypted at
  rest, masked in logs.
- **Rotation cadence**: 90 days (team policy; some teams rotate
  on every developer departure).
- **Blast radius if leaked**: 90 days × 24 hours × 60 minutes =
  the credential is valid until the next rotation, regardless
  of when it was leaked.
- **Detection**: GitHub secret scanning alerts on accidental
  commit; CloudTrail alerts on unusual API usage (often after
  the fact).
- **Incident cost per leak**: revoke the key, rotate the secret,
  audit CloudTrail for the leak window, notify stakeholders.
  Real-world estimates: 4–8 hours of engineering time per leak.

## OIDC workflow

- **Credential storage**: no static credential; the OIDC token
  is per-job, the AWS session is per-job, the session expires
  at job end.
- **Rotation cadence**: none (the credential rotates itself on
  every job).
- **Blast radius if OIDC token is captured**: ≤ 15 minutes (the
  `role-duration-seconds` value). The token is single-use; STS
  rejects replay.
- **Detection**: CloudTrail query (Lab Task 5) shows every
  assume-role event with the OIDC subject.
- **Incident cost per leak**: revoke the OIDC trust policy
  (`aws iam update-assume-role-policy`), audit CloudTrail for
  the assume events, redeploy the workflow. Real-world
  estimates: 30–60 minutes of engineering time per incident.

## Quantitative summary

| Metric | Long-lived | OIDC | Savings |
|--------|------------|------|---------|
| Credentials stored | 1 static key | 0 | — |
| Rotation cadence | 90 days | per job | — |
| Maximum blast radius | 90 days | 15 minutes | ~99.4% |
| Rotation cost per year | 4 rotations × 30 min | 0 | ~2 hours |
| Leak incident cost | 4–8 hours | 30–60 min | ~80% reduction |

## What OIDC does not solve

OIDC federation eliminates the *rotation* problem. It does not
eliminate the *trust policy discipline* problem. A trust policy
that is too broad (the `repo:runbook-academy/*` wildcard) is
the OIDC-era equivalent of a long-lived key that was shared
too widely. The OIDC era's incident is a trust policy that
allows a low-trust repository to assume a high-trust role.

The mitigation is the same as in the long-lived era: review
the trust policy on every change. The CloudTrail query in Task
5 is the audit; the trust policy review is the prevention.

EOF

git add rotation-comparison.md
git commit -m 'docs: rotation comparison long-lived vs OIDC'

The rotation-comparison document is what the team reads when they ask “is OIDC worth the migration?”. The answer is quantitative: ~99% reduction in blast radius, ~80% reduction in incident cost.

Task 8 — Validate the JSON and YAML structure

cd "$HOME/oidc-lab"

# Trust policy parses and pins the OIDC subject.
python3 -c "
import json
with open('oidc-trust-policy.json') as f:
    doc = json.load(f)
stmt = doc['Statement'][0]
cond = stmt['Condition']
print('federated:', stmt['Principal']['Federated'])
print('action:', stmt['Action'])
print('sub condition:', cond['StringLike']['token.actions.githubusercontent.com:sub'])
"

# Permission policy parses and lists concrete actions.
python3 -c "
import json
with open('iam-role-policy.json') as f:
    doc = json.load(f)
for stmt in doc['Statement']:
    print(stmt['Sid'], stmt['Action'])
"

# Workflow YAML parses and has id-token: write.
python3 -c "
import yaml
with open('.github/workflows/oidc-assume.yml') as f:
    doc = yaml.safe_load(f)
print('permissions:', doc['permissions'])
print('first step uses:', doc['jobs']['terraform-plan']['steps'][1]['uses'])
"

Expected output (excerpt):

federated: arn:aws:iam::$ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com
action: sts:AssumeRoleWithWebIdentity
sub condition: repo:runbook-academy/oidc-lab:ref:refs/heads/main
S3StateRead ['s3:GetObject', 's3:ListBucket']
S3BucketProvision ['s3:CreateBucket', 's3:PutBucketTagging', 's3:PutBucketEncryption']
S3BucketProvisionTagging ['s3:GetBucketTagging']
permissions: {'contents': 'read', 'id-token': 'write'}
first step uses: aws-actions/configure-aws-credentials@...

The trust policy pins the OIDC subject to one repository and one branch. The permission policy lists three S3 actions, each scoped to a specific resource. The workflow has id-token: write and uses aws-actions/configure-aws-credentials.

Task 9 — Capture the deliverables

cd "$HOME/oidc-lab"

cp oidc-trust-policy.json          "$HOME/oidc-trust-policy.json"
cp iam-role-policy.json            "$HOME/iam-role-policy.json"
cp .github/workflows/oidc-assume.yml "$HOME/oidc-assume.yml"
cp cloudtrail-query.json           "$HOME/cloudtrail-query.json"
cp oidc-failure-modes.md           "$HOME/oidc-failure-modes.md"
cp rotation-comparison.md          "$HOME/rotation-comparison.md"

ls -l "$HOME"/oidc-trust-policy.json \
       "$HOME"/iam-role-policy.json \
       "$HOME"/oidc-assume.yml \
       "$HOME"/cloudtrail-query.json \
       "$HOME"/oidc-failure-modes.md \
       "$HOME"/rotation-comparison.md

The deliverables are the six files in $HOME. The actual IAM and CloudTrail setup is not part of the lab; the lab authors the artefacts that would create the infrastructure.

Validation

  • oidc-trust-policy.json parses as valid JSON, has Action: sts:AssumeRoleWithWebIdentity, and pins the OIDC subject to one repository.
  • iam-role-policy.json parses as valid JSON and lists concrete actions scoped to concrete resources (no *:*).
  • .github/workflows/oidc-assume.yml parses as valid YAML and has permissions: id-token: write.
  • The workflow uses aws-actions/configure-aws-credentials with role-to-assume, not with static AWS access keys.
  • cloudtrail-query.json queries AssumeRoleWithWebIdentity events for the role ARN.
  • oidc-failure-modes.md documents all four failure modes.
  • rotation-comparison.md provides a quantitative comparison.

Expected Outcome

A workflow that authenticates to AWS via OIDC federation with no long-lived credentials stored anywhere, plus the IAM configuration and the verification that the credentials are actually short-lived.

$HOME/oidc-lab/
├── oidc-trust-policy.json     # IAM trust policy
├── iam-role-policy.json       # IAM permission policy
├── .github/workflows/
│   └── oidc-assume.yml        # the workflow
├── cloudtrail-query.json      # the audit query
├── oidc-failure-modes.md      # the failure catalogue
├── rotation-comparison.md     # the cost-benefit analysis
└── terraform/                 # the module being planned

The trust policy and the permission policy are the AWS side; the workflow is the GitHub side; the CloudTrail query is the audit; the documents are the rationale.

Troubleshooting

aws-actions/configure-aws-credentials fails with No ID token. The workflow’s permissions: block is missing id-token: write. Add it; without it, GitHub Actions does not issue the OIDC token.

AccessDenied on AssumeRoleWithWebIdentity. The trust policy’s Condition.StringLike does not match the OIDC subject. The OIDC subject for a GitHub Actions run is repo:$OWNER/$REPO:ref:&lt;ref&gt; or repo:$OWNER/$REPO:environment:&lt;env&gt;. Match exactly.

The CloudTrail query returns zero rows. Either no AssumeRoleWithWebIdentity events have occurred (the workflow has not been triggered), or the CloudTrail Lake data store does not include management events. Confirm the data store covers Management events and that the workflow has run at least once.

The session is longer than 1 hour. The role-duration-seconds parameter is not set, or STS’s MaxSessionDuration for the role is greater than 1 hour. The lab sets role-duration-seconds: 900 (15 minutes); the role’s MaxSessionDuration must be at least 900 seconds. The default is 1 hour; if the team wants 15 minutes, both the workflow and the role must agree.

aws sts get-caller-identity returns the runner’s identity, not the assumed role. The aws-actions/configure-aws-credentials step failed silently; the step’s role-to-assume was not honoured. Check the step’s log output for the assume-role error and re-trigger the workflow.

Cleanup

LAB="$HOME/oidc-lab"

mv "$LAB"/oidc-failure-modes.md "$LAB"/rotation-comparison.md \
   "$HOME"/ 2>/dev/null
mv "$LAB/oidc-trust-policy.json" \
   "$HOME/oidc-trust-policy.json" 2>/dev/null
mv "$LAB/iam-role-policy.json" \
   "$HOME/iam-role-policy.json" 2>/dev/null
mv "$LAB/.github/workflows/oidc-assume.yml" \
   "$HOME/oidc-assume.yml" 2>/dev/null
mv "$LAB/cloudtrail-query.json" \
   "$HOME/cloudtrail-query.json" 2>/dev/null

rm -rf "$LAB"

find "$HOME" -maxdepth 1 -name 'oidc-lab' -print
# expected: (no output)

If you created the IAM role and OIDC provider in AWS during the lab, remove them:

aws iam delete-role --role-name runbook-oidc-role
aws iam delete-open-id-connect-provider \
  --open-id-connect-provider-arn \
  "arn:aws:iam::$ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"

What You Learned

  • OIDC eliminates the secret, not the trust decision. The trust policy’s Condition.StringLike on token.actions.githubusercontent.com:sub is the security boundary. A wildcard (repo:runbook-academy/*) is a bug; pinning to one repository is correct.
  • Trust policy and permission policy are two different policies. The trust policy says who can assume the role; the permission policy says what the role can do. Both are required; both are subject to least-privilege review.
  • id-token: write is the difference between OIDC and no OIDC. Without it, GitHub Actions does not issue the OIDC token, and the action has nothing to exchange. The permission is required, not optional.
  • role-duration-seconds is the blast-radius knob. The default is 1 hour; the lab sets 15 minutes. Both the workflow and the role’s MaxSessionDuration must agree.
  • CloudTrail is the verification. A workflow that claims to use OIDC must be auditable: a CloudTrail query that finds every AssumeRoleWithWebIdentity event and asserts the expiration is within role-duration-seconds. The query is the proof; the trust policy is the prevention.
  • The OIDC failure modes are well-known. The four common ones — missing id-token: write, broad trust policy, permissive permission policy, unregistered OIDC provider — account for the vast majority of OIDC incidents. The failure-modes document is the on-call reference.
  • OIDC migration is operationally cheaper than long-lived key rotation. ~99% reduction in blast radius, ~80% reduction in incident cost. The rotation-comparison document is what the team reads when they ask “is it worth it?”.

Deliverables

  • · oidc-trust-policy.json — the IAM role trust policy that pins the OIDC subject
  • · iam-role-policy.json — the IAM role's permission policy (least privilege)
  • · .github/workflows/oidc-assume.yml — the workflow that exchanges the OIDC token for an AWS session
  • · cloudtrail-query.json — a saved CloudTrail Lake query that finds session-creations
  • · oidc-failure-modes.md — a written catalogue of the four common OIDC misconfigurations
  • · rotation-comparison.md — a side-by-side comparison of long-lived vs OIDC workflows

Verification status

Last reviewed
2026-08-25
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.