GitHub Actions Secrets and Environments — Security Guide 2026
Advertisement
Introduction
Why This Matters
Leaked CI/CD credentials are one of the most common vectors for cloud infrastructure breaches. In 2026, supply chain attacks targeting GitHub Actions workflows — stealing secrets via malicious third-party actions or pull request triggers — have become a standard attack pattern.
GitHub Actions provides multiple layers of secrets management: repository secrets, environment secrets, organization secrets, and OIDC-based keyless authentication. Understanding how each layer works, and when to use which, is essential for any team deploying production workloads through GitHub Actions.
GitHub Actions Secrets Hierarchy
GitHub offers four scopes of secrets, from broadest to narrowest:
| Scope | Where set | Who can access |
|---|---|---|
| Organization secrets | Org Settings | Selected or all repos in the org |
| Repository secrets | Repo Settings | All jobs in any workflow |
| Environment secrets | Repo Settings > Environments | Only jobs referencing that environment |
| Codespaces secrets | User/Org Settings | Development codespaces only |
Rule of thumb: always store secrets at the narrowest scope that meets your needs. Production database credentials should be environment secrets, not repository secrets.
Setting Secrets via the GitHub UI
Navigate to your repository's Settings → Secrets and variables → Actions, then:
- Click New repository secret
- Enter the name (e.g.,
DATABASE_URL) — uppercase with underscores is conventional - Enter the value
- Click Add secret
Secrets are encrypted at rest and masked in logs. You cannot read a secret's value after saving it — only overwrite it.
Setting Secrets via the GitHub CLI
# Set a repository secret
gh secret set DATABASE_URL --body "postgres://user:pass@host:5432/db"
# Set a secret from a file (useful for kubeconfig, private keys)
gh secret set KUBECONFIG < ~/.kube/config
# Set an environment secret
gh secret set PROD_API_KEY --env production --body "sk-live-abc123"
# List secrets (names only — values are never shown)
gh secret list
gh secret list --env production
# Delete a secret
gh secret delete OLD_SECRETUsing Secrets in Workflows
Secrets are accessed via the secrets context in workflow YAML:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy application
run: |
./scripts/deploy.sh
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
- name: Configure kubectl
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/configImportant: secrets are automatically masked in logs — any step that prints the secret value will show *** instead. However, avoid logging secrets intentionally, and never echo them in scripts that might be committed.
Environments and Protection Rules
Environments add a deployment approval gate between CI and production. Configure them under Settings → Environments.
jobs:
deploy-production:
runs-on: ubuntu-latest
environment:
name: production
url: https://myapp.example.com
steps:
- name: Deploy
run: helm upgrade --install myapp ./charts/myapp --atomic
env:
KUBECONFIG: ${{ secrets.PRODUCTION_KUBECONFIG }}Environment protection rules you should enable:
- Required reviewers — list team members or teams who must approve the deployment
- Wait timer — add a delay (e.g., 10 minutes) before deployment can proceed, giving time to cancel if needed
- Deployment branches — restrict the environment to specific branches (e.g.,
mainonly)
Keyless Authentication With OIDC
The most secure pattern in 2026 is to eliminate long-lived secrets entirely using OIDC federation. GitHub Actions can request short-lived cloud provider credentials automatically.
AWS OIDC integration:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC
contents: read
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::123456789012:role/GitHubActionsRole
aws-region: us-east-1
- name: Deploy to EKS
run: |
aws eks update-kubeconfig --name my-cluster --region us-east-1
kubectl apply -f k8s/AWS IAM role trust policy:
{
"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:myorg/myrepo:environment:production"
}
}
}
]
}GCP Workload Identity federation:
- name: Authenticate to Google Cloud via OIDC
uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/123/locations/global/workloadIdentityPools/github/providers/github
service_account: deployer@my-project.iam.gserviceaccount.comPreventing Secret Leaks in Pull Requests
Pull requests from forked repositories do not have access to repository secrets by default — this is a critical security boundary. However, pull_request_target runs in the context of the base repository and DOES have access to secrets. Misusing it is dangerous.
# SAFE: secrets are NOT available to PRs from forks
on: pull_request
# DANGEROUS: secrets ARE available; only use when required for PR labeling/commenting
on: pull_request_targetNever run untrusted code (e.g., npm test) in a pull_request_target workflow with secrets available.
Restrict pull_request_target to safe operations only:
on: pull_request_target
jobs:
label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Add label
uses: actions/labeler@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}Rotating and Auditing Secrets
# Rotate a secret (overwrite the existing value)
gh secret set DATABASE_URL --body "postgres://user:newpass@host:5432/db"
# Audit which workflows use a secret
grep -r "secrets.DATABASE_URL" .github/workflows/
# View secret usage in the GitHub audit log (org admins only)
gh api /orgs/myorg/audit-log --jq '.[] | select(.action == "secret.access")'Set a calendar reminder to rotate long-lived secrets every 90 days. Use OIDC wherever possible to eliminate the rotation burden entirely.
Common Mistakes
- Using
pull_request_targetto run tests from forks — this exposes secrets to untrusted code; usepull_request(no secrets) for testing fork contributions. - Storing secrets as repository secrets when environment secrets suffice — repository secrets are accessible to all workflows; use environment secrets to limit blast radius.
- Base64-encoding secrets thinking it adds security — base64 is encoding, not encryption; GitHub will still mask it when the decoded value is in the secrets store.
- Not restricting OIDC role trust policies — use the
subcondition to limit which repo, branch, and environment can assume an AWS/GCP role. - Checking
secrets.*into workflow conditions — never useif: secrets.MY_SECRET != ''to conditionally run steps; it partially reveals the secret in error messages.
Best Practices
- Prefer OIDC over long-lived secrets — no rotation, no storage, no accidental commit risk; available for AWS, GCP, Azure, and HashiCorp Vault.
- Scope OIDC trust to environments — include
:environment:productionin thesubclaim condition so only the production environment workflow can assume production roles. - Enable environment protection rules — required reviewers for production deployments prevent accidental or malicious deployments.
- Audit secret access in CI logs — ensure no step accidentally prints secrets; inspect logs after initial setup.
- Use
${{ secrets.GITHUB_TOKEN }}— it is automatically provided, scoped to the repository, and expires after the workflow run; prefer it over PATs for GitHub API calls.
Key Takeaways
- GitHub Actions has four secret scopes: organization, repository, environment, and codespaces; always use the narrowest scope that meets your needs.
- Environment secrets are only accessible to jobs that explicitly reference the environment — use them for production credentials.
- OIDC federation lets GitHub Actions workflows assume cloud provider roles without storing long-lived credentials anywhere.
- Restrict OIDC IAM trust policies by repository and environment using the
subclaim to prevent privilege escalation across repos. - The
pull_request_targetevent has access to secrets and runs in the base repo context; never run untrusted fork code in this event. - GitHub automatically masks secret values in workflow logs — any output matching a stored secret is replaced with
***. - Environment protection rules (required reviewers, wait timers, branch restrictions) add a human approval gate before production deployments.
- Rotate long-lived secrets every 90 days; switch to OIDC-based keyless authentication to eliminate rotation overhead entirely.
Advertisement