GitHub Actions secrets allow you to store sensitive data — API keys, passwords, tokens, certificates — securely in your repository settings and use them in workflows without exposing them in your code. This guide covers everything from basic secret usage to advanced patterns like OIDC federation, environment-scoped secrets, and automated rotation.
Basic Secret Usage
Secrets are defined in your repository settings (Settings > Secrets and variables > Actions) and referenced in workflows using the secrets context. They are automatically masked in workflow logs.
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to server
env:
# Reference a repository secret
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_TOKEN: ${{ secrets.API_TOKEN }}
run: |
echo "Deploying with secure credentials..."
./deploy.shEnvironment-Scoped Secrets
GitHub Environments let you define secrets that are specific to a deployment environment (staging, production) and add protection rules like required reviewers.
# Environment-specific secrets (staging vs production)
name: Deploy
on:
push:
branches: [main, staging]
jobs:
deploy:
runs-on: ubuntu-latest
# Pick environment based on branch
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
# These come from the selected environment's secrets
DATABASE_URL: ${{ secrets.DATABASE_URL }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: ./deploy.sh --env ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}OIDC Federation: No Long-Lived Secrets
The best practice in 2026 is to use OIDC (OpenID Connect) federation to authenticate with cloud providers instead of storing long-lived access keys. GitHub Actions can exchange a JWT token for temporary cloud credentials.
# OIDC: No long-lived credentials needed!
name: Deploy to AWS (OIDC)
on:
push:
branches: [main]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
# No ACCESS_KEY or SECRET_KEY secrets needed!
- name: Deploy to S3
run: aws s3 sync ./dist s3://my-bucket --deleteSecret Masking and Safe Usage
GitHub automatically masks secret values in logs, but there are important caveats to understand to avoid accidental exposure.
# Secrets are automatically masked in logs
# But be careful with derived values!
jobs:
careful:
runs-on: ubuntu-latest
steps:
- name: Use secret safely
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
run: |
# This output will be masked: ***
echo "Secret value: $MY_SECRET"
# DANGER: This may expose the secret!
# If MY_SECRET="abc", base64 gives "YWJj" which is NOT masked
echo $MY_SECRET | base64
# Safe: always use secrets directly, not derived forms
curl -H "Authorization: Bearer $MY_SECRET" https://api.example.com/endpoint
- name: Add to mask (for dynamic secrets)
run: |
# Manually mask a dynamically generated value
TOKEN=$(generate-token.sh)
echo "::add-mask::${TOKEN}"
echo "TOKEN=${TOKEN}" >> $GITHUB_ENVSecrets in Reusable Workflows
When using reusable workflows (workflow_call), secrets must be explicitly passed through. They are not inherited automatically.
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy Workflow
on:
workflow_call:
secrets:
DEPLOY_KEY:
required: true
DATABASE_URL:
required: true
SLACK_WEBHOOK:
required: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: ./deploy.sh
- name: Notify Slack
if: ${{ secrets.SLACK_WEBHOOK != '' }}
run: |
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"Deployment complete!"}' \
${{ secrets.SLACK_WEBHOOK }}
# .github/workflows/production.yml — caller
name: Production Deploy
on:
push:
branches: [main]
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
secrets:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}Automated Secret Rotation
Long-lived secrets are a security risk. Set up automated rotation using scheduled workflows.
# Automated secret rotation pattern
name: Rotate API Token
on:
schedule:
- cron: '0 0 1 * *' # Monthly rotation
workflow_dispatch: # Manual trigger
jobs:
rotate:
runs-on: ubuntu-latest
steps:
- name: Generate new token
id: generate
env:
OLD_TOKEN: ${{ secrets.API_TOKEN }}
run: |
# Call API to generate new token using old token
NEW_TOKEN=$(curl -s -X POST \
-H "Authorization: Bearer $OLD_TOKEN" \
https://api.example.com/tokens/rotate | jq -r '.token')
echo "::add-mask::${NEW_TOKEN}"
echo "new_token=${NEW_TOKEN}" >> $GITHUB_OUTPUT
- name: Update GitHub secret
uses: gliech/create-github-secret-action@v1
with:
name: API_TOKEN
value: ${{ steps.generate.outputs.new_token }}
github_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}Secret Storage Options Comparison
| Option | Scope | Visibility | Rotation | Cost | Best For |
|---|---|---|---|---|---|
| Repository Secrets | Single repo | Repo admins | Manual | Free | Simple projects |
| Environment Secrets | Single repo + env | Repo admins | Manual | Free | Multi-stage deploys |
| Organization Secrets | Multiple repos | Org admins | Manual | Free | Shared credentials |
| OIDC Federation | Single repo | No stored secret | Automatic | Free | Cloud deployments |
| HashiCorp Vault | Any | Configurable | Automatic | Paid | Enterprise |
Security Best Practices
- Use OIDC federation for cloud providers (AWS, GCP, Azure) instead of static access keys whenever possible.
- Scope secrets to environments. Production secrets should require environment protection rules (required reviewers, deployment branches).
- Never print secrets in workflow steps, even for debugging. GitHub masks the exact string, but encoding/transformations bypass masking.
- Rotate secrets regularly. Set calendar reminders or use automated rotation workflows for API keys.
- Use the minimum required permissions. Create service accounts with narrow scopes specifically for GitHub Actions.
- Audit secret access via GitHub's audit log. Review which workflows are using which secrets.
Frequently Asked Questions
Can forked repositories access secrets?
No. By default, secrets are not passed to workflows triggered by pull requests from forks. This prevents forked repos from exfiltrating your secrets. For public repos, you can configure which events allow secrets for external contributors, but this requires explicit approval.
What is the difference between repository secrets and organization secrets?
Repository secrets are only available to workflows in that specific repository. Organization secrets can be shared across multiple repositories and have a policy setting that controls which repos can access them (all repos, selected repos, or only private repos).
How many secrets can I store?
GitHub allows up to 100 secrets per repository and 1,000 per organization. Each secret can be at most 64 KB in size. For larger secrets (like certificates), consider storing a base64-encoded version and decoding it in the workflow.
Can I read a secret value after it is stored?
No. Once a secret is created, its value cannot be read through the GitHub UI or API. You can only update or delete it. This is by design — if you lose track of a secret, create a new one and rotate the credentials.
How do I pass secrets between jobs in a workflow?
Secrets cannot be directly passed between jobs as job outputs (outputs are visible in logs). Instead, re-reference the secret in each job that needs it using secrets.MY_SECRET, or generate a temporary credential in one job and pass it through encrypted artifact storage using GPG.