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.

ScopeWhat can read itGate in front of itDocumented limit
RepositoryAny workflow in that repository, on any branch that runs a workflowWhoever can merge a workflow file100 secrets per repository
EnvironmentOnly jobs that name the environment in jobs.<id>.environmentEnvironment 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 pass100 secrets per environment
OrganizationEvery repository on the secret's access list, including repositories added to that list laterWhoever manages the access list1,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.sh

Two 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.sh

NPM_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.

LayerDocumented handlingLifetime
Job environment on the runnerEvery process the job starts can read it, including third party actions in the same jobThe length of the job
Runner virtual machineEach runner runs in its own virtual machine, created on demand and destroyed after each build, never reusedDestroyed with the build
Runner storage volumeEach runner has its own encrypted storage volume, created on demand and destroyed after each buildDestroyed with the machine
CacheEncrypted and stored in a location only your runner can reachUntil the cache entry is evicted
WarpBuild control planeWarpBuild does not access or store any build secretsNothing is held
BYOC instance in your cloud accountA 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 workloadsTerminated 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 labelOSvCPURAMStoragePrice per minute
warp-ubuntu-latest-x64-2xUbuntu 24.0428 GB150GB SSD$0.004
warp-ubuntu-latest-x64-4xUbuntu 24.04416 GB150GB SSD$0.008
warp-ubuntu-latest-x64-8xUbuntu 24.04832 GB150GB 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

InputValueSource
Repositories holding the same long lived cloud key40Your secret inventory
Rotation cadenceQuarterlyYour security policy
Engineer minutes to rotate one repository12Update the secret, re-run the verification workflow, confirm the deploy path
Verification runs per repository per rotation1One workflow run per repository
Verification job duration6 minutesYour workflow run history
Runner label and ratewarp-ubuntu-latest-x64-4x at $0.008 per minutePricing 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.

Line40 repositories with a stored key40 repositories on an OIDC exchange
Secrets stored for that provider40, or 1 organization secret on a 40 repository access list0
Rotation labor per year32 hours0 hours
Runner minutes per rotation2400
Runner cost per year for rotation runs$7.68$0.00
Exposure window for a leaked valueEvery repository sharing the key, until someone revokes itOne workflow run, until the credential expires on its own
One time setupNoneOne 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.