GitHub Actions Secrets
Secrets are encrypted environment variables stored by GitHub and injected into workflow runs. They allow you to use API keys, tokens, and passwords in your automated workflows without ever storing them in your code or workflow files.
Why secrets exist
Workflow YAML files are committed to your repository and visible to everyone with read access. If you wrote AWS_SECRET_KEY: abc123 directly in the YAML, anyone who can read the file — or anyone who forks the repo — would have your credentials. Secrets solve this by keeping the values encrypted in GitHub and replacing them with *** in logs.
Setting up repository secrets
Navigate to your repository on GitHub.
Click Settings → Secrets and variables → Actions.
Click New repository secret.
Enter a name (e.g.
VERCEL_TOKEN) and the secret value.Click Add secret.
Using secrets in a workflow
Accessing secrets with ${{ secrets.NAME }}
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to AWS
run: aws s3 sync ./dist s3://my-bucket
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1 # not sensitive, inline is finePassing secrets as action inputs
- name: Send Slack notification
uses: slackapi/slack-github-action@v1
with:
channel-id: 'C1234567890'
slack-message: 'Deployment complete!'
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}The GITHUB_TOKEN secret
GitHub automatically provides a special GITHUB_TOKEN secret in every workflow run. You do not need to create it — it is injected automatically. It is scoped to the current repository and expires when the workflow finishes.
Using GITHUB_TOKEN
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
- name: Comment on PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Tests passed! Ready for review.'
})Restricting GITHUB_TOKEN permissions
permissions: contents: read # can read repo content pull-requests: write # can comment on PRs issues: write # can create/edit issues packages: write # can publish packages # Everything else defaults to 'none'
Types of secrets
Type | Scope | Where to set | Best for |
|---|---|---|---|
Repository secret | Single repository | Repo Settings → Secrets and variables → Actions | Credentials specific to one project |
Environment secret | Specific deployment environment (staging/prod) | Repo Settings → Environments → <env name> → Secrets | Different credentials per environment |
Organization secret | Multiple repositories in an org | Org Settings → Secrets and variables → Actions | Shared tokens used across many repos |
GITHUB_TOKEN | Single workflow run | Automatically injected | Interacting with GitHub API, creating releases |
Environment secrets — different credentials per environment
Using named environments with their own secrets
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging # uses staging environment secrets
steps:
- name: Deploy to staging
run: ./deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }} # staging API_KEY from 'staging' env
DB_URL: ${{ secrets.DB_URL }}
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # uses production environment secrets
steps:
- name: Deploy to production
run: ./deploy.sh
env:
API_KEY: ${{ secrets.API_KEY }} # production API_KEY from 'production' env
DB_URL: ${{ secrets.DB_URL }}Security: pull requests from forks
The safe pattern for fork PRs is to split your workflow: run tests without secrets using the pull_request event, then deploy using pull_request_target with manual approval.
Safe pattern: test without secrets on fork PRs
# This runs for ALL PRs (including forks) but has no secrets access
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test # no secrets needed for testsWarning: do not echo secrets in logs
BAD: never do this
# BAD — secret may appear in logs in base64 form
- run: echo "${{ secrets.MY_SECRET }}" | base64
# BAD — exposes secret in environment dump
- run: envGOOD: use secrets only where needed
# GOOD — pass secret as environment variable to a specific command
- name: Run deployment
run: ./deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
# GOOD — mask a computed value if you must process a secret
- run: |
COMPUTED_VALUE=$(echo "${{ secrets.MY_SECRET }}" | some-transform)
echo "::add-mask::$COMPUTED_VALUE" # tell GitHub to redact this tooRotating secrets
Generate a new token/key with the service provider.
Update the secret value in GitHub: Settings → Secrets → click the secret → Update.
Old value is immediately replaced — the next workflow run uses the new value.
Revoke the old token at the service provider.
Rotate secrets on a schedule (quarterly or after any team member leaves).
Use short-lived tokens (OIDC) where possible — see GitHub OIDC documentation.
Using OIDC for keyless authentication (advanced)
Instead of storing long-lived cloud provider credentials as secrets, you can configure GitHub Actions to request short-lived tokens from AWS, GCP, or Azure using OpenID Connect (OIDC). No secrets to rotate, no long-lived credentials.
Keyless AWS authentication with OIDC
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 — no stored secrets!)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: us-east-1
- name: Deploy to S3
run: aws s3 sync ./dist s3://my-bucket