Module 4: Secrets and Environment Management

7. OIDC — Authentication Without Static Secrets

Overview

So far, all the secrets management you learned is based on one model: you store a static value (an API key, a token) and use it in the workflows. It works, but it has an inherent problem: the secret exists as a string that someone can copy, expose, or forget to rotate. No matter how careful you are with masking, environments, and rotation, a static secret is fundamentally a string that can be stolen.

OpenID Connect (OIDC) proposes a different model: instead of giving your workflow a secret that says "I have this key, let me through", the workflow says "I am this workflow, from this repository, in this organization, running on this branch" and the destination service verifies that identity directly. There's no intermediate key. Nothing to rotate, nothing to expose, nothing to steal.

This capsule is conceptual. You're not going to implement full OIDC — that requires configuration on a cloud provider (AWS, GCP, Azure) that is part of guide #17 (Deployment & Cloud Infrastructure). What you will understand is the concept, why it's superior to static secrets, how the authentication flow works, and in which situations you should use it.

What you're going to learn:

  1. What problem OIDC solves and why it matters
  2. How identity-based authentication works
  3. The difference between static secrets and OIDC authentication
  4. Which cloud providers support OIDC with GitHub Actions
  5. When to use OIDC and when to stick with static secrets

The problem with static secrets

The lifecycle of a static secret

1. You create the API key in the provider (OpenAI, AWS, etc.)
2. You copy the value and paste it into GitHub Secrets
3. The workflow uses it as an environment variable
4. Every 90 days, you rotate the key manually
5. If someone exposes it, you revoke it and repeat from step 1

Every step is an opportunity for a mistake:

StepRisk
Copying the valueIt stays in the clipboard, in the terminal's history
Pasting into GitHubCopy-paste errors (spaces, newlines)
Using it in a workflowAccidental echo, transformations that bypass masking
Rotating every 90 daysIt's forgotten, postponed, done wrong
Revoking after exposureThe time window between exposure and revocation

The fundamental problem

A static secret is a bearer token:
  → Whoever has it can use it
  → It doesn't matter WHO you are, only WHAT you have
  → If I copy it, I have the same permissions as you
  → The secret doesn't know where the request came from

What OIDC is

OpenID Connect (OIDC) is an identity-based authentication protocol. Instead of verifying "do you have the right key?", it verifies "are you who you say you are?"

The analogy

Static secrets: It's like a physical key. If you have the key, you open the door. It doesn't matter who you are — the key doesn't verify identity. If you lose it, anyone can get in.

OIDC: It's like a biometric check. The door scans you, verifies that it's you (your identity), and decides whether to let you through based on rules ("only employees of department X can enter on Tuesdays"). There's no key to lose.

How it works with GitHub Actions

The simplified OIDC flow:

1. Your workflow requests an identity token from GitHub
   → "I'm the ci.yml workflow, from the repo owner/repo, branch main, run #42"

2. GitHub generates a signed JWT (JSON Web Token)
   → It contains: repo, branch, workflow, actor, event
   → Cryptographically signed by GitHub
   → It expires in minutes (not hours or days)

3. The workflow sends the JWT to the cloud provider (AWS, GCP)
   → "Here's my identity, verify it"

4. The cloud provider verifies the JWT with GitHub
   → "Did GitHub sign this token? Is the repo what it claims to be?"
   → "Are the access conditions met?"

5. If everything is valid, the cloud provider grants temporary credentials
   → A temporary access key (it expires in 1 hour)
   → The workflow uses those temporary credentials to operate

The visual diagram

┌─────────────────┐      1. Give me a token      ┌──────────┐
│  GitHub Actions  │ ──────────────────────────→ │  GitHub   │
│  Workflow        │                              │  OIDC     │
│                  │ ←────────────────────────── │  Provider │
│                  │      2. Signed JWT           └──────────┘
│                  │
│                  │      3. Verify my identity
│                  │ ──────────────────────────→ ┌──────────┐
│                  │                              │  Cloud    │
│                  │ ←────────────────────────── │  Provider │
│                  │      4. Temporary            │  (AWS/    │
│                  │         credentials           │   GCP)   │
│                  │                              └──────────┘
│                  │
│                  │      5. Operate with temp creds
│                  │ ──────────────────────────→ ┌──────────┐
│                  │                              │  Resource │
│                  │                              │  (S3, EC2,│
│                  │                              │   etc.)   │
└─────────────────┘                              └──────────┘

The JWT: what the identity token contains

