Back to blog

GitHub Actions Secrets: The Complete Guide (2026)

Learn how GitHub Actions secrets work in 2026: repository, environment, and organization secrets, real YAML examples, masking, limits, and best practices.

June 12, 2026by EnvManager Team
github-actionssecrets-managementci-cddevopssecurity

Every CI/CD pipeline eventually needs credentials: an API token to publish a package, a deploy key for production, a database URL for integration tests. GitHub Actions secrets are GitHub's built-in answer — encrypted values you store once and reference in workflows without ever committing them to your repository.

Used well, they keep credentials out of your Git history and out of your logs. Used carelessly, they leak through echo statements, sprawl across dozens of repositories with no audit trail, and sit unrotated for years.

This guide covers everything you need to know about GitHub Actions secrets in 2026: the three secret types, how to create and use them in YAML, how masking and fork protection work, the hard limits, and the gaps you'll hit once your team grows beyond a handful of repos. If you're new to the broader topic, our primer on what secrets management is is a good companion read.

What are GitHub Actions secrets?

GitHub Actions secrets are encrypted environment values stored in GitHub and exposed to workflow runs through the ${{ secrets.NAME }} context. GitHub encrypts secrets before they reach GitHub's servers (using libsodium sealed boxes), redacts them from workflow logs, and never shows the value again after you save it — you can only overwrite or delete a secret, not read it back.

Two things secrets are not:

  • They are not configuration variables. For non-sensitive values (a region name, a feature flag, a build mode), GitHub provides configuration variables, referenced with ${{ vars.NAME }}. Variables are stored unencrypted and visible in the UI. Reserve secrets for genuinely sensitive values.
  • They are not a runtime secrets manager for your application. Secrets exist only inside workflow runs. Your deployed app never talks to GitHub Actions secrets — it needs its own source of configuration at runtime (platform env vars, a secrets manager, or both).

Repository vs. environment vs. organization secrets

GitHub Actions supports secrets at three levels, and choosing the right one matters for both security and maintenance.

Repository secretsEnvironment secretsOrganization secrets
ScopeAll workflows in one repoOnly jobs targeting that environmentMultiple repos in an org
Best forRepo-specific tokens (e.g., a Codecov token)Deployment credentials per stage (staging vs. production)Shared credentials (registry tokens, shared API keys)
Protection rulesNoneYes — required reviewers, wait timers, branch restrictionsAccess policy: all repos, private repos, or selected repos
Who can createRepo admins / write accessRepo admins (org repos)Organization owners
Storage limit100 per repo100 per environment1,000 per org
PrecedenceOverrides orgOverrides repo and orgLowest

Two details worth memorizing:

  1. Precedence flows downward. If a secret named API_KEY exists at all three levels, the environment secret wins, then the repository secret, then the organization secret.
  2. Environment secrets only exist inside jobs that declare the environment. A job without environment: production simply cannot see production's secrets — and if the environment has required reviewers, the job pauses until someone approves it. This is the closest thing GitHub offers to access control around your most dangerous credentials, so use it for anything that touches production.

How to add secrets

In the GitHub UI

  • Repository secret: Settings → Secrets and variables → Actions → Secrets tab → New repository secret
  • Environment secret: Settings → Environments → select or create an environment → Add secret
  • Organization secret: Organization Settings → Secrets and variables → Actions → New organization secret, then choose a visibility policy (all repositories, private repositories, or selected repositories)

With the GitHub CLI

# Repository secret (prompts for the value, or pipe it in)
gh secret set PROD_DATABASE_URL

# Environment secret
gh secret set DEPLOY_KEY --env production

# Organization secret, limited to specific repos
gh secret set NPM_TOKEN --org my-org --repos "api,web,worker"

Naming rules: alphanumeric characters and underscores only, no leading number, no GITHUB_ prefix, and names are case-insensitive when referenced. Each secret is limited to 48 KB.

Using secrets in workflows (YAML examples)

Secrets are available through the secrets context. The two standard patterns are passing them as environment variables (env:) or as action inputs (with:).

Passing a secret to a step

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to server
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: ./scripts/deploy.sh

Inside deploy.sh, read the value from the environment ("$DEPLOY_TOKEN", with quotes). Avoid interpolating ${{ secrets.* }} directly into run: commands — GitHub's own guidance is to avoid passing secrets between processes on the command line, where they can land in process listings and shell history.

