Module 4: Secrets Environments And Identity
4. What is OIDC federation
Description
So far, this module solved "where to store a credential safely" (lesson 3: Secrets). This lesson takes a different, deeper step: what if a pipeline could prove its identity to AWS without ever having, at any point, any long-lived credential to store? That's OIDC (OpenID Connect) federation — the mechanism the market audit flags as the competition's most-cited gap. This lesson is purely conceptual: you understand the complete mechanism, piece by piece, before seeing a single line of the production YAML lesson 5 shows in full.
Connection to the module
Lessons 2 and 3 took for granted that a credential exists —an AWS access key— that needs protecting, either by getting it out of the YAML (lesson 3) or by understanding the risk of not doing so (lesson 2). This lesson challenges that premise: what if no key were needed at all? Lesson 5 takes exactly what you learn here and translates it into real, complete YAML, labeled representative for a specific technical reason you're going to understand better after this lesson.
Analogy: the visitor pass versus the copied master key
Imagine an office building with two different ways of giving someone access who only needs to come in for a few hours. The first: you hand them a physical copy of the master key. That copy still works tomorrow, next week, next year — until someone, actively, changes the lock. If the person loses the copy, or lends it out, or someone photographs it without them noticing, that key keeps opening the door indefinitely, for whoever has it.
The second way: at the front desk, the person shows their ID, the desk calls whoever invited them to confirm the visit is legitimate, and they're handed a temporary pass —a card that expires on its own at 6 p.m. that same day. Nobody had to copy any key. If the card gets lost, it stops working on its own, with nobody having to chase it down or change any lock. And every time the person needs to come back, they repeat the same process: identify themselves, someone trusted confirms it, they get a new pass, which expires on its own again.
A traditional IAM user's AWS access key is the first option: a copy that exists until someone revokes it by hand. OIDC is the second: every time the pipeline needs to act, GitHub —the trusted "front desk"— issues a token that expires in minutes, AWS confirms that token comes from a source it already decided to trust, and hands over temporary credentials, just for that specific run. There's no key copy anyone can lose, leak, or forget to rotate.
The complete mechanism, piece by piece
OIDC (OpenID Connect) is a standard protocol —not something invented by GitHub or AWS— for one system to prove its identity to another using a JWT (JSON Web Token, a digitally signed token anyone can read but nobody can forge without the private key of whoever signed it). GitHub Actions acts as the identity provider: when a workflow asks for it, GitHub signs a JWT saying, in essence, "this token comes from a specific run of the andes-cargo/andes-cargo-infra repository, triggered by this event, at this moment." AWS, on the other side, already configured beforehand that it trusts tokens signed by GitHub —with no shared credential needed between the two beforehand.
OIDC'S COMPLETE FLOW (conceptual — no YAML yet)
1. A GitHub Actions job runs, and asks for a JWT
│
▼
2. GitHub (the identity provider) signs a short-lived JWT
with "claims" (assertions) about who's asking:
repository, branch, event that triggered the run
│
▼
3. The job presents that JWT to AWS STS
(Security Token Service), asking to assume an IAM role
│
▼
4. AWS validates the JWT against an already-configured
"trust policy": "I trust tokens signed by GitHub, for THIS
specific repository, for THIS specific role"
│
▼
5. If validation passes, AWS STS returns TEMPORARY
credentials (valid for minutes or hours, never forever)
│
▼
6. The job uses those temporary credentials for the rest
of the run — and when they expire, they're useless for
anything, with nobody having to actively revoke them
Notice the central detail: at no point in this flow does an AWS key exist stored on GitHub, nor a GitHub key stored on AWS. The only thing that exists, beforehand, is a configured trust relationship —step 4— between two systems that never directly share a secret.
The two configuration pieces you're going to see named in lesson 5
permissions: id-token: write
A GitHub Actions workflow, by default, does not have permission to request an OIDC JWT — it's a deliberate security decision: if any workflow could request an identity token without declaring it, it would be much easier for a compromised step in a third-party Action to abuse that power without anyone noticing. The permissions: id-token: write block, explicitly declared at the job level (or the entire workflow's), is the explicit consent: "this job, specifically, needs to be able to request an identity token." Without this line, step 1 in the diagram above simply can't happen — the attempt to request the JWT fails before it ever reaches AWS.
The IAM trust policy
On AWS's side, a normal IAM role can be assumed in several ways — one of them, the one OIDC uses, is through an Identity Provider configured inside IAM that points specifically to token.actions.githubusercontent.com, GitHub Actions' token issuer. That role's trust policy (the JSON document defining who can assume it) doesn't say "I trust this shared secret key" — it says something much more specific and auditable: "I trust tokens signed by this exact issuer, only if the token's sub claim matches repo:andes-cargo/andes-cargo-infra:ref:refs/heads/main" (or whatever exact pattern gets configured). It's the piece that completely replaces the shared key: instead of a secret both sides know, there's a public rule, readable by anyone with access to the role, about exactly which repository, which branch, and which event is allowed to assume that role.
Why this is a qualitative leap, not just "one more hidden key"
It's worth returning, now that this mechanism is clear, to this module's lesson 1's Exercise 2. Moving a credential to a GitHub Secret (lesson 3) reduces the risk of it ending up in a commit, but the credential still exists: it's still a long-lived key, stored somewhere, valid until someone revokes it by hand. OIDC eliminates that piece entirely. There's no AWS key stored in any GitHub Secret. The only thing that permanently exists is the trust configuration on AWS's side (the trust policy) — and that configuration, on its own, with no valid JWT presented at the exact moment of a real run, gives nobody access to anything.
This also answers a question you might have already asked yourself: what happens if someone steals the complete ci.yml file, with all its configuration? With lesson 3's pattern (secrets.AWS_ACCESS_KEY_ID), stealing the YAML does nothing without also stealing the Secret's value —but the Secret exists, somewhere, and that "somewhere" is always a possible target. With OIDC, stealing the complete ci.yml also does nothing: the attacker would also need to get GitHub to sign them a valid JWT on the real repository's behalf —something they can't forge without compromising GitHub's own infrastructure.
Common mistakes
Thinking OIDC means "no credentials at all" (conceptual). What happens: someone concludes that, since there's no long-lived key stored, the flow's final step (using AWS credentials to act) doesn't need any credential either. Why it happens: the phrase "no keys to store" is easy to oversimplify into "no credentials at all." How to spot it: if you think step 6 in the diagram above —using credentials for the rest of the run— doesn't need any real credential. How to fix it: OIDC does produce temporary AWS credentials at the end of the flow (the AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN of an assumed session) — the difference isn't "zero credentials," it's "credentials born already expired within minutes, generated fresh on every run, instead of a key that lives forever until someone kills it by hand."
Assuming the trust policy replaces the need for IAM permissions on resources (scope-based). What happens: someone configures the trust policy correctly —the role trusts the right repository— and assumes that's enough for the pipeline to do anything in AWS. How to spot it: if your mental model of OIDC is "who can get in," without yet thinking about "what they can do once inside." How to fix it: the trust policy controls who can assume the role (the identity question, this lesson's focus); the role's permissions (a separate permissions policy, attached to the same IAM role) control what it can do once it has assumed it — exactly the same least-privilege principle you already applied to LambdaManifestProcessorRole and AppServerRole in terraform-and-iac-guide. A role with a perfect trust policy but overly broad permissions is still a real risk.
Exercises
Exercise 1 — Trace the complete flow from memory. Without looking at this lesson's diagram, describe in your own words OIDC's six steps, from a job requesting a token to it using temporary AWS credentials.
See solution
(1) A GitHub Actions job requests a JWT. (2) GitHub, as identity provider, signs that JWT with information about who's requesting it (repository, branch, triggering event). (3) The job presents that JWT to AWS STS, asking to assume a specific IAM role. (4) AWS validates the JWT against the trust policy already configured on that role — confirms the token comes from a trusted issuer and matches exactly the allowed repository/branch. (5) If validation passes, AWS STS returns temporary credentials, valid for a limited time. (6) The job uses those temporary credentials for the rest of the run; when they expire, they stop working on their own, with nobody having to revoke them.
Exercise 2 — Explain permissions: id-token: write to someone who's never seen it. A colleague asks you why you'd need to explicitly declare a permission just to "request an identity token" — shouldn't a workflow be able to do that whenever it needs to? Answer them in two or three sentences.
See solution
A complete answer sounds, roughly, like this: "It's a deliberate security decision, not an accidental limitation. If any workflow could request an identity token without anyone explicitly declaring it, it would be much easier for a compromised step —for example, a third-party Action with a malicious version— to abuse that power without it being noticed in a YAML review. permissions: id-token: write makes that consent explicit, visible, and auditable line by line, exactly like any other minimal permission we declare on purpose instead of assuming by default."
Exercise 3 — Distinguish the trust policy's role from IAM permissions' role. An IAM role named AndesCargoDeployRole has a trust policy correctly trusting only the andes-cargo/andes-cargo-infra repository, main branch. But its permissions policy gives it full AdministratorAccess over the account. Is this configured correctly? Explain what each piece controls.
See solution
The trust policy is configured correctly — it correctly answers "who can assume this role": only runs from the right repository and branch. But the permissions policy doesn't follow the least-privilege principle: AdministratorAccess gives the pipeline the ability to do absolutely anything in the AWS account, far beyond what a Terraform pipeline over Andes Cargo's four resources actually needs. The trust policy controls "who gets in"; the permissions policy controls "what they can do once inside" — and both pieces need to be configured correctly for the design to be safe, not just one of the two.
Summary and next step
In this lesson you understood OIDC federation's complete mechanism, without yet seeing a line of production YAML: a short-lived JWT, signed by GitHub as identity provider, presented to AWS STS, validated against an IAM trust policy that trusts the issuer instead of storing a shared key, and temporary credentials as the final result. You saw the two configuration pieces —permissions: id-token: write on GitHub's side, the trust policy on AWS's side— and why this is a qualitative leap over moving a credential to a Secret, not just "one more hidden key."
Before moving on you should be able to: draw OIDC's six-step flow from memory; explain what the trust policy controls versus what a role's permissions policy controls; and explain why permissions: id-token: write has to be declared explicitly.
Lesson 5 takes this mechanism and translates it into complete, real YAML —exactly what you'd write in a production pipeline— labeled from its first line as representative, with the exact technical reason it doesn't run in this guide.
Resources
- GitHub Docs — About security hardening with OpenID Connect — the official, complete explanation of OIDC's mechanism in GitHub Actions, this lesson's conceptual basis.
- OpenID Connect — Official specification — the standard GitHub builds its OIDC implementation on; no need to read it in full for this guide, but it confirms OIDC isn't a GitHub proprietary invention.
- AWS Docs — Creating OpenID Connect (OIDC) identity providers — official AWS documentation on how, on IAM's side, the identity provider trusting GitHub tokens gets configured.