JustAppSec
Back to guides

Secrets Management in GitHub Actions

GitHub Actions workflows need secrets for deployments, API calls, and package publishing. This guide covers how to manage those secrets 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


Content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly, send corrections to [email protected]. Always consult official documentation and validate key implementation decisions before making design or security choices.

Need help?Get in touch.