Passing a secret to an action input

      - name: Publish to npm
        uses: JS-DevTools/npm-publish@v3
        with:
          token: ${{ secrets.NPM_TOKEN }}

Using environment secrets

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment: production   # unlocks production's secrets + protection rules
    steps:
      - uses: actions/checkout@v4
      - name: Deploy
        env:
          DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}   # environment value wins over repo/org
        run: ./scripts/deploy.sh production

Secrets and reusable workflows

Secrets are not automatically passed to reusable workflows. Either forward them explicitly or use secrets: inherit:

jobs:
  call-deploy:
    uses: my-org/workflows/.github/workflows/deploy.yml@main
    secrets: inherit   # or list them explicitly under secrets:

The if: conditional gotcha

You can't reference secrets directly in if: expressions. The standard workaround is routing through an environment variable:

jobs:
  notify:
    runs-on: ubuntu-latest
    env:
      HAS_WEBHOOK: ${{ secrets.SLACK_WEBHOOK != '' }}
    steps:
      - name: Send Slack notification
        if: env.HAS_WEBHOOK == 'true'
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        run: ./scripts/notify.sh

A related quirk: an unset secret resolves to an empty string rather than failing the run, which is why the empty-string check above works — and why typos in secret names fail silently. Double-check spelling when a step mysteriously receives nothing.

GITHUB_TOKEN and OIDC: the secrets you don't have to store

Before stuffing more credentials into the secrets store, check whether you need them at all.

GITHUB_TOKEN is created automatically for every workflow run and is always available in the secrets context. It authenticates against the GitHub API for the current repository — pushing tags, commenting on PRs, publishing to GitHub Packages. Scope it down with the permissions: key rather than minting a personal access token for jobs that only touch GitHub itself.

OIDC (OpenID Connect) eliminates long-lived cloud credentials entirely. Instead of storing an AWS_SECRET_ACCESS_KEY that's valid 24/7, your workflow requests a short-lived token from GitHub's OIDC provider; your cloud account is configured to trust tokens from specific repos and branches and exchanges them for temporary credentials that expire when the job ends. AWS, Azure, GCP, and HashiCorp Vault all support this, and the workflow side is small:

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/gha-deploy
          aws-region: eu-west-1

The rule of thumb in 2026: use OIDC for cloud provider access, GITHUB_TOKEN for GitHub API access, and stored secrets only for everything that supports neither — third-party API keys, database URLs, webhook URLs, signing keys.

Limits, gotchas, and security behavior

