When using GitHub Actions to access cloud providers like AWS, GCP, or Azure, static API keys (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, etc.) pose the primary risk of leakage. OpenID Connect (OIDC) integration allows the GitHub Actions runner to directly authenticate with the cloud environment, eliminating the need to store long-lived secrets in environment variables. This approach uses short-lived (short-term) access tokens that are valid only while the workflow is running.
Storing static credentials in GitHub repository or organization secrets leads to serious security vulnerabilities, such as the risk of unauthorized access or third-party code reading these sensitive information. OIDC protocol positions GitHub as a trusted Identity Provider (IdP), enabling your cloud provider (AWS IAM, GCP Workload Identity, Azure AD) to directly define temporary roles (IAM Role) based on this trust relationship. In this guide, we will explore how to set up a secret-less architecture between GitHub Actions and cloud platforms, including token structure and security hardening details.
What is GitHub Actions OIDC and How Does it Solve the Long-Lived Secret Problem?
GitHub Actions OIDC is a standardized authentication mechanism that allows GitHub to produce a cryptographically signed JSON Web Token (JWT) when a workflow runs, which is then used to request temporary access to the target cloud platform. In traditional CI/CD pipelines, a long-lived IAM user is created and its keys are copied and pasted into GitHub Secrets. This approach not only incurs operational costs but also poses a significant security risk in case of a breach.
The biggest threats posed by static secret usage are:
- Leakage Risk: Long-lived access information accidentally written to logs or commit history can be used indefinitely by attackers.
- Key Rotation Difficulty: In large corporate structures with hundreds of repositories, regularly changing static keys requires complex automation and is often neglected.
- Over-Privilege: Typically, a single "CI/CD IAM user" is created with broad permissions for all projects, violating the principle of least privilege.
OIDC architecture eliminates the need to store static secrets. When a workflow starts, the GitHub OIDC provider generates a special JWT for the runner. The cloud provider (e.g., AWS STS) verifies the JWT signature using GitHub's public key and checks if the claims (repository, environment, ref) match the predefined IAM rules. If validated, the cloud provider delivers temporary credentials to the runner, which are automatically destroyed when the workflow completes.
ℹ️ OIDC Basic Principle
When using OIDC, you do not store AWS or GCP access keys in the "Secrets" section of your GitHub repository settings. All you need to provide is the ARN information of the target IAM role or the pool name. Since this information is not sensitive, it can be written openly in the repository.
How OpenID Connect (OIDC) Authentication Flow Works
The OIDC authentication process involves a 6-step handshake mechanism between the GitHub Runner, GitHub OIDC Provider, and Cloud Security Service (AWS STS / GCP Security Token Service). The entire process is automated with cryptographic key verifications.
The following diagram illustrates the token exchange and role assumption steps that occur in the background when a GitHub Actions workflow runs:
Let's examine the technical steps of this flow:
- Token Request: When
id-token: writepermission is defined in a GitHub Actions workflow, the runner requests a token from the local OIDC service. - JWT Generation: The GitHub OIDC provider generates a JWT containing information about the repository, actor, and environment.
- Transmission to Cloud: The runner sends this JWT token to the AWS STS (
AssumeRoleWithWebIdentity) or GCP Workload Identity endpoint. - Cryptographic Verification: The cloud provider verifies the JWT signature using the public key from
https://token.actions.githubusercontent.com/.well-known/openid-configurationand checks if the token claims match the predefined IAM rules. - Temporary Credential Delivery: If validated, the cloud provider delivers temporary access credentials to the runner.
- Resource Access: The runner uses these temporary credentials to access cloud resources like S3, EKS, or Cloud Run.
AWS IAM and GitHub Actions OIDC Integration Step-by-Step
To integrate GitHub Actions with AWS using OIDC, first, you need to add GitHub as an Identity Provider (OIDC) in AWS IAM and then create an IAM role that trusts this provider.
1. Define Identity Provider
You can add the GitHub OIDC provider to your AWS account using the AWS CLI:
aws iam create-open-id-connect-provider \
--url "https://token.actions.githubusercontent.com" \
--client-id-list "sts.amazonaws.com"
# --thumbprint-list parameter is now automatically managed by AWS and usually does not need to be specified.
AWS manages the GitHub OIDC thumbprint verification automatically. When using the CLI or Terraform/CloudFormation, providing the thumbprint-list parameter is usually not necessary and can be ignored if specified.
2. Configure IAM Role and AssumeRole Policy
After adding the OIDC provider, create an IAM role that only allows access from a specific GitHub organization and repository. The Trust Policy of this role is the most critical security layer, as it prevents unauthorized repositories from accessing your AWS account.
Below is an example trust-policy.json that allows role assumption only when the main branch of the my-app repository in the mustafaerbay organization is running:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:mustafaerbay/my-app:ref:refs/heads/main"
}
}
}
]
}
This policy allows the role to be assumed only by the specified repository and branch.
To create the role with this trust policy and attach necessary permissions (e.g., S3 deployment permissions), use the following commands:
# Create IAM role with trust policy
aws iam create-role \
--role-name GitHubActions-S3-Deploy-Role \
--assume-role-policy-document file://trust-policy.json
# Attach policy to the role
aws iam attach-role-policy \
--role-name GitHubActions-S3-Deploy-Role \
--policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess
3. Write GitHub Actions Workflow YML File
In your GitHub repository, create a .github/workflows/deploy.yml file with id-token: write permission and use the official aws-actions/configure-aws-credentials action:
name: Deploy to AWS S3
on:
push:
branches:
- main
permissions:
id-token: write # Required for OIDC JWT token
contents: read # To read code in the repository
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: AWS Credentials OIDC Configuration
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActions-S3-Deploy-Role
aws-region: eu-central-1
- name: S3 Sync Job
run: |
aws s3 sync ./dist s3://my-production-bucket-name --delete
When the workflow runs, the configure-aws-credentials action obtains a JWT token directly from GitHub's internal OIDC server, requests temporary AWS credentials from AWS STS, and populates the environment variables with these credentials.
GCP (Google Cloud) Workload Identity Federation Configuration
On the Google Cloud Platform (GCP) side, OIDC authentication is provided through the "Workload Identity Federation" service. GCP allows external identity providers (like GitHub) to be linked with Service Accounts, eliminating the need for secrets.
Workload Identity Pool and Provider Creation
Using the GCP CLI (gcloud), create a Workload Identity Pool and a GitHub OIDC provider:
# 1. Create Workload Identity Pool
gcloud iam workload-identity-pools create "github-actions-pool" \
--project="my-gcp-project-id" \
--location="global" \
--display-name="GitHub Actions Pool"
# 2. Add GitHub OIDC Provider
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
--project="my-gcp-project-id" \
--location="global" \
--workload-identity-pool="github-actions-pool" \
--display-name="GitHub Provider" \
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
--issuer-uri="https://token.actions.githubusercontent.com"
The --attribute-mapping parameter maps claims from the GitHub JWT to the GCP IAM context.
Service Account Linking and Workflow Example
To link the created Identity Pool with a GCP Service Account, grant the roles/iam.workloadIdentityUser role:
# Grant Workload Identity User role to the Service Account
gcloud iam service-accounts add-iam-policy-binding "ci-deployer@my-gcp-project-id.iam.gserviceaccount.com" \
--project="my-gcp-project-id" \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider" \
--condition="expression=assertion.repository == 'mustafaerbay/my-app' && assertion.ref == 'refs/heads/main',title=github-repo-main-branch,description=Allow only main branch of my-app repo"
Warning: The gcloud iam service-accounts add-iam-policy-binding command grants permissions to a Service Account. Incorrect or overly broad permissions can lead to unauthorized access to your cloud resources. Ensure that the role and conditions are appropriate for the least privilege principle before applying this command. Test changes in a non-production environment and have a backup or rollback plan.
In your GitHub Actions workflow YML file, use Google's official google-github-actions/auth action:
name: Deploy to GCP Cloud Run
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
jobs:
deploy-gcp:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Authenticate to Google Cloud via OIDC
uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider'
service_account: 'ci-deployer@my-gcp-project-id.iam.gserviceaccount.com'
- name: Cloud Run Deploy
run: |
gcloud run deploy my-service --image gcr.io/my-gcp-project-id/my-image:latest --region europe-west1
⚠️ Critical Security Alert
When configuring Workload Identity, it's crucial to include the
attribute.repositoryandattribute.ref(branch) constraints in the IAM condition to prevent unauthorized access. Failing to do so may allow any repository or branch to assume the production Service Account.
Security Hardening: Subject Claim (sub) and Audience (aud) Restrictions
When integrating OIDC, one of the most significant mistakes is not restricting the sub (Subject) claim in the cloud provider's validation process. If the IAM trust policy does not constrain the sub field, any publicly accessible GitHub repository can assume the role in your cloud account.
The JWT produced by the GitHub OIDC service contains claims such as:
| Claim Name | Example Value | Description |
|---|---|---|
iss |
https://token.actions.githubusercontent.com |
The OIDC provider URL that issued the token. |
aud |
sts.amazonaws.com or a custom URL |
The intended audience of the token. |
sub |
repo:org/repo:ref:refs/heads/main |
Details about the repository and trigger. |
repository |
org/repo |
The full name of the GitHub repository. |
actor |
mustafaerbay |
The user who triggered the workflow. |
environment |
production |
The environment name if defined in GitHub. |
To maximize security, minimize the use of wildcards (*) in IAM conditions.
The following table compares common sub pattern usage and their security levels:
| Usage Purpose | Condition Sub Pattern | Security Level |
|---|---|---|
| Main Branch Only | repo:my-org/my-repo:ref:refs/heads/main |
Very High (Recommended) |
| Production Environment | repo:my-org/my-repo:environment:production |
Very High |
| All Branches and PRs in a Repo | repo:my-org/my-repo:* |
Medium (Only for dev/test roles) |
| All Repositories in an Organization | repo:my-org/* |
Low (Only for general read roles) |
Especially for production deployment roles, including the environment condition is an excellent security hardening measure. When you enable "Required Reviewers" for the production environment in GitHub, unauthorized individuals or automations are completely prevented from assuming the production role.
Token Durations, Rotation, and Edge Cases Management
When transitioning to OIDC integration, there are operational and technical details to consider, particularly regarding token durations and GitHub Actions runner architecture.
The default validity period of a role assumed through AWS STS is 1 hour (3600 seconds). However, for longer-running integration tests or large container image builds, this duration might be insufficient. You can adjust the duration using the duration-seconds parameter in the configure-aws-credentials action:
- name: AWS Credentials Configuration (2 Hours Duration)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActions-LongJob-Role
aws-region: eu-central-1
role-duration-seconds: 7200 # Maximum 12 hours (dependent on IAM role limit)
However, the IAM role's MaxSessionDuration setting on the AWS side defaults to 1 hour. If you request 7200 seconds, you must first increase the role's maximum session duration using the AWS CLI:
aws iam update-role \
--role-name GitHubActions-LongJob-Role \
--max-session-duration 28800 # Between 1 hour (3600) and 12 hours (43200)
Warning: The aws iam update-role command modifies an IAM role's session duration. This directly affects the validity period of temporary credentials assumed by the role. Increasing max-session-duration beyond the necessary period can extend the time frame for potential misuse of leaked credentials. Evaluate security implications and apply the least privilege principle when making this change.
Another edge case is the use of Self-Hosted Runners. Self-hosted runners, when running in an on-premise network, must have outbound access to https://token.actions.githubusercontent.com to obtain OIDC tokens. If your environment has restrictive outbound firewall rules or proxies, the OIDC token request may timeout (HTTP 403 or Connection Timeout). Ensure that your security firewall allows necessary HTTPS (Port 443) access to the GitHub OIDC domain.
💡 PR and Fork Security
In open-source or large team projects, pull requests (PRs) from forks are, by default, unable to obtain OIDC tokens. GitHub security ensures that
id-token: writepermission is passive for forked repositories, preventing malicious PRs from accessing your cloud account.
Conclusion
GitHub Actions OIDC integration is a modern, sustainable standard that eliminates the risks associated with static secret management in cloud infrastructure security. By granting id-token: write permission to your workflows and defining cloud-side IAM roles with strict sub claim constraints, you can create fully temporary and auditable authorizations without storing a single long-lived API key or secret in your GitHub repository or cloud account.
After transitioning to OIDC architecture, your first step should be to remove all static CI/CD IAM users and access keys from your cloud environment and clean up any access keys stored in GitHub Secrets. Adhering to the principle of least privilege by defining separate IAM roles for each repository and environment will elevate your CI/CD security posture to the highest level.
Top comments (0)