Skip to content
C Codeloom
DevOps

GitHub Actions Secrets Management: A Practical Guide

Learn how to store, scope, and rotate secrets in GitHub Actions. Cover repository, environment, and organization secrets, plus OIDC for cloud access without static keys.

·5 min read · By Codeloom
Beginner 11 min read

What you'll learn

  • The three scopes of secrets in GitHub Actions
  • How to reference secrets safely from workflows
  • Why environment secrets and required reviewers matter
  • How OIDC removes the need for long-lived cloud keys
  • Common mistakes that leak secrets to logs

Prerequisites

  • Comfortable with the Linux command line

CI pipelines need secrets. Deploy keys, registry credentials, API tokens, database URLs — none of those belong in your repository. GitHub Actions gives you a built-in secret store with three scopes, an environments feature for approvals, and OIDC for token-based cloud access. Used together they cover almost every secret-handling case without ever shipping a credential to disk.

Where secrets live

There are three places to put a secret, in order of broadest to narrowest reach:

  • Organization secrets: shared by many repositories under one org.
  • Repository secrets: visible to every workflow in one repo.
  • Environment secrets: visible only when a workflow runs in a specific environment, like production.

Pick the narrowest scope that still works. A token used to publish to npm from one repo belongs in that repo, not the organization. A production database password belongs in the production environment so only deploy jobs see it.

Reading a secret in a workflow

Secrets are exposed to workflows through the secrets context. They are masked in logs automatically — but only if you reference them through the context.

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

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Publish to npm
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
          npm publish

Notice that the secret is passed via an environment variable, not interpolated directly into a shell command. That is intentional. If you write run: echo ${{ secrets.NPM_TOKEN }}, GitHub substitutes the value before the shell ever sees it, which means any character in the secret can break or hijack the command. Going through env: is safer and clearer.

Environments and required reviewers

An environment is a named deployment target with its own secrets and protection rules. The most useful rule is required reviewers — a job targeting that environment pauses until a human approves it.

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - run: ./deploy.sh

With production configured to require approval and to hold the real deploy key, even a malicious push to main cannot deploy without a human clicking approve. That single setting prevents a surprising number of incidents.

OIDC: stop storing cloud keys

Long-lived AWS access keys in repository secrets are a leak waiting to happen. GitHub Actions can instead exchange a short-lived OIDC token with your cloud provider for temporary credentials. No static key ever exists.

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/github-deploy
          aws-region: us-east-1
      - run: aws s3 sync ./dist s3://my-bucket

On the AWS side you create an IAM role with a trust policy that only accepts tokens from your repo and branch. The result is least privilege and zero static credentials in the repo. Google Cloud and Azure offer the equivalent — Workload Identity Federation and federated credentials respectively.

Avoid leaking secrets to logs

GitHub masks known secret values in logs, but it cannot mask transformed versions. Common foot-guns include:

  • Base64-encoding a secret and then printing it.
  • Concatenating a secret with other text and echoing the result.
  • Setting set -x in a script that uses the secret on a command line.
  • Uploading a build artifact that includes a config file with the secret embedded.

A simple discipline helps. Treat secrets like radioactive material — pass them only into the one tool that needs them, never into a log statement, never into a build artifact.

Forks and pull requests

Secrets are not exposed to workflows triggered by pull requests from forks. That is a deliberate, important safety net — otherwise a stranger could open a PR that prints secrets.AWS_SECRET_ACCESS_KEY. If you need a workflow that requires secrets on PRs, use pull_request_target carefully, with a separate job that only runs after a maintainer label, or move to a workflow that runs after merge.

Rotation and auditing

Rotate secrets on a schedule, not after an incident. A token you have not rotated in two years is more likely to have leaked than a token you rotated last month. GitHub’s audit log records every time a secret is created, updated, or deleted; review it occasionally for entries you do not recognize.

gh secret list --repo my-org/my-repo
gh secret set NPM_TOKEN --repo my-org/my-repo

The gh CLI makes rotation scriptable, which is the only way rotation actually happens.

A short checklist

Use the narrowest scope. Pass secrets through env:, never inline. Require reviewers on production environments. Prefer OIDC over static cloud keys. Rotate on a calendar. Watch the audit log. Do those six things and most of the common GitHub Actions secret accidents simply cannot happen to you.