Things that bite teams in practice (as of June 2026 — see GitHub's secrets reference for the current numbers):

  • Size: secrets are limited to 48 KB each. For anything larger, GitHub's documented workaround is encrypting the file with GPG, committing the ciphertext, and storing only the passphrase as a secret — but note that GitHub does not redact the decrypted content from logs.
  • Count: 100 repository secrets, 100 secrets per environment, 1,000 organization secrets. A workflow run can use all 100 repo and all 100 environment secrets, but only the first 100 organization secrets (alphabetically) available to that repo.
  • Masking is best-effort. GitHub redacts known secret values from logs, but transformations defeat it: base64-encode a secret, split it, or embed it in JSON and the masked value no longer matches. Use ::add-mask:: for derived sensitive values, and never deliberately print secrets.
  • Forks don't get secrets. With the exception of GITHUB_TOKEN, secrets are not passed to workflows triggered from forked repositories — that's what stops a drive-by pull request from exfiltrating your deploy keys. Be extremely careful with pull_request_target, which does run with secrets access.
  • Dependabot-triggered workflows don't receive your Actions secrets either; Dependabot has its own separate secrets store.
  • Write-only by design. Nobody can read a secret back out through the UI or API. Good for security, bad for recovery: if the only copy of a credential lives in GitHub, losing the original means rotating it.

Best practices checklist

  1. Never commit secrets as a fallback. The secrets store complements — never excuses — keeping credentials out of Git. Our guide to Git and environment variables covers keeping .env files out of your history.
  2. Use environment secrets + protection rules for production. Required reviewers on a production environment are cheap insurance.
  3. Prefer OIDC over stored cloud keys. A credential that doesn't exist can't leak.
  4. Scope GITHUB_TOKEN with permissions: at the workflow or job level; default to contents: read.
  5. Pin third-party actions to a commit SHA. Any action you run can read the secrets exposed to its job. A compromised tag is a secrets-exfiltration vector.
  6. Rotate on a schedule and after every departure. GitHub won't do this for you (more below).
  7. Audit what's actually used. Stale secrets in long-forgotten repos are unowned risk.

For the broader discipline beyond CI, see our secrets management best practices guide.

Where GitHub Actions secrets stop — and where a secrets manager helps

GitHub Actions secrets are excellent at one job: getting encrypted values into workflow runs. But they were never designed to be your team's system of record for secrets, and the gaps show as soon as you operate more than a few repositories:

  • No rotation. There's no built-in expiry, rotation, or even a "last updated" nudge. Secrets quietly age until something forces your hand.
  • No cross-repo or cross-platform sharing. Organization secrets help within one GitHub org, but the same STRIPE_SECRET_KEY probably also lives in Vercel, Railway, your local .env files, and a staging server — each copied by hand, each drifting independently.
  • No central audit trail. The audit log records that a secret was updated, but there's no unified view across repos and platforms of who changed which value, when, and what it was before. Values are write-only, so you can't even diff.
  • Write-only storage means no recovery. When GitHub is the only place a value exists, "what is our production DSN again?" becomes a rotation exercise.
  • Coarse access control. Anyone with repo write access can overwrite repository secrets; there's no viewer/editor distinction or per-secret permissioning.

This is the gap a dedicated secrets manager fills, with GitHub Actions as one sync target rather than the source of truth. EnvManager takes exactly that approach: your variables live in one place, encrypted client-side with AES-256-GCM before they ever leave your browser, and the GitHub Actions integration syncs them out to repository, environment, or organization secrets — alongside Vercel, Railway, Render, Dokploy, Coolify, AWS Secrets Manager, and GCP Secret Manager, so every platform receives the same value from the same source. Update a key once, sync everywhere; rotation stops being an archaeology project.

Around that core you get what GitHub doesn't provide: role-based access control (admin/editor/viewer with environment-specific access), immutable audit logs of every read and write, .env import/export, and a CLI with real-time file-watching sync for local development. Pricing is deliberately simple: a 14-day free trial with no credit card, then a flat $9/month ($7.50/month billed annually) for your whole team — unlimited members, no per-seat math; Enterprise adds SAML SSO and self-hosting (see pricing).

If your secrets currently live in five dashboards and a Slack DM, start a free trial and consolidate them — or compare options first in our roundup of the best secrets management tools.

FAQ

What is the difference between GitHub secrets and environment variables?

Secrets (${{ secrets.NAME }}) are encrypted at rest, hidden after creation, and redacted from logs — use them for credentials. Configuration variables (${{ vars.NAME }}) are stored in plain text and visible in the UI — use them for non-sensitive settings like regions or build flags. Both can be defined at repository, environment, and organization level.

How do I use a secret in a GitHub Actions workflow?

Reference it from the secrets context, typically as an environment variable:

steps:
  - name: Call API
    env:
      API_KEY: ${{ secrets.API_KEY }}
    run: ./scripts/call-api.sh

Then read $API_KEY from the environment in your script rather than interpolating the secret into the command line.

Can I read a GitHub Actions secret after creating it?

No. Secrets are write-only: once saved, the value can be updated or deleted but never viewed again through the UI or API. Keep the source of truth somewhere recoverable — a secrets manager — and treat GitHub as a sync target.

Are GitHub Actions secrets available to forked repositories?

No. Apart from the automatic GITHUB_TOKEN, secrets are not passed to workflows triggered from forks, which prevents malicious pull requests from stealing credentials. The notable exception is the pull_request_target event, which runs in the base repository's context with secrets access — treat it with care.

How big can a GitHub Actions secret be?

Each secret is limited to 48 KB. You can store up to 100 repository secrets, 100 secrets per environment, and 1,000 organization secrets (as of June 2026). For larger payloads, GitHub recommends encrypting the file with GPG and storing only the passphrase as a secret.

Ready to manage your environment variables securely?

EnvManager helps teams share secrets safely, sync configurations across platforms, and maintain audit trails.

Start your free trial

Get DevOps tips in your inbox

Weekly security tips, environment management best practices, and product updates.

No spam. Unsubscribe anytime.