Module 4: Secrets Environments And Identity

5. The OIDC pattern in YAML, named

Description

This lesson is representative from its first line. The YAML you're going to read here is exactly what you'd write in a real production pipeline, against a real AWS account — but it doesn't run in this guide. The reason isn't laziness or a shortcut: it's a researched, confirmed technical limitation, with a direct quote from an act maintainer, plus a second reason from this guide's design. You're going to read the complete pattern, step by step, understanding each piece with the vocabulary you built in lesson 4 — and you're going to know, precisely, where to go if you wanted to actually build it.

Connection to the module

Lesson 4 gave you the complete conceptual mechanism: JWT, permissions: id-token: write, trust policy. This lesson translates that mechanism, without changing a single idea, into real YAML and a real trust policy JSON document. Lesson 6 follows the same honesty pattern —representative, labeled, with cited technical reason— for Environments with required approval.


Why this lesson doesn't execute anything, with the exact quote

Two reasons, researched separately, both necessary for this YAML to actually run:

Reason 1 — act doesn't implement OIDC token issuance. Verified directly against a public discussion in act's own repository, with a response from a project collaborator (ChristopherHX):

"Additionally nektos/act doesn't implement it's own oidc tokens. (needs to change the jwk endpoint)"

"That's impossible, because only GitHub Actions from github.com can sign the token."

The second sentence is the most important one to understand: it's not that act "hasn't gotten around to adding" this feature as some roadmap coincidence — it's that signing the JWT (step 2 in lesson 4's diagram) depends on cryptographic infrastructure that only exists inside real github.com. act runs workflows on your machine, but it isn't GitHub — it doesn't have the private key GitHub uses to sign those tokens, and it couldn't safely simulate it even if it wanted to.

Reason 2 — there's no real AWS account to federate against. Even if act could issue a valid JWT, step 4 of the flow (AWS validates the JWT against a trust policy) needs a real IAM role, configured in a real AWS account, with a real Identity Provider pointing at token.actions.githubusercontent.com. LocalStack —this entire guide's $0 lab— doesn't implement OIDC validation: it only accepts the dummy test/test credentials without validating anything against any account. Even if act could issue the token, there'd be nothing real on the other side to validate it.

Both reasons are independently necessary, and neither one gets resolved within this guide's $0 scope. That's why this pattern gets shown, in full, explained step by step —what a production pipeline would write— and labeled representative at the exact moment it appears, not at the lesson's end.


The complete YAML, explained step by step

Step 1 — The Identity Provider in IAM (once, per AWS account)

Before any workflow can request credentials, the AWS account needs to register GitHub as a trusted source. This is done once, not on every run — it's infrastructure configuration, typically declared in Terraform (something terraform-and-iac-guide didn't cover, because at that point this guide didn't yet exist as part of the ecosystem's thread):

resource "aws_iam_openid_connect_provider" "github_actions" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = [
    "sts.amazonaws.com",
  ]

  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1",
  ]
}

url points at GitHub Actions' exact token issuer —the same token.actions.githubusercontent.com lesson 4 named. client_id_list limits which audience can use this provider (sts.amazonaws.com, the AWS service that's going to validate the token). thumbprint_list is a cryptographic fingerprint of the issuer's certificate, which AWS uses to confirm it's talking to GitHub's real server, not an impostor.

Step 2 — The IAM role and its trust policy

resource "aws_iam_role" "andes_cargo_deploy" {
  name = "AndesCargoDeployRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github_actions.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" = "repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main"
          }
        }
      }
    ]
  })
}

Action = "sts:AssumeRoleWithWebIdentity" —not plain sts:AssumeRole— is the specific STS variant for federation with an external token, instead of with another AWS credential. The Condition block is where this trust policy's precision lives: StringLike on the JWT's sub (subject) claim restricts exactly which repository and which branch can assume this role —repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main, not a single character wider than that. A badly written trust policy, with too broad a pattern (for example, without the branch part), would let any branch of that repository —including a feature branch created by any collaborator— assume a role meant only for main. This is, verified against AWS's official documentation, the most common OIDC configuration mistake in real implementations.

