Overview

Secrets in GitHub Actions are encrypted values injected into workflows at runtime. Used correctly, they keep credentials out of source code and out of logs. Used incorrectly, they leak in plaintext to anyone who can read a workflow run. This page covers the three secret scopes, OIDC as the preferred alternative to static credentials, secret scanning, and rotation. Read github-actions for the workflow structure these secrets plug into.

Prefer OIDC over long-lived static credentials for cloud providers

Static API keys and service account tokens are long-lived and become liabilities the moment they are created. OIDC (OpenID Connect) lets a GitHub Actions runner prove its identity to a cloud provider and receive a short-lived token for the duration of the job.

permissions:
  id-token: write
  contents: read
 
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/MyRole
      aws-region: us-east-1

AWS, GCP, and Azure all support OIDC federation. The runner token expires when the job ends. No rotation. No storage. No leak surface beyond the job duration. Use OIDC for any workflow that touches a cloud provider API.

Use environment secrets for deployment-stage isolation

GitHub supports three secret scopes: repository, environment, and organization. Choose the narrowest scope that works.

  • Repository secrets are available to all workflows in the repo. Use for development tools and tokens that every workflow legitimately needs.
  • Environment secrets are scoped to a named environment (staging, production) and are only injected when a job explicitly targets that environment. Use for deployment credentials and tokens that should not run in CI on feature branches.
  • Organization secrets share a value across repos. Use for org-wide tokens (npm publish, shared registry) that you would otherwise duplicate.

Require environment protection rules on production environments: require a reviewer approval before a deployment runs. This adds a human gate in front of production credentials.

Never echo or print a secret value

GitHub masks known secret values in logs with ***. The mask is pattern-matched; it breaks when the secret is base64-encoded, URL-encoded, or split across a string operation. The only safe rule: never write code that prints a secret.

# Wrong: the mask may not catch this
- run: echo "Token is ${{ secrets.API_TOKEN }}"
 
# Wrong: interpolating a secret into a shell string enables injection
- run: curl -H "Authorization: ${{ secrets.API_TOKEN }}" $URL
 
# Correct: pass the secret as an environment variable
- run: curl -H "Authorization: $API_TOKEN" "$URL"
  env:
    API_TOKEN: ${{ secrets.API_TOKEN }}
    URL: ${{ github.event.inputs.url }}

Passing secrets as environment variables prevents shell injection and keeps the value out of the rendered YAML that appears in the workflow run log.

Enable secret scanning on every repository

GitHub Secret Scanning scans new commits and open pull requests for known credential patterns: AWS access keys, GitHub tokens, Stripe keys, and dozens more. Enable it in Settings - Security - Secret scanning. On public repos it is on by default; on private repos it requires GitHub Advanced Security or a GitHub Enterprise plan.

Enable “Push protection” to block a push that contains a detected secret before it lands in the repository. This stops the most common accidental commit. Legitimate secrets (test fixtures, rotation overlap) can be bypassed with an explicit justification that is logged.

For patterns not covered by GitHub’s detector, add custom patterns in Settings - Security - Secret scanning - Custom patterns. Test patterns against a sample value before enabling push protection.

Rotate secrets on a schedule and after any exposure

Set a rotation cadence proportional to the blast radius. Tokens that can deploy to production should rotate at least quarterly and immediately after a job that used them fails with an authentication error.

Create a reminder in the same system you use for other operational tasks. Store the rotation date in a comment on the secret (GitHub shows the last updated date). After rotation, verify that the new secret works in a non-production environment before removing the old one.

Treat any secret that has appeared in a log, a diff, or a PR description as compromised. Rotate it before investigating; the investigation can wait, the credential cannot.

Scope GITHUB_TOKEN permissions at the workflow level

The default GITHUB_TOKEN receives write access to repository contents, issues, and pull requests. Most workflows need only a subset. Declare the minimum at the workflow or job level.

permissions:
  contents: read

Restrict at the top of the workflow file to apply the minimum to all jobs, then grant additional permissions per-job where needed. See github-actions for per-workflow permission blocks.