Managing Secrets in GitHub Actions Workflows
Store every GitHub Actions secret at the narrowest scope that works, hand it to one step rather than one job, and delete the cloud keys OIDC replaces.
Managing secrets in GitHub Actions comes down to three decisions: which scope holds each value, which step can read it, and which values can be deleted outright because a short lived credential replaces them. GitHub holds the encrypted value and injects the ones a workflow references into the job environment on the runner, and the WarpBuild runner security documentation states that WarpBuild does not access or store any build secrets.
This guide covers the scoping ladder with its documented limits, the OIDC exchange that removes stored cloud keys, what the runner side does with the value and what is destroyed with the machine, and a time model for the rotation work a stored key creates every quarter.
Diagnosis
Four shapes produce almost every secrets ticket, and three of them are scope problems rather than tooling problems.
One key in many repositories. A cloud access key created during a migration ends up in 40 repositories, or in one organization secret shared with 40 repositories. Rotating it means touching every one of them in a single window, so it never gets rotated.
A job level env block. A secret declared under jobs.<id>.env sits in the environment of every step in that job, including third party actions, so a compromised action reaches the same value that the deploy step uses.
An organization secret with an open access list. Organization secrets are readable by every repository on the list. When the list is set to all repositories, a repository created next month inherits production credentials on its first workflow run.
A fork pull request that fails on a missing value. With the exception of GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repository (GitHub documentation on using secrets, checked on 2026-08-13). The job that needs a real credential belongs on a pull_request_target or workflow_run trigger with its own review, and the fork path runs the checks that need nothing.
The scoping ladder
Three scopes exist, and each exposes the value to a different set of readers. The limits below come from the GitHub secrets reference, checked on 2026-08-13, and the environment behavior from managing environments for deployment, checked on the same date.
| Scope | What can read it | Gate in front of it | Documented limit |
|---|---|---|---|
| Repository | Any workflow in that repository, on any branch that runs a workflow | Whoever can merge a workflow file | 100 secrets per repository |
| Environment | Only jobs that name the environment in jobs.<id>.environment | Environment protection rules: required reviewers, up to 6 people or teams; a wait timer; deployment branch and tag policies. Jobs reach the secret only after the configured rules pass | 100 secrets per environment |
| Organization | Every repository on the secret's access list, including repositories added to that list later | Whoever manages the access list | 1,000 secrets per organization. Where a repository has access to more than 100, a workflow uses the first 100 sorted alphabetically by name |
Two more limits shape the design. Individual secrets are limited to 48 KB, so a large service account JSON file or a signing certificate gets base64 encoded and often split. The alphabetical cutoff at 100 organization secrets is the quiet one: a workflow that references the 101st secret by name receives an empty string rather than an error, and the failure surfaces later as a confusing authentication rejection.
Environment scope is the only rung with an approval gate, which is why deployment credentials belong there and build credentials do not.
Take the inventory first
Before moving anything, list what exists. The GitHub CLI reads all three scopes, and the environment list needs one API call.
gh secret list --repo example-org/api
gh secret list --org example-org
gh api /repos/example-org/api/environments --jq '.environments[].name'Run that across the repositories that deploy, then sort the output into three piles: values a cloud identity exchange can replace, values that must stay stored, and values nothing references any more.
Fix
1. Delete what nothing references
Search the workflow files for each secret name before you plan any migration. Secrets left behind by a retired pipeline are the cheapest ones to remove, and every deletion shortens the next rotation window.
2. Move each remaining secret down to the narrowest scope
A build credential that only the test job needs becomes a repository secret. A production database password becomes an environment secret behind required reviewers. An organization secret earns its scope only when three or more repositories genuinely share the same value and the access list names them explicitly rather than covering all repositories.
3. Replace cloud keys with an OIDC exchange
This step deletes secrets rather than moving them. The job asks GitHub for a signed identity token describing the repository, branch, and workflow, and the cloud provider exchanges it for temporary credentials after checking the claims against a trust policy that was written once (GitHub: security hardening with OpenID Connect, checked on 2026-08-13).
name: deploy
on:
push:
branches: [main]
permissions: {}
jobs:
deploy:
runs-on: warp-ubuntu-latest-x64-4x
environment: production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v5
- name: Exchange the GitHub identity token for AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/github-actions-deploy
role-session-name: deploy-${{ github.run_id }}
aws-region: us-east-1
- name: Apply the release
run: ./scripts/deploy.shTwo details decide whether this holds. The permissions: {} line at workflow level sets every scope to none, so the job block below it grants exactly contents: read and id-token: write and nothing else. The trust policy on the AWS side matches the sub claim with an exact string comparison against one repository and one ref, so editing the workflow file cannot widen it. OIDC token exchange, defined covers the claim set, and how to authenticate to AWS from GitHub Actions with OIDC covers the trust policy shape and the errors it produces.
4. Hand what remains to one step
A secret declared at step level exists in that step alone. Pin every third party action to a full length commit SHA so an upstream tag move cannot introduce code that reads the environment around it.
5. Give the survivors a rotation date
Whatever stays stored needs an owner and a cadence. The model further down prices what that cadence costs in engineering hours, which is the argument for shrinking the list in step 3.
Configuration
A workflow that uses both patterns
The build job carries a registry token scoped to the one step that publishes. The deploy job stores nothing and sits behind an environment.
name: release
on:
push:
tags: ["v*"]
permissions: {}
jobs:
build:
runs-on: warp-ubuntu-latest-x64-8x
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- name: Run the test suite
run: make test
- name: Publish the package
env:
NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
run: npm publish --provenance
deploy:
needs: build
runs-on: warp-ubuntu-latest-x64-4x
environment: production
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v5
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/github-actions-deploy
role-session-name: release-${{ github.run_id }}
aws-region: us-east-1
- run: ./scripts/deploy.shNPM_PUBLISH_TOKEN is readable by the publish step and by no other step in the workflow. The deploy job reaches its credentials only after the production environment's reviewers approve, because environment secrets and the environment level identity grant both wait on the protection rules.
What the runner side does with the value
Once GitHub delivers a secret, the handling is documented per layer. The rows below come from the runner security documentation and the BYOC security hardening documentation.
| Layer | Documented handling | Lifetime |
|---|---|---|
| Job environment on the runner | Every process the job starts can read it, including third party actions in the same job | The length of the job |
| Runner virtual machine | Each runner runs in its own virtual machine, created on demand and destroyed after each build, never reused | Destroyed with the build |
| Runner storage volume | Each runner has its own encrypted storage volume, created on demand and destroyed after each build | Destroyed with the machine |
| Cache | Encrypted and stored in a location only your runner can reach | Until the cache entry is evicted |
| WarpBuild control plane | WarpBuild does not access or store any build secrets | Nothing is held |
| BYOC instance in your cloud account | A freshly provisioned instance terminated when the job finishes, carrying exactly the permissions you attach through your own security groups and IAM instance profiles. WarpBuild does not inject additional permissions into runner workloads | Terminated with the job |
Every label in the catalog gets the handling in that table. A reviewer who wants the same statements in questionnaire order can work from the security review checklist for GitHub Actions runners, and the boundary question on its own is answered in are build secrets visible to the runner platform.
Runner labels used above
Per-minute rates from the pricing page:
| Runner label | OS | vCPU | RAM | Storage | Price per minute |
|---|---|---|---|---|---|
| warp-ubuntu-latest-x64-2x | Ubuntu 24.04 | 2 | 8 GB | 150GB SSD | $0.004 |
| warp-ubuntu-latest-x64-4x | Ubuntu 24.04 | 4 | 16 GB | 150GB SSD | $0.008 |
| warp-ubuntu-latest-x64-8x | Ubuntu 24.04 | 8 | 32 GB | 150GB SSD | $0.016 |
Cost or Time Model
The number that matters in secrets management is engineering hours per rotation. The runner minutes a rotation consumes stay small at any fleet size, while the human time scales with the repository count.
Assumptions
| Input | Value | Source |
|---|---|---|
| Repositories holding the same long lived cloud key | 40 | Your secret inventory |
| Rotation cadence | Quarterly | Your security policy |
| Engineer minutes to rotate one repository | 12 | Update the secret, re-run the verification workflow, confirm the deploy path |
| Verification runs per repository per rotation | 1 | One workflow run per repository |
| Verification job duration | 6 minutes | Your workflow run history |
| Runner label and rate | warp-ubuntu-latest-x64-4x at $0.008 per minute | Pricing page |
The arithmetic
Labor per rotation: 40 repositories times 12 minutes equals 480 minutes, which is 8 hours. Four rotations a year is 32 hours, and an unplanned rotation after a leaked value compresses the same 8 hours into one afternoon while someone else audits what the key touched.
Runner minutes per rotation: 40 runs times 6 minutes equals 240 minutes. At $0.008 per minute that is $1.92 per rotation and $7.68 a year.
| Line | 40 repositories with a stored key | 40 repositories on an OIDC exchange |
|---|---|---|
| Secrets stored for that provider | 40, or 1 organization secret on a 40 repository access list | 0 |
| Rotation labor per year | 32 hours | 0 hours |
| Runner minutes per rotation | 240 | 0 |
| Runner cost per year for rotation runs | $7.68 | $0.00 |
| Exposure window for a leaked value | Every repository sharing the key, until someone revokes it | One workflow run, until the credential expires on its own |
| One time setup | None | One identity provider per cloud account, one role and trust policy per repository and target |
The dollar line stays small at any fleet size. The 32 hours is what pays for the migration in step 3, and it repeats every year the stored key survives.
Where WarpBuild fits
Compliance evidence for the runner side is SOC 2 Type 2 with three Trust Services Criteria: Security, Availability, and Confidentiality. Request the report and the supporting security documentation at trust.warpbuild.com.
Every price and limit on this page carries a source link and a checked-on date. GitHub reorganizes its Actions documentation paths regularly, so re-open the two GitHub references before quoting the limits in an internal policy, and re-run the arithmetic above against your own repository count and job duration.
FAQ
Should a secret live at repository, environment, or organization scope?
Use the narrowest scope that still lets the workflow run. A repository secret is readable by any workflow in that repository. An environment secret is readable only by jobs that name the environment, and only after the environment protection rules pass, which is the one scope with a gate in front of it. An organization secret is readable by every repository on its access list, including repositories added to that list later.
Which secrets can an OIDC exchange remove entirely?
The long lived cloud provider keys. AWS, GCP, and Azure all accept a signed identity token from GitHub and return temporary credentials, so the access key ID and secret access key stop being stored anywhere. Registry passwords, third party API tokens, and signing keys stay in the secret store unless the provider supports federation, and those are the values worth scoping to one step.
Can the runner platform read the secret while the job runs?
The WarpBuild runner security documentation states that WarpBuild does not access or store any build secrets, and that secrets stay in your source code repository and are only accessible to your runner environment. That environment is a virtual machine created on demand for one job and destroyed after the build, along with its encrypted storage volume. Inside the job, every process the job starts can read the value, which is why step scoping matters.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.