Back to guides

Secrets management in GitHub Actions: prevent leaks and rotate safely

By Davy Rogers

GitHub Actions workflows need secrets for deployments, API calls, and package publishing. Here's how to manage them without leaking them.

Use GitHub Repository Secrets

Store secrets in repository or organization settings, never in workflow files:

# .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        env:
          API_KEY: ${{ secrets.DEPLOY_API_KEY }}
        run: ./deploy.sh

Reference: GitHub - Using Secrets in GitHub Actions

Use Environments for Sensitive Secrets

GitHub Environments let you add protection rules before a workflow can access secrets:

  • Required reviewers: a human must approve before the workflow runs.
  • Wait timer: enforced delay before deployment.
  • Deployment branches: only specific branches can access the environment.
jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment: production  # requires approval
    steps:
      - name: Deploy
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: ./deploy.sh

Reference: GitHub - Using Environments for Deployment

Never Print Secrets in Logs

GitHub automatically masks secrets in logs, but this is not foolproof:

# BAD - can leak through encoding, substrings, or indirection
- run: echo "Key is ${{ secrets.API_KEY }}"

# BAD - base64 encoding bypasses masking
- run: echo "${{ secrets.API_KEY }}" | base64

# GOOD - mask additional values manually
- run: |
    echo "::add-mask::$DERIVED_VALUE"
    ./deploy.sh

Common leak vectors:

  • Error messages that include the secret value.
  • Debug mode (ACTIONS_STEP_DEBUG) showing environment.
  • Encoding transformations (base64, URL encoding).
  • Multi-line secrets where only the first line is masked.

Reference: GitHub - Masking a Value in a Log

Restrict pull_request_target Workflows

The pull_request_target event runs with access to repository secrets, even for PRs from forks. This is a major attack vector.

# DANGEROUS - runs untrusted code with secrets access
on: pull_request_target
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # checks out fork code
      - run: npm install  # runs fork's package.json scripts with secrets in env

Rules:

  • Never checkout PR code in pull_request_target workflows.
  • If you must, run untrusted code in a separate job without secrets access.
  • Prefer pull_request (no secrets access) for CI on external PRs.

Reference: GitHub - Events That Trigger Workflows

Use OIDC Instead of Long-Lived Credentials

GitHub Actions supports OpenID Connect (OIDC) for cloud provider authentication. This eliminates long-lived secrets entirely.

AWS

permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/github-deploy
      aws-region: us-east-1

Reference: GitHub - Configuring OIDC in AWS

GCP

steps:
  - uses: google-github-actions/auth@v2
    with:
      workload_identity_provider: projects/123/locations/global/workloadIdentityPools/github/providers/my-repo
      service_account: [email protected]

Reference: GitHub - Configuring OIDC in GCP

Azure

steps:
  - uses: azure/login@v2
    with:
      client-id: ${{ secrets.AZURE_CLIENT_ID }}
      tenant-id: ${{ secrets.AZURE_TENANT_ID }}
      subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

Reference: GitHub - Configuring OIDC in Azure

Pin Actions to Full SHA

Third-party actions can be compromised. Pin to a commit SHA, not a tag:

# BAD - tag can be moved to a malicious commit
- uses: actions/checkout@v4

# GOOD - immutable reference
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Reference: GitHub - Security Hardening for GitHub Actions

Set Minimum Permissions

Use the permissions key to restrict what the GITHUB_TOKEN can do:

permissions:
  contents: read    # only what's needed
  packages: write   # if publishing packages

Start with no permissions and add only what the workflow needs:

permissions: {}  # no permissions by default

Reference: GitHub - Automatic Token Authentication

Rotate Secrets

  • Rotate secrets on a regular schedule (90 days or less).
  • Rotate immediately if a secret may have been exposed.
  • Use GitHub's secret scanning to detect leaked tokens: GitHub - Secret Scanning

Checklist

  • All secrets stored in GitHub Settings (repo or org level), not in code
  • Environments configured with required reviewers for production
  • No echo or logging of secret values
  • pull_request_target workflows do not checkout fork code with secrets
  • OIDC used for cloud provider auth (no long-lived keys)
  • Third-party actions pinned to full commit SHA
  • GITHUB_TOKEN permissions set to minimum required
  • Secret rotation schedule in place
  • Secret scanning enabled

Related Guides

Published 04 Mar 2026

Frequently asked questions

Where should I store secrets used by GitHub Actions?
In GitHub repository secrets, organisation secrets, or (best) environment secrets gated by required reviewers. Never commit them to a workflow file or .env in the repo.
How do I avoid long-lived AWS or Azure keys in CI?
Use OpenID Connect (OIDC) federation. GitHub issues a short-lived token your cloud provider trusts, so no static credential ever touches the runner.
Should I pin actions by tag or SHA?
Always by full commit SHA. Tags are mutable - a compromised maintainer can repoint v1 to a malicious commit. SHA pinning protects you and is enforced by Cyber Essentials-aligned policies.
What do I do if a secret leaks in workflow logs?
Rotate the secret immediately, audit downstream services for misuse, then redact and re-run if practical. Treat leaked-but-private as leaked-publicly.

Related

Want a professional to look at it?Get an AppSec Health Check.