GitHub Actions Secrets Management: From Leak Risks to OIDC Keyless Deployment
One weekend in March 2025, the GitHub Security Team sent an urgent email to over 23,000 repository owners.
Their secrets might have been exposed.
The culprit was an action called tj-actions/changed-files—this widely-used tool had been compromised by attackers, quietly stealing all environment variables and secrets from workflows. You might be wondering: my project uses this action too, am I affected?
To be honest, this incident brought a problem many people overlook to the forefront: how exactly should you manage secrets in GitHub Actions?
In this article, we’ll discuss three things: how to choose between the three-tier architecture for secrets, how to follow 8 security rules, and how OIDC can help you completely say goodbye to the nightmare of static credential leaks.
1. GitHub Actions Secrets Three-Tier Architecture
GitHub provides three levels to store secrets: Repository, Environment, and Organization. Which one to choose? The answer is: it depends on your scenario.
Repository Secrets: The First Choice for Personal Projects
This is the simplest level. Secrets are stored at the repository level, accessible to all workflows. If you have a personal project, monorepo, and no multi-environment deployment needs—this is enough.
The only downside: you can’t distinguish between staging and production secrets with the same name. For example, if you have a DATABASE_URL with different values for staging and production, what do you do? This is where Environment Secrets come in.
Environment Secrets: Essential for Multi-Environment Deployment
Environment secrets can be isolated by environment and also support approval workflows. You can create staging and production environments in your GitHub repository settings and configure different secrets for each.
A key feature: only jobs that reference that environment can access the corresponding secrets. This makes security boundaries clearer.
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # Reference staging environment
steps:
- run: echo "Deploying to staging..."
- env:
API_KEY: ${{ secrets.API_KEY }} # Secret from staging environment
deploy-production:
runs-on: ubuntu-latest
environment: production # Reference production environment (can configure approval)
steps:
- run: echo "Deploying to production..."
- env:
API_KEY: ${{ secrets.API_KEY }} # Secret from production environment
In the configuration above, deploy-staging can only access staging environment secrets, while deploy-production can only access production environment secrets. They don’t interfere with each other.
Additionally, Environments support “protection rules”—for example, the production environment can be configured to require manual approval before execution. This is especially useful in team collaboration.
Organization Secrets: Team Sharing, Unified Management
If your team has dozens of repositories, each needing to configure the same AWS_ACCESS_KEY—copying and pasting dozens of times, then changing dozens of times when updating—just thinking about it is overwhelming.
Organization secrets solve this problem. Configure once at the organization level, and all repositories can use it. You can also control which repositories have access: all repositories, or a specific list.
# In your repository's workflow, usage is exactly the same as repository secrets
steps:
- env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
How to Choose?
Simply put:
| Scenario | Recommended Level |
|---|---|
| Personal project, single environment | Repository Secrets |
| Multi-environment deployment (staging/production) | Environment Secrets |
| Team multi-repo, shared credentials | Organization Secrets |
From my experience, many projects start with Repository Secrets and migrate to Environment Secrets when they have multi-environment needs—the migration cost isn’t high, but it’s still recommended to plan ahead.
2. Secrets Security Best Practices — 8 Iron Rules
We’ve covered how to store secrets, now let’s discuss how to use them. I’ve summarized 8 rules learned from real-world experience, each with lessons behind them.
1. Follow Naming Conventions
Use all caps + underscores, like AWS_ACCESS_KEY_ID. Don’t use lowercase, don’t use camelCase. The reason is simple: you can tell at a glance this is a secret, and it won’t be confused with regular variables.
2. Don’t Store JSON as a Single Secret
This is a classic pitfall. Some people stuff an entire configuration file into one secret:
{"api_key": "xxx", "db_url": "yyy", "token": "zzz"}
Then parse it in the workflow using fromJson. The problem is: once this secret leaks, all sensitive information leaks together. The correct approach is to store each value as an independent secret.
3. Pass Explicitly, Don’t Inline
# Wrong approach ❌
- run: my-cli --token ${{ secrets.MY_TOKEN }}
# Correct approach ✅
- env:
MY_TOKEN: ${{ secrets.MY_TOKEN }}
run: my-cli --token $MY_TOKEN
Why? GitGuardian’s research shows that command-line arguments can be seen by other processes on the same machine through ps x -w. Environment variables are relatively much safer.
4. Rotate Regularly
Change every 30 to 90 days. I know rotation is annoying—but compared to remediation after a leak, this hassle is really nothing. The Blacksmith team suggests that if you use cloud services (AWS/GCP), you can combine with OIDC to completely skip this step.
5. Pin Actions to SHA
The first line of defense against supply chain attacks.
# Wrong approach ❌
- uses: tj-actions/changed-files@v45
# Correct approach ✅
- uses: tj-actions/changed-files@b827595e0a7e97537d7c7a2f458b5a8e6d5c8e39
Use commit SHA instead of version tags. Tags can be tampered with by attackers; SHA is immutable.
6. Give GITHUB_TOKEN Only Minimum Permissions
GitHub automatically provides GITHUB_TOKEN for each workflow. The default permissions are too high—it can write code and modify issues. It’s recommended to change it to read-only in your workflow or repository settings:
permissions:
contents: read
7. Check if Secrets are Properly Masked in Logs
GitHub automatically replaces ${{ secrets.XXX }} with *** in logs. But if you write it like this:
- run: echo "Token is $MY_TOKEN"
The real token value will appear in logs. Remember to test your workflows to confirm there’s no accidental exposure.
8. Register Derived Sensitive Values
If your workflow generates new sensitive values from one secret (like generating a JWT from an API key), register this new value as a secret too, don’t just pass it in memory.
These 8 rules aren’t just theory—after the tj-actions incident, the StepSecurity team audited thousands of public repositories and found that a significant proportion violated these rules. Fixing them isn’t hard, but it takes time to check each item.
3. OIDC — Keyless Cloud Deployment Authentication
The fourth rule mentioned “regularly rotate secrets.” Honestly, rotation is really tedious—every time you have to manually change the AWS console, update GitHub secrets, and notify team members.
OIDC (OpenID Connect) provides a way out: simply don’t store secrets.
How Does OIDC Work?
Traditional approach: You create an IAM user in AWS, generate an access key, and store the key in GitHub secrets. Each time the workflow runs, it uses this static key to request AWS resources.
OIDC approach: GitHub acts as an identity provider, proving to AWS that “this workflow comes from the eastondev/my-repo repository.” After AWS verifies, it issues a short-term JWT token (valid for minutes to hours). The workflow uses this temporary token to complete tasks, and the token automatically expires after it expires.
No need to store any long-term credentials, no rotation, no leak risk.
AWS OIDC Configuration Example
The steps are divided into two parts: AWS-side trust relationship configuration, GitHub-side token request.
AWS Side (Console Operations):
- Create an IAM Identity Provider with URL set to
https://token.actions.githubusercontent.com - Create an IAM Role with trust policy limited to your repository:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789: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:eastondev/my-repo:ref:refs/heads/main"
}
}
}]
}
This configuration means: only the main branch of the eastondev/my-repo repository can assume this role.
GitHub Side (Workflow Configuration):
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Required: request OIDC token
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucket
Notice there’s no secrets.AWS_ACCESS_KEY here—direct role authentication.
GCP and Azure
All three major clouds support OIDC with similar configurations:
| Cloud Platform | GitHub Action | Official Documentation Keyword |
|---|---|---|
| AWS | aws-actions/configure-aws-credentials | OIDC federation |
| GCP | google-github-actions/auth | Workload Identity Federation |
| Azure | azure/login | Federated Identity Credentials |
An Unexpected Benefit of OIDC
According to johal.in’s testing, OIDC authentication latency is about 87% lower than traditional secrets approach. The reason is simple: no need to read credentials from GitHub secrets API, directly exchange local JWT for temporary token.
From my personal experience, OIDC is the preferred approach for cloud deployment. The only barrier is slightly more complex configuration—but once configured, you never have to worry about it again.
4. Supply Chain Attack Protection — tj-actions Incident Review
Let’s return to the tj-actions/changed-files incident mentioned at the beginning. How did it happen? What can we learn?
Incident Timeline
In March 2025, attackers obtained maintainer permissions for the tj-actions repository (the specific method is still under investigation, possibly credential leak or account compromise). They inserted a malicious script into the v45 version code, which quietly reads all environment variables and secrets during workflow execution and sends them to attacker-controlled servers.
Semgrep’s analysis shows that all workflows using tj-actions/changed-files@v45 are affected—whether you use GitHub secrets or OIDC, as long as this action can access environment variables, they will be stolen.
Unit42 Palo Alto’s report points out that over 23,000 repositories used this action. Including forks of some well-known projects.
Lessons Learned
This incident exposed several issues:
- Action version tags are untrustworthy—attackers can modify tags to point to malicious commits
- Third-party actions can access your secrets—once compromised, all secrets are exposed
- Historical run logs may leak sensitive information—even if fixed now, past run records may still have traces
Your Checklist
If you’ve ever used tj-actions/changed-files, it’s recommended to check each item:
□ Check workflow logs to confirm no secrets leaked to output
□ Rotate all potentially exposed secrets (API keys, tokens, etc.)
□ Pin actions to commit SHA instead of version numbers
□ Audit the maintainer source of other third-party actions
□ Consider using Dependabot or Renovate to automate action version checks
For projects not yet compromised, pinning SHA is the most important protection measure. Although SHA is harder to read than version numbers, this is the only way to prevent tag tampering.
Additionally, GitHub mentioned an important direction in their 2026 security roadmap: separating code contribution permissions from credential management permissions. In the future, more fine-grained access control may be introduced, allowing third-party actions to only access necessary secrets, not all. This is good news—but for now, we still need to defend ourselves.
Conclusion
GitHub Actions Secrets management isn’t a complex technical problem, but a security practice requiring continuous attention. To summarize:
How to choose the three-tier architecture: Repository Secrets for personal projects, switch to Environment Secrets for multi-environment deployment, Organization Secrets for team sharing.
How to follow security rules: Pin SHA, explicit passing, regular rotation—these three are most important.
How to authenticate cloud deployment: OIDC is the first choice, no secrets storage, no rotation hassles.
How to prevent supply chain attacks: Limit third-party actions’ secrets access, audit maintainer sources.
After learning from the tj-actions incident, my approach is: all actions pinned to SHA, all cloud deployments using OIDC, and monthly secrets audits. This process took about two weeks to establish, but afterwards the maintenance cost is very low.
If you haven’t started these checks yet, I suggest running through the checklist in this article today. Compared to remediation afterwards, the cost of prevention beforehand is much lower.
Configure GitHub Actions OIDC Keyless Deployment
Configure OIDC authentication for AWS cloud services to achieve secure deployment without storing static credentials
⏱️ Estimated time: 30 min
- 1
Step1: Create IAM Identity Provider
Create an identity provider in AWS IAM console:
• Provider URL: https://token.actions.githubusercontent.com
• Audience: sts.amazonaws.com
• Record the generated Provider ARN - 2
Step2: Create IAM Role and Configure Trust Policy
Create an IAM Role with trust policy limited to your GitHub repository:
```json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID: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:OWNER/REPO:ref:refs/heads/main"
}
}
}]
}
```
• Replace ACCOUNT_ID with your AWS account ID
• Replace OWNER/REPO with your GitHub repository
• Can remove :ref:refs/heads/main to allow all branches - 3
Step3: Configure Workflow to Use OIDC
Request OIDC token in GitHub Actions workflow:
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Required: request OIDC token
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/GitHubActionsRole
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucket
```
• id-token: write is the required permission declaration
• No need to configure any secrets.AWS_ACCESS_KEY_ID - 4
Step4: Test and Verify
Run workflow to verify OIDC configuration:
• Check workflow logs, confirm successful authentication
• Verify role-to-assume is correct
• Confirm AWS resources can be accessed without any static credentials
• Test target operations (like S3 sync, ECR push, etc.)
FAQ
What's the difference between Repository Secrets and Environment Secrets?
How to use secrets safely in workflows?
• Explicit passing: Inject through env field, don't inline use ${{ secrets.XXX }}
• Least privilege: Only pass to steps that need it, when using GITHUB_TOKEN set permissions: contents: read
• Regular rotation: Recommend rotating every 30-90 days, cloud deployments can use OIDC to completely skip rotation
What is OIDC? Why is it safer than traditional secrets?
How to prevent supply chain attacks (like tj-actions incident)?
• Pin Actions to commit SHA, don't use version tags
• Limit secrets to only pass to trusted actions
• Audit third-party action maintainer sources
• Regularly run Dependabot or Renovate to check version updates
Will secrets leak in logs?
What scenarios are Organization Secrets suitable for?
10 min read · Published on: Apr 18, 2026 · Modified on: Apr 20, 2026
GitHub Actions Complete Guide
If you landed here from search, the fastest way to build context is to jump to the previous or next post in this same series.
Previous
GitHub Actions Cache Strategy: Speed Up CI/CD Pipeline 5x
A practical guide to GitHub Actions cache strategy: complete configuration examples from npm to Docker, cache key design best practices, and performance optimization data. Master the caching mechanism to accelerate your CI/CD pipeline 5x and save build costs.
Part 4 of 5
Next
This is the latest post in the series so far.
Related Posts
GitHub Actions Matrix Build: Multi-Version Parallel Testing in Practice
GitHub Actions Matrix Build: Multi-Version Parallel Testing in Practice
GitHub Actions Getting Started: YAML Workflow Basics and Trigger Configuration
GitHub Actions Getting Started: YAML Workflow Basics and Trigger Configuration
GitHub Actions Getting Started: YAML Workflow Basics and Trigger Configuration

Comments
Sign in with GitHub to leave a comment