跳到正文
DEV Community· Abhinav Yadav·· 2 小時前AI 評分53

為 GitHub Actions 的 actions/checkout 設定 persist-credentials: false

Always Set persist-credentials: false in actions/checkout

AI 導讀

文章建議在所有 GitHub Actions 的 `actions/checkout` 步驟設定 `persist-credentials: false`,避免 GitHub token 留在同一 job 的後續步驟可讀取的檔案內。

正文

Cover image for Always Set persist-credentials: false in actions/checkout

Abhinav Yadav

There's a default in actions/checkout\ that most teams never change — and that quietly leaves a GitHub token sitting in a readable file for every subsequent step in the job.

The fix is one line. The reasoning is worth understanding.

The Setting

- uses: actions/checkout@v4
  with:
    persist-credentials: false  # default is true
~~~

By default (\`persist-credentials: true\`), the checkout action writes a git credential config file containing your GitHub token to \`$RUNNER_TEMP\`. That file persists for the lifetime of the job.

## What the File Looks Like

The checkout action creates a file at \`$RUNNER_TEMP/git-credentials-<UUID>.config\`:

~~~ini
[http "https://github.com/"]
    extraheader = AUTHORIZATION: basic <Base64-encoded-string>
~~~

The Base64 string decodes to \`x-access-token:<GITHUB_TOKEN>\`. It's not encrypted. It's not write-protected. It sits in \`$RUNNER_TEMP\` for every step after checkout to read.

## How Easy It Is to Exploit

Any step in the same job — including actions from third-party repos — can extract the token with a shell one-liner:

~~~bash
GH_TOKEN=$(
  cat "$RUNNER_TEMP"/git-credentials-*.config \
    | awk 'NR==2 {print $5}' \
    | base64 --decode \
    | cut -d: -f2
)
echo "token: ${GH_TOKEN:0:10}..."
~~~

This doesn't require \`${{ github.token }}\` or \`${{ secrets.GITHUB_TOKEN }}\` to appear anywhere in the workflow. The token is already on disk.

> [!WARNING]
> If any action you use is compromised via a supply chain attack, or if your workflow is vulnerable to script injection through untrusted input, that action can silently exfiltrate your GitHub token via this file.

Setting \`persist-credentials: false\` makes the checkout action **delete the credentials file** after checkout completes. The file is gone before the next step runs.

## The Practical Risk

The most realistic threat vectors:

**1. Compromised third-party action:** You pin \`some-org/some-action@v2\`. The maintainer's account is compromised, a new patch release is pushed, and the action now exfiltrates credentials. If you reference by tag (not commit SHA), you get the new version automatically.

**2. Script injection via PR titles or branch names:** Workflows that echo \`${{ github.event.pull_request.title }}\` into a \`run:\` block are vulnerable. An attacker opens a PR with a crafted title that executes a shell command reading the credentials file.

The \`persist-credentials: false\` flag eliminates the file-based exfiltration path. It doesn't prevent all attacks, but it removes the easiest one.

## What If I Need Git Auth in Later Steps?

Setting \`persist-credentials: false\` means subsequent \`git pull\`, \`git push\`, or other authenticated operations will fail without credentials being re-established.

The clean solution is \`gh auth setup-git\`, which configures Git to use the GitHub CLI credential helper:

~~~yaml
- uses: actions/checkout@v4
  with:
    persist-credentials: false

- name: Set up git credentials via GH CLI
  run: gh auth setup-git
  env:
    GH_HOST: github.com

- name: Pull latest
  run: git pull origin main
  env:
    GH_TOKEN: ${{ github.token }}
~~~

This configures \`~/.gitconfig\` with:

~~~ini
[credential "https://github.com"]
    helper =
    helper = !/usr/bin/gh auth git-credential
~~~

The credential helper calls out to the GitHub CLI at auth time, which reads \`GH_TOKEN\` from the environment — **only when Git actually needs to authenticate**, and without writing the token to disk.

The token is scoped to the steps where \`GH_TOKEN\` is explicitly passed in \`env:\`. It's not sitting in a file readable by any arbitrary code in the job.

## Pinning Actions by Commit SHA

While hardening your checkout step, pin by full commit SHA rather than a mutable tag:

~~~yaml
# Mutable — tag can be force-pushed to point at new, malicious code
- uses: actions/checkout@v4

# Immutable — this exact commit is guaranteed
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
~~~

Combined with \`persist-credentials: false\`, this closes both the supply chain mutation vector and the credential-on-disk vector in one checkout block.

## Automated Detection

Human review will miss this. Use static analysis in CI:

**[zizmor](https://docs.zizmor.sh)** — Rust-based, runs as a GitHub Action:

~~~yaml
- uses: woodruffw/zizmor-action@v1
~~~

Output when \`persist-credentials\` is not set:

~~~plaintext
warning[artipacked]: credential persistence through GitHub Actions artifacts
 --> .github/workflows/build.yml:7:9
  |
7 |       - uses: actions/checkout@v4
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^ does not set persist-credentials: false
  |
  = note: this finding has an auto-fix
~~~

**[ghasec](https://github.com/koki-develop/ghasec)** and **[ghalint](https://github.com/suzuki-shunsuke/ghalint)** also catch this and surface it with rule references.

Running any of these in CI ensures the check doesn't rely on engineers remembering to apply it manually.

> [!TIP]
> There is an open PR in the \`actions/checkout\` repo to flip the default to \`false\`, but it has been stalled for years. Until the default changes, enforce it via linting.

## Checklist

- [ ] \`persist-credentials: false\` on all \`actions/checkout\` steps
- [ ] Actions pinned to full commit SHAs, not mutable tags
- [ ] \`gh auth setup-git\` pattern for workflows that need post-checkout git auth
- [ ] zizmor or ghasec running in CI to catch new workflows
- [ ] Minimal \`permissions:\` scope on each job (\`contents: read\` unless write is required)

The same lesson applies to AI tooling: a token that sits where other code can read it will eventually be read. See [stop giving AI agents your production keys](https://yadui.dev/blog/stop-giving-ai-agents-production-keys) and the [LiteLLM 1.107.0 incident response guide](https://yadui.dev/blog/litellm-1-107-0-incident-response-ai-gateways) for what that looks like in practice.


---

*Originally published at [yadui.dev](https://yadui.dev/blog/github-actions-persist-credentials-security) by Abhinav Yadav.*

來源:DEV Community · dev.to