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:
- What problem OIDC solves and why it matters
- How identity-based authentication works
- The difference between static secrets and OIDC authentication
- Which cloud providers support OIDC with GitHub Actions
- 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:
| Step | Risk |
|---|---|
| Copying the value | It stays in the clipboard, in the terminal's history |
| Pasting into GitHub | Copy-paste errors (spaces, newlines) |
| Using it in a workflow | Accidental echo, transformations that bypass masking |
| Rotating every 90 days | It's forgotten, postponed, done wrong |
| Revoking after exposure | The 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
| Claim | Meaning | Use in access policies |
|---|---|---|
sub | Subject — the complete identity | Restricting by repo and branch |
repository | The repo running the workflow | Limiting access to specific repos |
ref | The branch or tag | Only allowing main for production |
event_name | The workflow's trigger | Distinguishing push from PR |
actor | Who triggered the workflow | Auditing |
exp | The token's expiration | The 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:
- Create an Identity Provider in IAM for GitHub Actions
- Create an IAM Role with a trust policy that accepts tokens from your repo
- 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
| Aspect | Static Secrets | OIDC |
|---|---|---|
| Rotation | Manual, every 90 days | Not necessary (ephemeral tokens) |
| Risk of exposure | High (the value can be copied) | Low (tokens expire in minutes) |
| Configuration | Simple (paste into GitHub) | Complex (configure the cloud provider) |
| Scope | Any service that accepts keys | Only cloud providers with OIDC support |
| Auditing | It's hard to know who used the key | Every use has a complete identity |
| Revocation | Manual, in the provider | Automatic (the token expires on its own) |
| Support | Universal | AWS, 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:
| Risk | Description | Mitigation |
|---|---|---|
| A broad trust policy | If you configure the role to accept "any branch of any repo", a compromised repo can access the resources | Restrict it to specific repos and branches in the trust policy |
| Dependency on GitHub | If GitHub Actions has an outage, you can't authenticate | Have an emergency static key (only for break-glass) |
| Setup complexity | The initial configuration is more complex than pasting a key | Document the setup and automate it with Infrastructure as Code |
| Difficult debugging | OIDC 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
| Service | Authentication | Reason |
|---|---|---|
| OpenAI API | Static secrets | It doesn't support OIDC, only API keys |
| AWS S3 (storing models) | OIDC | AWS supports OIDC, it avoids rotating keys |
| Docker Hub | Static secrets | Docker Hub doesn't support OIDC with GitHub |
| GitHub Container Registry | GITHUB_TOKEN | The automatic token, it needs no secrets |
| GCP Cloud Run (deploy) | OIDC | GCP supports Workload Identity |
| Anthropic API | Static secrets | It doesn't support OIDC |
| Pinecone (vector DB) | Static secrets | It doesn't support OIDC |
| AWS ECR (Docker registry) | OIDC | AWS 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: writeenables OIDC- The AWS credentials come from OIDC (no static secrets)
- The ECR login uses OIDC's temporary credentials
OPENAI_API_KEYis 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:
| Question | Answer |
|---|---|
| Repository | acme-corp/ai-assistant |
| Branch | main (from ref: refs/heads/main) |
| Who triggered it | alice (the actor claim) |
| When it expires | 5 minutes after issue (exp - iat = 300 seconds) |
| Event | push 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: writein 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
- GitHub Actions OIDC — Official Docs — Official documentation of OIDC in GitHub Actions
- AWS — GitHub Actions OIDC — Configuring OIDC with AWS IAM
- GCP — Workload Identity Federation — OIDC with Google Cloud
- Azure — Federated Credentials — OIDC with Azure
- OpenID Connect Specification — OIDC's technical specification
- GitHub Blog — OIDC for GitHub Actions — GitHub's introductory article about OIDC