When GitHub generates the JWT for a workflow, it includes claims (assertions) about the identity:

{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:owner/repo:ref:refs/heads/main",
  "aud": "https://github.com/owner",
  "ref": "refs/heads/main",
  "sha": "abc123def456...",
  "repository": "owner/repo",
  "repository_owner": "owner",
  "actor": "developer-username",
  "workflow": "CI Pipeline",
  "event_name": "push",
  "run_id": "12345678",
  "run_number": "42",
  "iat": 1710000000,
  "exp": 1710000300,
  "nbf": 1710000000
}

The important claims

ClaimMeaningUse in access policies
subSubject — the complete identityRestricting by repo and branch
repositoryThe repo running the workflowLimiting access to specific repos
refThe branch or tagOnly allowing main for production
event_nameThe workflow's triggerDistinguishing push from PR
actorWho triggered the workflowAuditing
expThe token's expirationThe token is ephemeral (minutes)

Why this is safer than a static secret

A static secret:
  → "I have sk-abc123..." → Full access, no context
  → I don't know if it comes from your CI, from a laptop, or from an attacker
  → Valid until you revoke it manually

An OIDC token:
  → "I am repo:owner/my-app:ref:refs/heads/main" → Contextual access
  → I know exactly where it comes from
  → It expires in 5 minutes automatically
  → There's nothing to copy or steal (the token changes on every run)

OIDC with cloud providers

AWS — What it would look like

# Conceptual example — do NOT implement here, see guide #17
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Use AWS resources
        run: |
          aws s3 ls
          aws ecr get-login-password

What changes:

  • Before: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} (a static secret)
  • After: role-to-assume: arn:aws:iam::... (an identity, no secret)

The configuration required in AWS:

  1. Create an Identity Provider in IAM for GitHub Actions
  2. Create an IAM Role with a trust policy that accepts tokens from your repo
  3. Assign the necessary permissions to the role

GCP — What it would look like

# Conceptual example — do NOT implement here, see guide #17
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Authenticate to GCP via OIDC
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/123/locations/global/workloadIdentityPools/github/providers/my-repo
          service_account: github-actions@my-project.iam.gserviceaccount.com

      - name: Use GCP resources
        run: gcloud compute instances list

Azure — What it would look like

# Conceptual example — do NOT implement here, see guide #17
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Login to Azure via OIDC
        uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

Notice that client-id, tenant-id, and subscription-id are not secrets — they're public identifiers. The secret never exists.


Comparison: Static Secrets vs OIDC

AspectStatic SecretsOIDC
RotationManual, every 90 daysNot necessary (ephemeral tokens)
Risk of exposureHigh (the value can be copied)Low (tokens expire in minutes)
ConfigurationSimple (paste into GitHub)Complex (configure the cloud provider)
ScopeAny service that accepts keysOnly cloud providers with OIDC support
AuditingIt's hard to know who used the keyEvery use has a complete identity
RevocationManual, in the providerAutomatic (the token expires on its own)
SupportUniversalAWS, GCP, Azure, HashiCorp Vault

When to use each one

Use Static Secrets when:
  ✅ The service doesn't support OIDC (OpenAI, Anthropic, etc.)
  ✅ It's a small project with no cloud infrastructure
  ✅ You need something fast without complex configuration
  ✅ The service only offers API keys for authentication

Use OIDC when:
  ✅ You work with cloud providers (AWS, GCP, Azure)
  ✅ You have multiple repos that access the same resources
  ✅ Security is critical (compliance, auditing)
  ✅ You want to eliminate manual secret rotation
  ✅ The team has experience with IAM/identity management

OIDC and AI systems: the current reality

What you CAN do with OIDC today

✅ Authenticate against AWS to deploy your AI app
✅ Push Docker images to ECR/GCR without registry passwords
✅ Access S3/GCS to store models and datasets
✅ Deploy to ECS/Cloud Run without static credentials

What you CANNOT do with OIDC (yet)

