GitSecrets in GitHub Actions

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
  1. Navigate to your repository on GitHub.

  2. Click Settings → Secrets and variables → Actions.

  3. Click New repository secret.

  4. Enter a name (e.g. VERCEL_TOKEN) and the secret value.

  5. Click Add secret.

Naming convention
Secret names are conventionally ALL_CAPS_WITH_UNDERSCORES. They cannot start with `GITHUB_` (reserved for GitHub's own tokens).
Using secrets in a workflow

Accessing secrets with ${{ secrets.NAME }}

YAML
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 fine

Passing secrets as action inputs

YAML
- 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

YAML
- 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.'
      })
GITHUB_TOKEN permissions
By default, `GITHUB_TOKEN` has read permissions for most resources and write permissions for the repository it is running in. You can restrict or expand permissions using the `permissions:` key in the workflow.

Restricting GITHUB_TOKEN permissions

YAML
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

YAML
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 }}
Tip
Configure environment protection rules on the `production` environment to require manual approval before the deploy job runs: Settings → Environments → production → Required reviewers.
Security: pull requests from forks
Warning
For security, secrets are NOT available to workflows triggered by pull requests from forks (i.e. `pull_request` events from external contributors). If a workflow tries to access `secrets.MY_SECRET` in a fork PR, the value will be empty — not redacted, but empty. This prevents attackers from creating a PR that exfiltrates your secrets via a modified workflow.

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

YAML
# 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 tests
Warning: do not echo secrets in logs
Warning
GitHub automatically redacts known secret values from logs, replacing them with `***`. However, this is not foolproof — if you base64-encode, URL-encode, or otherwise transform a secret value, GitHub may not recognise it and will print it in plain text. Never echo, print, or log secret values, even as debugging.

BAD: never do this

YAML
# BAD — secret may appear in logs in base64 form
- run: echo "${{ secrets.MY_SECRET }}" | base64

# BAD — exposes secret in environment dump
- run: env

GOOD: use secrets only where needed

YAML
# 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 too
Rotating 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

YAML
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