Handle Secrets in GitHub Actions Safely
03 Feb 2026secrets.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:
- Go to Settings → Environments, create
production. - Add required reviewers, or restrict to
mainbranch only. - 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
- Fork PRs never get secrets automatically — verify you haven’t overridden that with
pull_request_targetplus a fork checkout in the same job. - Production secrets live in a protected environment, not repo-level.
- Cloud credentials go through OIDC, not stored access keys, wherever the provider supports it.
- Audit third-party actions pinned by tag (
@v4) vs by commit SHA — a tag can be moved by the action’s maintainer (or an attacker who compromises their account); a SHA can’t.
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.