❌ Authenticate against the OpenAI API (it doesn't support OIDC)
❌ Authenticate against the Anthropic API (it doesn't support OIDC)
❌ Authenticate against Pinecone, Weaviate, etc.
❌ Authenticate against most SaaS AI tools

LLM providers still use static API keys. OIDC applies to the infrastructure where you deploy your app (AWS, GCP), not to the AI APIs your app consumes. That's why you need BOTH: OIDC for cloud infrastructure and static secrets for LLM APIs.

The likely future

Today (2026):
  Cloud infra → OIDC (no secrets)
  LLM APIs    → Static secrets (API keys)

The near future:
  Cloud infra → OIDC
  LLM APIs    → Service accounts with OIDC (when the providers support it)

The id-token: write permission

For a workflow to be able to request a JWT from GitHub, you need to declare the permission:

permissions:
  id-token: write     # Necessary for OIDC
  contents: read      # Reading the repo

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # ... steps that use OIDC

Without id-token: write, the workflow can't request an identity token and OIDC authentication fails.

A security consideration

id-token: write lets the workflow "identify itself" to external services. Only grant this permission to workflows that need it. A workflow that only runs lint and tests shouldn't have this permission.


OIDC's risks and limitations

It's not a silver bullet

OIDC eliminates static secrets for cloud providers, but it introduces its own risks:

RiskDescriptionMitigation
A broad trust policyIf you configure the role to accept "any branch of any repo", a compromised repo can access the resourcesRestrict it to specific repos and branches in the trust policy
Dependency on GitHubIf GitHub Actions has an outage, you can't authenticateHave an emergency static key (only for break-glass)
Setup complexityThe initial configuration is more complex than pasting a keyDocument the setup and automate it with Infrastructure as Code
Difficult debuggingOIDC errors are cryptic ("AccessDenied" with no context)Configure CloudTrail/Cloud Logging for auditing

The "break glass" pattern

Even with OIDC, it's advisable to have an emergency static credential that only gets used when OIDC fails (for example, during a GitHub outage):

Normal:
  Workflow → OIDC → AWS (no secrets)

Emergency (GitHub outage):
  Admin → Break-glass key → AWS (a static secret stored safely)
  → The key gets revoked immediately after resolving the emergency

Connection with guide #17

The complete implementation of OIDC — configuring the Identity Provider in AWS, creating IAM Roles with trust policies, configuring Workload Identity in GCP — is out of scope for this CI/CD guide.

Guide #17 (Deployment & Cloud Infrastructure) covers:

Guide #17: Deployment & Cloud Infrastructure
├── Module X: Configuring AWS IAM for GitHub Actions OIDC
├── Module X: Workload Identity Federation in GCP
├── Module X: Azure Federated Credentials
├── Module X: Deploying to ECS/Cloud Run with OIDC
└── Module X: Monitoring OIDC access

What you learned here — the concept, the flow, the comparisons — prepares you to implement OIDC when you reach that guide.


Troubleshooting

"Can I use OIDC for the OpenAI API?"

Answer: No, OpenAI (and most LLM providers) only accept static API keys. OIDC is for cloud providers (AWS, GCP, Azure). For OpenAI, you need GitHub Secrets as you learned in the previous capsules.

"Does OIDC completely replace GitHub Secrets?"

Answer: No. OIDC replaces static secrets for cloud providers that support it. But you still need GitHub Secrets for:

  • LLM providers' API keys (OpenAI, Anthropic)
  • Tokens for SaaS services without OIDC support
  • Any service that only accepts API keys

"Is OIDC more complex to configure?"

Answer: Yes, the initial configuration is more complex than pasting an API key into GitHub Secrets. You need to configure the cloud provider (IAM roles, trust policies). But once configured, you never have to rotate secrets — the long-term benefit outweighs the initial cost.

"What happens if GitHub Actions goes down — do I lose access?"

Answer: If GitHub Actions can't generate tokens (an outage), your workflows can't authenticate with OIDC. It's an availability risk, not a security one. The mitigation is to have an alternative access mechanism (an emergency static key) for outage situations.

"Does OIDC work with self-hosted runners?"

Answer: Yes. Self-hosted runners can request OIDC tokens just like hosted runners. The token identifies the repo and the workflow, not the runner.


Exercises

Exercise 1: Compare authentication flows

Draw the authentication flow for a deploy to AWS using: (a) static secrets and (b) OIDC. Identify in each flow where a static secret exists that could be stolen.

See solution

Flow A: Static secrets

1. The admin creates an AWS Access Key + Secret Key
   → The secret exists here (in the AWS console)

2. The admin copies and pastes it into GitHub Secrets
   → The secret exists here (on GitHub, in the admin's clipboard)

3. The workflow uses ${{ secrets.AWS_SECRET_ACCESS_KEY }}
   → The secret exists here (in the runner's memory)

4. AWS validates the key
   → Access granted

Risk points: steps 1, 2, 3
The secret is a static value that exists in 3 different places.

Flow B: OIDC

1. The admin configures an IAM Role with a trust policy for the repo
   → There's no secret, only a policy

2. The workflow requests a JWT from GitHub
   → An ephemeral token, not copyable

3. The workflow sends the JWT to AWS
   → AWS verifies the identity with GitHub

4. AWS generates temporary credentials (1 hour)
   → Ephemeral credentials, not reusable

Risk points: none with static secrets
The token changes on every run and expires in minutes.

The conclusion: With OIDC, there is no static string that someone can steal and reuse. Every authentication is ephemeral and verifiable.

Exercise 2: Determine which authentication to use

For each service, decide whether you would use static secrets or OIDC and justify it.

See solution
ServiceAuthenticationReason
OpenAI APIStatic secretsIt doesn't support OIDC, only API keys
AWS S3 (storing models)OIDCAWS supports OIDC, it avoids rotating keys
Docker HubStatic secretsDocker Hub doesn't support OIDC with GitHub
GitHub Container RegistryGITHUB_TOKENThe automatic token, it needs no secrets
GCP Cloud Run (deploy)OIDCGCP supports Workload Identity
Anthropic APIStatic secretsIt doesn't support OIDC
Pinecone (vector DB)Static secretsIt doesn't support OIDC
AWS ECR (Docker registry)OIDCAWS supports OIDC

The general rule: If it's a cloud provider (AWS, GCP, Azure) → OIDC. If it's a SaaS/API → static secrets.

Exercise 3: Write a conceptual workflow with OIDC

Write a GitHub Actions workflow that would use OIDC to authenticate against AWS and push a Docker image to ECR. It doesn't need to work — it's a conceptual exercise.

See solution
# .github/workflows/deploy-with-oidc.yml (conceptual)
name: Deploy to AWS with OIDC
on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
          aws-region: us-east-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push Docker image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/my-ai-app:$IMAGE_TAG .
          docker push $ECR_REGISTRY/my-ai-app:$IMAGE_TAG

      - name: Run AI evaluation (still needs static secret)
        run: python scripts/evaluate_prompts.py
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

Key points:

  • permissions.id-token: write enables OIDC
  • The AWS credentials come from OIDC (no static secrets)
  • The ECR login uses OIDC's temporary credentials
  • OPENAI_API_KEY is still a static secret because OpenAI doesn't support OIDC
  • In a real workflow, you would need to configure the IAM role in AWS first

Exercise 4: Analyze GitHub Actions' JWT

Given the following JWT, identify: the repository, the branch, who triggered the workflow, and when it expires.

See solution
{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:acme-corp/ai-assistant:ref:refs/heads/main",
  "repository": "acme-corp/ai-assistant",
  "repository_owner": "acme-corp",
  "actor": "alice",
  "ref": "refs/heads/main",
  "workflow": "Deploy to Production",
  "event_name": "push",
  "run_id": "98765432",
  "iat": 1710000000,
  "exp": 1710000300
}

The analysis:

QuestionAnswer
Repositoryacme-corp/ai-assistant
Branchmain (from ref: refs/heads/main)
Who triggered italice (the actor claim)
When it expires5 minutes after issue (exp - iat = 300 seconds)
Eventpush to main
Workflow"Deploy to Production"

The security implication: A cloud provider can configure its trust policy to only accept tokens from the repo acme-corp/ai-assistant, branch main, event push. Any other combination would be rejected.


Summary

  • OIDC eliminates static secrets for cloud providers — there's no key to rotate, expose, or steal
  • Identity-based authentication: "I am this workflow, from this repo" instead of "I have this key"
  • An ephemeral JWT: the identity token expires in minutes and changes on every run
  • Supported by: AWS, GCP, Azure, HashiCorp Vault
  • NOT supported by: OpenAI, Anthropic, most SaaS AI tools
  • You need both: OIDC for cloud infrastructure + static secrets for LLM APIs
  • The required permission: id-token: write in the workflow
  • The complete implementation is in guide #17 (Cloud Infrastructure)
  • It's the future: the trend is toward zero-static-secrets, but adoption is gradual

Additional resources

  1. GitHub Actions OIDC — Official Docs — Official documentation of OIDC in GitHub Actions
  2. AWS — GitHub Actions OIDC — Configuring OIDC with AWS IAM
  3. GCP — Workload Identity Federation — OIDC with Google Cloud
  4. Azure — Federated Credentials — OIDC with Azure
  5. OpenID Connect Specification — OIDC's technical specification
  6. GitHub Blog — OIDC for GitHub Actions — GitHub's introductory article about OIDC