ghalint

Activity, over time

Daily GitHub activity and social mentions.

Mentions

Daily posts linking to this repository

Loading mentions chart

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Mentions

2posts · 30d
Nov 50 posts
  1. Always Set persist-credentials: false in actions/checkout --- title: "Always Set persist-credentials: false in actions/checkout" published: true description: "Set persist-credentials: false in actions/checkout. The default leaves a GitHub token on disk for later steps. The fix, and how to still git push." tags: githubactions, security, devops, cicd canonical_url: yadui.dev cover_image: yadui.dev --- 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`: [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: 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: - 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: [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: # 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 — Rust-based, runs as a GitHub Action: - uses: woodruffw/zizmor-action@v1 Output when `persist-credentials` is not set: 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 and 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 and the LiteLLM 1.107.0 incident response guide for what that looks like in practice. --- Originally published at yadui.dev by Abhinav Yadav.

  2. DEV · · @masutaka

    It May Be a Good Idea to Verify CLI Binaries Used in GitHub Actions with Both SHA256 Checksums and Attestations --- title: "It May Be a Good Idea to Verify CLI Binaries Used in GitHub Actions with Both SHA256 Checksums and Attestations" published: true description: "Why I verify CLI binaries downloaded from GitHub Releases with both a SHA256 checksum and gh attestation verify, and how gh release verify-asset can help for immutable releases." tags: ["github", "githubactions", "security"] cover_image: dev-to-uploads.s3.us-east-2.amazonaws.com canonical_url: masutaka.net --- In masutaka/actions, which I introduced in [2026-02-23-1], CLI binaries such as jactionlint are downloaded from each tool's GitHub Releases. The downloaded binaries are verified with both a SHA256 checksum and gh attestation verify. In this post, I'll explain how and why. Verifying with SHA256 Checksums and Attestations Here's how masutaka/actions verifies jactionlint: :link: .github/workflows/gh_action_lint.yml - name: Install jactionlint env: GH_TOKEN: ${{ github.token }} REPO: jdx/jactionlint ASSET: "*_linux_amd64.tar.gz" TAG: v2.0.2 ASSET_SHA256: 026bce6d0ef196f5b22bf43acb1348d47c5d0a5ae61d6ec3bdba1229a823161b SIGNER_WORKFLOW: jdx/jactionlint/.github/workflows/release.yaml EXECUTABLE: jactionlint run: | gh release download "$TAG" \ --repo "$REPO" \ --pattern "$ASSET" \ --output asset.tar.gz # 正規の release workflow がそのタグからビルドした成果物であることを検証する gh attestation verify asset.tar.gz \ --repo "$REPO" \ --signer-workflow "$SIGNER_WORKFLOW" \ --source-ref "refs/tags/$TAG" # ピンした時点から中身が変わっていないことを検証する echo "${ASSET_SHA256} asset.tar.gz" | sha256sum -c - tar -xzf asset.tar.gz "$EXECUTABLE" It downloads the asset from GitHub Releases and uses it only if it passes both gh attestation verify and sha256sum -c. --signer-workflow only checks the workflow path; it doesn't pin the ref or commit. So I use --source-ref to restrict it to artifacts built from the recorded tag. The attestations checked by gh attestation verify have to be generated by the publisher in their release workflow. For the publisher's side of the story, Songmu's post goes into detail: :link: GitHub Actions上で証明書を付与するサプライチェーン構築 | おそらくはそれさえも平凡な日々 After reading it, I also migrated my own github-nippou to actions/attest. By the way, --signer-workflow can point to a different repository than --repo. For example, ghalint is signed by suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml, a reusable workflow called from its release.yaml. Why Verify with Both? Because SHA256 checksums and gh attestation verify verify different things. - SHA256 checksum - Verifies that "the content hasn't changed since the checksum was recorded" - Even a binary built locally without going through CI will match if you record its value - gh attestation verify - Verifies that "the artifact was built from that tag by the legitimate release workflow" - However, unless it's an immutable release, tags can be moved. If an attacker gains write access, moves the tag to a tampered commit, and rebuilds, it still passes - It also passes if the artifact is rebuilt under the same tag and the asset is overwritten. For example, when a build-time dependency gets compromised later on I couldn't find any official recommendation to use both, but I did find some supporting information. The GitHub Docs page on Artifact attestations says that "artifact attestations are not a guarantee that an artifact is secure." There's also SLSA (Supply-chain Levels for Software Artifacts), an OpenSSF framework that defines levels of software supply chain security. The build provenance checked by gh attestation verify is also in the SLSA format. SLSA's Threats & mitigations states that the threat of compromised build-time dependencies is out of SLSA's scope. The mitigation it suggests is pinning dependencies by digest: > You may be able to mitigate this threat by pinning your build dependencies, preferably by digest rather than version number. This is aimed at publishers, but I think users pinning by checksum follows the same idea. That said, checksums only protect against replacement after you've recorded them. If a dependency was already compromised at the time of the first release, both the checksum and the attestation will pass. Personally, I'd be happy if gh attestation verify could also check that the artifact matches a recorded checksum, but for now I'm verifying with two separate methods. If There's No Build Provenance, Release Attestations May Help What follows is something I learned while writing this post. I've only tried it locally and don't use it in masutaka/actions. gh attestation verify verifies build provenance, so it can't be used unless the publisher generates it. For example, tfupdate, a tool that bulk-updates version constraints for Terraform core and providers, doesn't generate build provenance, as you can see from the repository's Attestations page. So gh attestation verify fails with a 404: $ gh attestation verify asset.tar.gz --repo minamijoyo/tfupdate Error: HTTP 404: Not Found (https://api.github.com/repos/minamijoyo/tfupdate/attestations/sha256:559bc7c6c15496682a3207cbdc4e6327de252f5f1a4348f5098027d6bcc4ebe2?per_page=30&predicate_type=https%3A%2F%2Fslsa.dev%2Fprovenance%2Fv1) Even so, if the repository has immutable releases enabled, you can use release attestations. GitHub generates them automatically for releases published after the setting is turned on. A release attestation lets you verify that "this asset was included in the release for this tag." It doesn't show up on the Attestations page, so check the release page to see whether it's an immutable release. tfupdate v0.10.2 is an immutable release, so it can be verified with gh release verify-asset: $ gh release verify-asset v0.10.2 asset.tar.gz --repo minamijoyo/tfupdate Calculated digest for asset.tar.gz: sha256:559bc7c6c15496682a3207cbdc4e6327de252f5f1a4348f5098027d6bcc4ebe2 Resolved tag v0.10.2 to sha1:9d61d78d73e1325629e46c9e92d713ab258b50cd Loaded attestation from GitHub API ✓ Verification succeeded! asset.tar.gz is present in release v0.10.2 This method is described in the GitHub Docs page Verifying the integrity of a release. A release attestation doesn't tell you whether the artifact was built by the legitimate workflow. Still, it does confirm that the downloaded file is the same as the asset in that release. With an immutable release, tags can't be moved and assets can't be overwritten, so it also closes the tag-moving loophole I mentioned in "Why Verify with Both?" Conclusion It seems like a good idea to verify CLI binaries used in GitHub Actions and elsewhere with both SHA256 checksums and gh attestation verify. The checksum confirms the content hasn't changed, and the attestation confirms it was built by the legitimate release workflow. Even if the publisher doesn't generate build provenance, gh release verify-asset looks usable for immutable releases. I've only tried this one locally. Here's a comparison of the three methods: | Verification | What it confirms | What the publisher needs | |---|---|---| | SHA256 checksum | The content hasn't changed since the checksum was recorded | Nothing (a checksums.txt lets you cross-check the value you record) | | gh attestation verify | It was built from that tag by the legitimate release workflow | Generating build provenance | | gh release verify-asset | It was included in the release for that tag | Enabling immutable releases | Another option is to install via mise-action. For tools using the aqua backend, checksums are recorded in mise.lock, along with attestation information if available. In the AI era, this kind of defense in depth should matter more than ever. I hope this helps someone. References - GitHub Actions上で証明書を付与するサプライチェーン構築 | おそらくはそれさえも平凡な日々 - Artifact attestations - GitHub Docs - Verifying the integrity of a release - GitHub Docs - Threats & mitigations - SLSA

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

No packages found.

Live mentions badge

Show how many posts mention your project in the last 30 days.

Live mentions · last 30 days