Step 3 — The workflow that assumes the role

name: apply

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  terraform-apply:
    runs-on: ubuntu-latest
    steps:
      - name: Check out andes-cargo-infra
        uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v6
        with:
          role-to-assume: arn:aws:iam::123456789012:role/AndesCargoDeployRole
          aws-region: us-east-1

      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.15.8"

      - name: Terraform apply
        run: terraform apply -auto-approve -input=false

Three new pieces compared to everything you've built so far:

  • permissions: id-token: write —at the workflow level, not the step's— is the explicit consent you already know from lesson 4: without this line, the next step fails before attempting anything.
  • aws-actions/configure-aws-credentials@v6 is AWS's official Action that orchestrates the entire lesson 4 flow for you: it requests the JWT from GitHub, presents it to AWS STS with role-to-assume, and —if step 2's trust policy allows it— exports the resulting temporary credentials as environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN) for the following steps to use with no additional configuration.
  • A note on @v6: GitHub's official documentation, in its canonical example of this same pattern, pins this Action by commit SHA, not by tag —aws-actions/configure-aws-credentials@<40-character-sha>— precisely for the supply-chain security practice already named in Module 2, lesson 5: a tag can point at a different commit in the future; a SHA can't. This lesson uses @v6 (the Action's current major) for readability —it's easier to read and remember in a learning context— but a real production pipeline should prefer the fixed SHA of whichever release it uses.
  • Notice what does not show up anywhere in this workflow: no secrets.AWS_ACCESS_KEY_ID, no .secrets, no Secret configured in Settings. None is needed — it's exactly the central point of OIDC versus lesson 3's pattern.

What to expect if you tried to run this with act (representative, the exact failure)

If you saved this YAML as .github/workflows/oidc-demo.yml in andes-cargo-infra/ and ran act push -j terraform-apply, the Configure AWS credentials via OIDC step would fail — not from a syntax error, but exactly for Reason 1 above: act has no way of issuing the JWT that step needs to request. The real error reported by act users who tried this exact pattern (documented in the discussion cited above) is consistent with "couldn't retrieve the OIDC token" — the Action never even gets to try talking to AWS STS, because the earlier step, requesting the token from GitHub, already fails inside act's own environment.


Where to actually build this: cloud-security-and-guardrails-guide

This guide doesn't build Step 1 or Step 2 against a real AWS account —it would need IAM permissions beyond what LocalStack can offer, and a real AWS account would break this entire guide's $0 commitment. cloud-security-and-guardrails-guide is the sister guide that does build this pattern end to end: the Identity Provider, the role, the complete trust policy, and a real workflow running against a real AWS account, with the level of security depth —SAST/DAST, supply chain, policy as code— that's deliberately outside this guide's scope.


Common mistakes

Writing this YAML inside Andes Cargo's real ci.yml or apply.yml, "to have it ready" (flow-based). What happens: someone, motivated by how complete this pattern looks, copies it directly into a real project workflow, replacing LocalStack's dummy credentials. Why it happens: the YAML looks finished and correct —because it is, for a real account— and it's tempting to "get ahead" on the work. How to spot it: if your real apply.yml, run against LocalStack, now has an aws-actions/configure-aws-credentials step with role-to-assume. How to fix it: revert that change — LocalStack doesn't have any real IAM role to assume via OIDC, and that step would fail immediately, even running on real GitHub, because there's no valid role-to-assume pointing at anything. The dummy-credentials pattern (test/test, via Secrets since lesson 7) remains the right one for everything this guide executes against LocalStack.

Confusing thumbprint_list with something that can be made up or copied from any example (configuration-based). What happens: someone copies the thumbprint_list value from an old tutorial, without checking whether it's still valid. Why it happens: it's a hexadecimal value that doesn't seem to change, so it gets assumed static forever. How to spot it: if your real OIDC configuration (outside this guide) uses a thumbprint_list copied from an article more than a year old, without confirming it against AWS's current documentation. How to fix it: the thumbprint corresponds to GitHub's server certificate, and certificates get renewed periodically — an outdated value can silently break OIDC validation. cloud-security-and-guardrails-guide is the guide that goes deeper into keeping this correct in a real deployment.


Exercises

Exercise 1 — Cite, from memory, this lesson's two technical reasons. Without looking back, explain the two independent reasons this YAML doesn't run in this guide, and why both are necessary —solving just one isn't enough.

See solution

Reason 1: act doesn't implement OIDC token issuance — confirmed by a project collaborator (ChristopherHX, nektos/act discussion #2029): signing the JWT depends on cryptographic infrastructure exclusive to real github.com, which act neither has nor can safely simulate. Reason 2: even if act could issue the token, there's no real AWS account with an IAM role and trust policy configured to validate it against — LocalStack doesn't implement OIDC validation. They're independent because solving Reason 1 (if act ever implemented it) wouldn't solve Reason 2, and vice versa: both pieces would be needed at once for this pattern to actually run in this guide.

Exercise 2 — Find the flaw in an incomplete trust policy. A colleague writes this condition for their trust policy: "StringLike": { "token.actions.githubusercontent.com:sub": "repo:andes-cargo/andes-cargo-infra:*" }. What security problem does it have, compared to this lesson's?

See solution

The pattern repo:andes-cargo/andes-cargo-infra:* lets any reference within that repository assume the role —any branch, any Pull Request, any tag— not just main. This lesson's trust policy is much narrower: repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main, which only allows runs triggered specifically from the main branch. With the colleague's broad pattern, anyone able to open a feature branch in that repository —or even a Pull Request from a fork, depending on configuration— could, in theory, get a workflow on that branch to assume the production deployment role.

Exercise 3 — Explain what aws-actions/configure-aws-credentials exports and why it matters. Without looking at this lesson's YAML, explain exactly what the Configure AWS credentials via OIDC step does, and why the next step (Terraform apply) needs no additional env: with credentials.

See solution

aws-actions/configure-aws-credentials orchestrates lesson 4's entire OIDC flow: it requests the JWT from GitHub, presents it to AWS STS along with the specified role-to-assume, and if the role's trust policy allows it, receives temporary credentials back. The Action exports those credentials as standard environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN) for the rest of the same job's steps — that's why terraform apply, in the next step, needs no explicit env:: the AWS CLI (and, by extension, Terraform's provider) already knows to read those three environment variables automatically, with no additional configuration.


Summary and next step

In this lesson you read the complete OIDC pattern, in real YAML: IAM's Identity Provider (once, per account), the role's trust policy (restricted to an exact repository and branch), and the workflow that uses aws-actions/configure-aws-credentials with role-to-assume to assume that role with no key stored at all. You saw the two cited, verified technical reasons this pattern doesn't run in this guide, and the direct pointer to cloud-security-and-guardrails-guide to build it against a real account.

Before moving on you should be able to: write an OIDC trust policy's structure from memory, including the condition on sub; explain why aws-actions/configure-aws-credentials needs no GitHub Secret at all; and cite the exact reason —with source— why act can't execute this pattern.

Lesson 6 applies this exact same honesty pattern —representative, with cited technical reason— to a different mechanism: GitHub Environments with required human approval.

Resources

  1. GitHub Docs — Configuring OpenID Connect in Amazon Web Services — this lesson's complete pattern's official source, including the SHA-pinning example.
  2. aws-actions/configure-aws-credentials — GitHub — the Action's official repository, with its complete README and role-to-assume examples.
  3. nektos/act discussion #2029 — the exact source for this lesson's quote about why act doesn't implement OIDC token issuance.
  4. AWS Docs — Creating a role for web identity or OpenID Connect Federation — official AWS documentation on OIDC's trust policy, including Condition's complete syntax.
  5. cloud-security-and-guardrails-guide (NIEVA) — the guide that builds this pattern end to end against a real AWS account, with SAST/DAST, supply chain, and policy-as-code, all outside this guide's scope.