Based on the DOP-C02 exam guideReport an error
For the past decade, securely handling AWS credentials in CI/CD systems has been a chronic headache for nearly every DevOps team. Embedding IAM User access keys in GitHub Secrets is familiar, but that familiarity is itself the source of incidents. According to a 2022 Snyk report, AWS access keys discovered in public GitHub repositories exceeded 10,000 per year. Capital One's 2019 incident, Twitter's 2023 internal key leak—the root cause is the same: "static credentials issued once, copied to unknown locations, lost to audit trails".
OIDC federation solves this problem from a fundamentally different direction. It doesn't issue credentials. Instead, GitHub creates a short-lived JWT proving its identity every workflow run, and AWS STS validates it to issue a 1-hour temporary credential. Since static keys don't exist, there's nothing to leak.
Today covers: ① the exact mechanism of how this works and its six-step flow ② which conditions in the IAM Trust Policy determine security ③ how it combines with GitHub Environments to create dual defense ④ what trade-offs exist with Self-hosted Runners and CodeBuild.
Let's view one complete flow of GitHub Actions calling AWS APIs in slow motion.
1. GitHub Actions runner starts workflow
permissions: id-token: write must be present
|
v
2. runner requests token from GitHub OIDC provider
POST https://token.actions.githubusercontent.com/...
Body: { audience: "sts.amazonaws.com" }
|
v
3. GitHub issues JWT
header: { alg: "RS256", kid: "..." }
payload: {
iss: "https://token.actions.githubusercontent.com",
aud: "sts.amazonaws.com",
sub: "repo:my-org/my-app:environment:production",
ref: "refs/heads/main",
repository: "my-org/my-app",
workflow: "Deploy",
...
}
signature: RSA signature (verifiable via JWKS endpoint)
|
v
4. aws-actions/configure-aws-credentials@v4 calls STS
AssumeRoleWithWebIdentity {
RoleArn: "arn:aws:iam::ACCOUNT:role/GHActionsProdRole",
WebIdentityToken: <above JWT>,
RoleSessionName: "github-actions-<run-id>"
}
|
v
5. STS validates
- JWT signature verification (GitHub JWKS endpoint)
- iss == OIDC Provider URL
- aud matches trust policy condition
- sub matches trust policy condition (StringEquals/StringLike)
- Role's trust policy is "Allow"
|
v
6. Returns temporary credentials (default 1h, max 12h)
AccessKeyId: "ASIA..."
SecretAccessKey: "..."
SessionToken: "..."
Expiration: "..."
Let's examine each stage's details.
🔍 Deep Dive: The JWT issued in step 2 is signed with RS256 (RSA signature), and AWS STS validates it by retrieving the public key from GitHub's JWKS endpoint (
https://token.actions.githubusercontent.com/.well-known/jwks). When registering an IAM Identity Provider, you were asked for a thumbprint—this is that step: pre-trusting GitHub's identity via TLS certificate thumbprint. Since July 2023, AWS began auto-validating thumbprints for well-known IdPs (GitHub, GitLab, etc.), reducing the need for manual entry. This change prevents incidents during thumbprint rotation.
💡 Related Theory: OIDC (OpenID Foundation 2014) is a standard that adds an authentication layer on top of OAuth 2.0 (RFC 6749, 2012). While OAuth 2.0 handles "authorization delegation", OIDC handles "proving user identity". The key is an ID Token, a JWT (RFC 7519), containing standard claims (iss, sub, aud, exp, iat, nbf) that the IdP issued it, enabling self-verification. SAML 2.0 (OASIS 2005) serves the same role but is XML-based and heavy for mobile/API environments. AWS supports both, but GitHub Actions integration uses OIDC only.
📚 Case Study: In 2022, a fintech company discontinued IAM User access key policy and migrated to GitHub Actions OIDC. Immediately after, a workflow stopped working. Root cause: they forgot the
permissions:block in the workflow YAML, and GitHub defaults toid-token: none, so it doesn't issue OIDC tokens. The error message was cryptic (Could not assume role). After 30 minutes of debugging, they found it. Afterward, the team madepermissions: id-token: write, contents: readstandard in all workflow templates. In exams, when "OIDC isn't working → what to check first?", the answer is almost always this one line.
sub Condition in Trust Policy — Security Hinges HereThe security of OIDC integration depends almost entirely on how precise the sub condition in Trust Policy is. If sub is a wildcard, the blast radius of a breach explodes.
The sub value GitHub issues in JWT varies by context:
| Workflow Context | sub Value |
|---|---|
push to main | repo:my-org/my-repo:ref:refs/heads/main |
push to feature/x | repo:my-org/my-repo:ref:refs/heads/feature/x |
pull_request | repo:my-org/my-repo:pull_request |
| Tag push | repo:my-org/my-repo:ref:refs/tags/v1.0 |
| Environment used | repo:my-org/my-repo:environment:production |
| Reusable workflow | repo:my-org/my-repo:job_workflow_ref:org/shared/.github/workflows/deploy.yml@refs/heads/main |
How you write the trust policy condition determines security level:
// 1) Too loose — any repo in org with main branch can assume Role
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-org/*:ref:refs/heads/main"
}
// 2) Moderate — only specific repo's main branch
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-app:ref:refs/heads/main"
}
// 3) Safest — specific repo + specific environment + workflow passing Required Reviewers only
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:my-org/my-app:environment:production"
}⚠️ Trap: The danger of pattern 1 is a frequent exam and real-world topic. With 100 repos in the org, if even one is compromised, that repo's main branch can assume prod Role. In exams, when asked "what's the risk of wildcard
repo:org/*sub condition?", the answer is always "if any other repo is compromised, prod credentials can be stolen."
🔍 Deep Dive: Pattern 3's
environment:productionis safest because GitHub Environment can have Required Reviewers or wait timer. To useenvironment: production, the workflow must first pass GitHub's gate; only then does GitHub issue a JWT withsub=environment:production. This means AWS Role assumption doesn't happen without human approval. AWS IAM restriction + GitHub Environment gate completes the dual defense.
💡 Related Theory: This pattern is a textbook application of defense in depth (NIST SP 800-160). Even if one layer (GitHub Environment) breaches, the next (IAM trust policy sub condition) blocks. It's also an implementation of principle of least privilege (Saltzer & Schroeder 1975) in OIDC federation—the Role's Permission Policy only touches prod resources, and the contexts that can assume that Role are narrowed.
📚 Case Study: In 2023, a SaaS company almost suffered an incident where fork PRs triggered workflows accessing secrets. GitHub's default is that secrets aren't exposed in fork PRs, but a developer misused
pull_request_targetevent, making fork code access base repo secrets. Fortunately, the OIDC token'ssubwaspull_request, and prod IAM Role's trust policy only allowedsub: ref:refs/heads/main, so the fork PR couldn't assume the Role. Critical blocking point. After this, the company org-wide bannedpull_request_targetusage.
GitHub Environments aren't just labels; they're protection layers that intervene right before workflow execution. Available gates:
| Gate | Behavior |
|---|---|
| Required Reviewers | Specified person or team must approve before deploy job starts (max 6, some required) |
| Wait Timer | Wait N minutes (max 30 days) even after approval. Creates emergency rollback window |
| Deployment Branches | Restrict which branches/tags can deploy to this environment |
| Environment Secrets | Separate secrets storage per environment |
| Custom Protection Rules | (GitHub Apps) external tool evaluates gate |
jobs:
deploy-prod:
runs-on: ubuntu-latest
environment:
name: production
url: https://app.example.com # Link for post-deploy verification
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::222:role/GHA-Production
aws-region: ap-northeast-2
- run: ./deploy.sh production🔍 Deep Dive: Wait Timer is often overlooked but highly valuable in practice. "5-minute wait after approval" gives someone 5 minutes to shout "stop". Combined with CodeDeploy's
BlockTrafficauto hook, you get 4-stage protection: human approval → wait → auto-validation → traffic shift.
💡 Related Theory: GitHub Environments + Required Reviewers is fundamentally the manual gating pattern (Google SRE Book, Chapter 16). Full automation is fast but has wide blast radius; full manual is safe but slow. Gating is compromise—"ask humans only where human judgment has value". AWS CodePipeline's Manual Approval Action embodies the same philosophy.
Pro exams frequently ask the selection criteria for both tools. The key is "AWS-native depth vs multi-cloud + developer experience".
| Dimension | GitHub Actions | AWS CodePipeline/CodeBuild |
|---|---|---|
| AWS Native Integration | OIDC + AWS Action, general depth | Native, all steps in one console |
| Multi-Cloud | Excellent, GCP/Azure identical | AWS-only |
| Build Environment | GH-hosted runner (per-minute billing) + Self-hosted | CodeBuild (per-minute billing, VPC-native) |
| PR Integration | Native PR comments, status checks | CodeStar Connections + Notifications |
| Approval Gate | Environments + Required Reviewers | Manual Approval Action |
| Secrets | Repo/Org/Environment Secrets | Secrets Manager / SSM Parameter Store |
| Multi-Account | OIDC + per-environment IAM Role | Cross-account Role + Pipeline roleArn |
| Free Tier | Certain free allocation | 100 minutes/month free (CodeBuild) |
| Strengths | Developer UX, rich marketplace actions | AWS fine-grained control, IAM/KMS consistency, VPC-native |
🎯 Scenario: A company runs "main builds in GitHub Actions, multi-account prod deployment in AWS CodePipeline". Why? GitHub Actions excels at build/test developer experience; CodePipeline's Cross-account Role + Manual Approval + KMS integration suits prod deployment precision. In exams when "which of these?" choices appear, look at the scenario's "what's most important?"—auto build/test PR = GH Actions, multi-account precision deploy = CodePipeline.
GitHub-hosted runners run on GitHub's public infrastructure, so they can't access VPC-internal resources (private RDS, internal ELB, EFS). Two solutions exist:
[Option A: Self-hosted Runner on AWS]
ASG of EC2 (or ECS Fargate)
|
| GitHub Actions Runner Controller (ARC) on EKS
| or actions-runner installation script
v
VPC private subnet
|
v
Private RDS / internal ALB / EFS access
[Option B: CodeBuild VPC mode]
GitHub webhook → CodeBuild project
|
| vpcConfig: { vpcId, subnets, securityGroupIds }
v
Build runs in VPC private subnet
| Dimension | Self-hosted Runner | CodeBuild VPC Mode |
|---|---|---|
| Maintenance | You patch runner, OS updates | AWS fully managed |
| Scaling | ASG or ARC scales with GitHub queue | CodeBuild auto-scales |
| Cost | EC2/Fargate + operational time | Per-build-minute |
| VPC Integration | Instance lives inside VPC | ENI attachment |
| GitHub Integration Depth | 100% (native GH Actions) | GH webhook + status API |
| Startup Latency | Instant if runner always running | Cold start tens of seconds |
⚠️ Trap: Connecting Self-hosted Runner to a public repo risks external PRs executing arbitrary code on the runner. GitHub explicitly warns "use self-hosted runners for private repos only". In exams, when asked about "public repo + self-hosted runner" danger, it's almost always the right answer.
🔍 Deep Dive: GitHub Actions Runner Controller (ARC) is a Kubernetes operator that dynamically spins up runners. When PR queue grows, it provisions EKS pods; when done, it reclaims. AWS commonly pairs this with Karpenter for spot nodes, significantly reducing self-hosted runner operational costs.
As orgs grow, copying deploy logic to 100 repos becomes inevitable. That's when reusable workflows pay dividends.
# .github/workflows/shared-deploy.yml in org/shared-workflows
on:
workflow_call:
inputs:
environment:
required: true
type: string
role-arn:
required: true
type: string
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ inputs.role-arn }}
aws-region: ap-northeast-2
- run: ./deploy.sh# All 100 service repos call this
jobs:
prod-deploy:
uses: my-org/shared-workflows/.github/workflows/shared-deploy.yml@v1
with:
environment: production
role-arn: arn:aws:iam::222:role/GHA-Production🔍 Deep Dive: The crucial detail: reusable workflow's OIDC
subvalue differs from normal. It's not the calling workflow's repo but the called reusable workflow's repo + ref that goes into sub. Pattern:repo:my-org/shared-workflows:job_workflow_ref:my-org/shared-workflows/.github/workflows/shared-deploy.yml@refs/heads/main. Trust policy conditions must be written against this. Exams almost always ask this detail when reusable workflow + OIDC appear together.
💡 Related Theory: Reusable workflows are essentially DRY (Don't Repeat Yourself, Hunt & Thomas 1999) applied to CI/CD. With the same logic in 100 repos, one security patch (e.g., changing AssumeRole region) requires 100 applications. Shared workflows change once and automatically apply on next invocation. This is CALMS's Sharing axis expressed in code.
⚠️ Trap 1: Missing
permissions: id-token: write— most common mistake. Must be explicit at workflow or job level.
⚠️ Trap 2:
subcondition with wildcardrepo:org/*— any compromised repo in org reaches prod.
⚠️ Trap 3: Changing
audwithout updating trust policy — ifaws-actions/configure-aws-credentials@v4audienceoption changes, trust policyaudmust match.
⚠️ Trap 4: Attempting session duration > 12 hours — default 1h, max 12h. Increase Role's
MaxSessionDurationproperty and specifyRoleSessionDurationin call.
⚠️ Trap 5: Assuming same workflow_run executed twice uses cached OIDC token on second execution — actually a new token each time. In exams: "Is OIDC token cached?" → No.
⚠️ Trap 6: Thinking
pull_requestevent can access secrets → fork PRs have secrets masked. Butpull_request_targetruns in base repo context so secrets are accessible → fork malicious code execution risk. Frequent exam topic.
sub condition is security's core. environment:production safest, repo:org/* risky.permissions: id-token: write is OIDC's absolute requirement. Missing is the most common failure.sub is by definition repo, not calling repo. Careful writing trust policy.Click a choice to reveal the answer and explanation.
Question 1
What is the most secure way to manage credentials when deploying to AWS from GitHub Actions?
Question 2
What is the risk of setting sub in the OIDC trust policy to repo:my-org/*:ref:refs/heads/main?
Question 3
An OIDC token is not issued in a GitHub Actions workflow. What should be checked first?
Question 4
A build that "runs a migration script against a private RDS database" is required. What is the most appropriate configuration?
Question 5
At which stage does the Required Reviewers feature of GitHub Environments provide protection?
Question 6
What is the default value of the aud condition in the trust policy, and why would you change it?
Question 7
100 repos use common deploy logic through a reusable workflow. How should the trust policy sub condition be written?
Question 8
What is the risk in a scenario where a forked PR accesses the base repo secrets?
Question 9
What are the default and maximum validity periods of temporary credentials issued through OIDC?
Question 10
You built a combination of OIDC + GitHub Environments + trust policy sub: environment:production. Someone tries to run the prod deploy job directly from the main branch. How is this blocked?
OIDC federation is not just "keyless authentication" but a paradigm shift that removes static credentials from the system entirely. Because the JWT sub claim carries the full context (repo, ref, environment, reusable workflow ref), writing the trust policy conditions precisely enables very fine-grained access control. Combined with GitHub Environments + Required Reviewers, it completes a dual defense of a GitHub gate and an AWS IAM gate. The three most common practical pitfalls by far are a missing permissions: id-token: write, a sub wildcard, and misuse of pull_request_target. Tomorrow we revisit this from the perspective of the CodeBuild build system.