IAnswerable

Handle Secrets in GitHub Actions Safely

03 Feb 2026

secrets.AWS_SECRET_ACCESS_KEY sitting in a repo-level GitHub secret feels secure. It’s the setup that leaks most often, and usually through a pull request from a fork you didn’t think to worry about.

Why fork PRs are the danger zone

pull_request workflows triggered by a fork PR do not get access to your repo’s secrets, by default — GitHub blocks it. That’s the safe default. The leak happens when someone switches to pull_request_target to work around a legitimate limitation (needing write access, or needing secrets to run a check), without understanding what changes.

pull_request_target runs with the base repo’s permissions and secrets, but can be configured to check out the fork’s code. If your workflow does that and then runs anything from the checked-out code — a build script, a test command, a linter with a plugin system — a malicious fork PR can get that code to run with your secrets in scope.

The dangerous path looks like this:

flowchart TD
    A["Fork opens a PR"] --> B{"Workflow trigger?"}
    B -->|"pull_request"| C["No repo secrets in scope\n(safe default)"]
    B -->|"pull_request_target"| D["Base repo secrets in scope"]
    D --> E{"Checks out fork's code\nand runs it?"}
    E -->|"No"| F["Safe: privileged actions only\n(comment, label)"]
    E -->|"Yes"| G["Fork's code runs\nwith your secrets"]
  
# Dangerous: base repo secrets + untrusted fork code in the same job
on: pull_request_target
jobs:
  test:
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - run: npm install && npm test   # runs fork's package.json scripts, with your secrets in env

If you need pull_request_target (commenting on PRs, labeling, anything requiring write access), split it: do the privileged part (commenting, labeling) in the pull_request_target job without checking out fork code, and do untrusted code execution in a separate pull_request job with no secrets.

Environment secrets over repo secrets

Repo-level secrets are available to every workflow, every branch, no gate. Environment secrets add a checkpoint:

  1. Go to Settings → Environments, create production.
  2. Add required reviewers, or restrict to main branch only.
  3. Move production secrets there instead of repo-level.
jobs:
  deploy:
    environment: production
    steps:
      - run: ./deploy.sh
        env:
          API_KEY: ${{ secrets.PROD_API_KEY }}

Now a workflow run against a feature branch, or a PR, simply doesn’t get PROD_API_KEY — the environment gate blocks it before the job even starts, regardless of what the workflow YAML says.

OIDC instead of long-lived cloud keys

If you’re deploying to AWS, GCP, or Azure, stop storing static access keys as secrets entirely. GitHub can issue a short-lived OIDC token that your cloud provider trusts directly:

permissions:
  id-token: write
  contents: read

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

No AWS_SECRET_ACCESS_KEY sitting in GitHub at all. The IAM role trusts GitHub’s OIDC provider and only for a specific repo and branch, configured in the role’s trust policy. A leaked workflow log or a compromised action can’t extract a long-lived key, because there isn’t one — the token that gets issued is scoped and expires within the hour.

Quick checklist

The common thread: assume any code a fork PR can influence will eventually run, and make sure your secrets aren’t in scope when it does.