← All articles

Secrets in Custom GitHub Actions: How Reusable Workflows Silently Share Your Credentials

July 15, 2026

The Trust Problem Hiding in Your Workflow Files

When developers think about credential leaks in CI/CD, they usually picture a hardcoded token in a .yml file or a secret printed to a log. But there is a subtler risk that most engineering teams never audit: the secrets you pass — intentionally or accidentally — into custom GitHub Actions and reusable workflows authored by third parties, or even by other teams inside your own organization.

Every time you write uses: some-org/some-action@v2 in a workflow and then reference ${{ secrets.PROD_DB_PASSWORD }} in the same job, you are executing arbitrary code with access to that secret's runtime environment. If the action is compromised, outdated, or simply poorly written, your credentials go with it.

How GitHub Actions Actually Handles Secrets

It helps to understand what GitHub does — and does not — protect:

  • Secrets are masked in logs. If the literal value of a secret appears in a log line, GitHub replaces it with ***. This is a display filter, not a sandbox.
  • Secrets are injected as environment variables. Inside a running Action, secrets.MY_KEY becomes an environment variable accessible to any process spawned by that Action's code.
  • Third-party Action code runs with those variables in scope. The JavaScript or Docker container backing the Action can read, exfiltrate, or log those values — and GitHub has no mechanism to prevent it.
  • Reusable workflows can inherit secrets explicitly. The secrets: inherit keyword automatically passes all calling-workflow secrets into a reusable workflow. This is a one-line credential broadcast.

The Three Most Common Ways This Goes Wrong

1. Pinning to a Tag Instead of a Commit SHA

Most teams write uses: actions/checkout@v4 or uses: some-vendor/deploy-action@main. Tags and branch names are mutable — a maintainer (or an attacker who has compromised the maintainer's account) can push new code under the same reference. Your workflow pulls the updated code on the next run without any change on your side.

The fix is to pin every external Action to an immutable commit SHA:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Tools like StepSecurity's Action Advisor and Dependabot can automate SHA pinning and keep those pins current.

2. Using secrets: inherit in Reusable Workflows

Reusable workflows (called with uses: ./.github/workflows/deploy.yml or a remote path) can receive secrets in two ways: explicitly named, or via secrets: inherit. The inherited form is tempting because it requires no maintenance — but it means every secret in the calling workflow's context flows into the called workflow, including secrets the called workflow has no business seeing.

Prefer explicit secret passing instead:

jobs:
  deploy:
    uses: ./.github/workflows/deploy.yml
    secrets:
      DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
      # Only pass what is actually needed

This limits blast radius if the reusable workflow is ever modified to behave maliciously or is forked by another team with different trust assumptions.

3. Running Untrusted Actions in the Same Job as Privileged Steps

Environment variables are process-scoped within a job, but secrets injected into a job's environment are visible to all steps in that job unless you take explicit measures. If step 1 sets AWS_SECRET_ACCESS_KEY and step 3 runs a third-party linter action, that action's process inherits the variable.

Isolate privileged operations into their own jobs with needs: dependencies, and only inject sensitive secrets into the jobs that require them:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: some-third-party/linter@abc1234  # No secrets in this job

  deploy:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        env:
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: ./deploy.sh

Auditing Your Existing Workflows

Run through this checklist for every .github/workflows/*.yml file in your repositories:

  1. List every uses: reference. Identify which are pinned to SHAs, which to tags, and which to branches. Tags and branches require immediate attention.
  2. Search for secrets: inherit. Every instance is a candidate for refactoring to explicit secret passing.
  3. Map which secrets appear in which jobs. Any job that mixes third-party actions with sensitive secrets is a risk.
  4. Check the pull_request_target trigger. Workflows using this trigger run with write permissions and secret access even when triggered by a fork — a well-documented attack vector. Use pull_request instead unless you have a specific need and understand the implications.
  5. Review Action permissions. Add permissions: blocks to every workflow and job, granting only the minimum GitHub token scopes required (e.g., contents: read). This limits what a compromised Action can do with GITHUB_TOKEN.

What About Organization-Authored Actions?

Internal custom Actions written by your own teams carry different but equally real risks. Common problems include:

  • Actions that console.log() or core.info() their entire input object, inadvertently printing secrets to logs (GitHub's masking only works if it knows the exact secret value).
  • Actions that write inputs to disk in /tmp or as job artifacts, which persist beyond the workflow run.
  • Actions that shell out to tools (curl, aws-cli, gcloud) with credentials interpolated directly into command strings, making them visible in process lists.

Treat your internal Actions like any other code that handles credentials: code review, dependency scanning, and periodic audits of what they log and write.

SOC 2 and HIPAA Relevance

From a compliance perspective, uncontrolled secret flow through CI/CD pipelines is a direct gap against SOC 2 CC6.1 (logical access controls) and CC6.6 (restricting access to authorized users and processes). HIPAA Security Rule §164.312(a)(1) similarly requires access controls that limit credential exposure to the minimum necessary. Auditors increasingly ask for evidence of pipeline secret governance — not just that secrets exist in a vault, but that they cannot leak laterally through automated systems.

A Practical Remediation Priority Order

  1. Pin all external Actions to commit SHAs this week.
  2. Replace every secrets: inherit with an explicit secret list.
  3. Add minimum-permission permissions: blocks to all workflows.
  4. Separate privileged jobs from jobs that run third-party actions.
  5. Scan your repositories for secrets that may already be embedded in workflow files, action source code, or artifacts produced by past runs.

For that last step, run a free GhostCred scan to surface any credentials already sitting in your repos or .env files before an attacker finds them first — results typically come back in under 60 seconds, mapped to the relevant SOC 2 and HIPAA controls.

Summary

Custom GitHub Actions and reusable workflows are powerful — and precisely because they execute arbitrary code in an environment where secrets are present, they deserve the same scrutiny as any other code that touches credentials. Mutable tags, broad secret inheritance, and mixed-trust jobs are the three patterns most likely to cause a real incident. Fixing them requires no new tooling, just disciplined workflow hygiene applied consistently across every repository your team owns.

See what's exposed in your own code.

Run a free scan