↓Skip to main content

Lock your GitHub Actions

GitHub Actions now supports workflow lockfiles.

Because GitHub Actions are fetched from Git, the previous advice was to pin to a full SHA instead of a tag. This is because tags are mutable and susceptible to supply-chain attacks where an attacker points a previously safe tag at malicious code.

For example, an old workflow looks like:

name: Lint

on: [push, pull_request, workflow_dispatch]

permissions: {}

env:
  FORCE_COLOR: 1
  RUFF_OUTPUT_FORMAT: github

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: j178/prek-action@44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d # v3.1.0

Here’s how I set up lockfiles.

Install #

Install the gh-actions-lock extension to the gh CLI:

$ gh extension install github/gh-actions-lock
✓ Installed extension github/gh-actions-lock

Lock ’em #

In your repo, run:

$ gh actions-lock

✓ All 1 workflows valid

The workflow now uses vX.Y.Z tags and looks like this:

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 946971c..3503607 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -13,7 +13,7 @@ jobs:
     runs-on: ubuntu-latest

     steps:
-      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+      - uses: actions/checkout@v7.0.1
         with:
           persist-credentials: false
-      - uses: j178/prek-action@44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d # v3.1.0
+      - uses: j178/prek-action@v3.1.0

And it generates a lockfile at .github/workflows/actions.lock:

# This file is machine-generated by `gh actions-lock`.
# Do not edit by hand; run `gh actions-lock` to update.
# Docs: https://gh.io/actions-lockfile
version: 'v0.0.3'
workflows:
    '.github/workflows/lint.yml':
        - 'actions/checkout@v7.0.1'
        - 'j178/prek-action@v3.1.0'
dependencies:
    'actions/checkout@v7.0.1':
        ref: 'v7.0.1'
        commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
        owner_id: 44036562
        repo_id: 197814629
    'j178/prek-action@v3.1.0':
        ref: 'v3.1.0'
        commit: 'sha1-44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d'
        owner_id: 10510431
        repo_id: 1052387486

Commit modified workflows and the new lockfile.

Renovate and Dependabot #

Good news, both already support lockfiles! Your periodic updates will also update the lockfile. Remember to use cooldowns.

Validate #

GitHub also recommends verifying lockfiles in CI to ensure it’s up-to-date and not corrupt.

They suggest copying this verify-lockfile.yml into your repo. I decided to put it into my existing lint workflow, but in its own job so it runs independently and in parallel:

name: Lint

on: [push, pull_request, workflow_dispatch]

permissions: {}

env:
  FORCE_COLOR: 1
  RUFF_OUTPUT_FORMAT: github

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v7.0.1
        with:
          persist-credentials: false
      - uses: j178/prek-action@v3.1.0

  # Fails when .github/workflows/actions.lock is out of sync with the
  # workflows or a locked pin no longer verifies upstream.
  lockfile:
    runs-on: ubuntu-slim
    env:
      GH_TOKEN: ${{ github.token }}

    steps:
      - uses: actions/checkout@v7.0.1
        with:
          persist-credentials: false
      - run: gh extension install github/gh-actions-lock --pin v0.4.1
      - run: gh actions-lock --verify

Note I’m using the lightweight ubuntu-slim runner here. Also, it’s pinning to v0.4.1; we’ll have to update that at some point or figure out a way to automate it. That’s for another time.

Don’t forget that we just modified the actions in a workflow, so need to relock with gh actions-lock. Nothing actually changes if adding to lint.yml because it’s already locking actions/checkout. But if you created a new file like verify-lockfile.yml, you’ll need to relock for that. The CI will let you know.

zizmor #

zizmor is a security linter for GitHub Actions and strongly recommended. If you don’t use it, please do.

Versions before v1.32.0 tell you to hash-pin actions, and v1.32.0 is the first to support lockfiles.

I use zizmor in prek, so add/update zizmor in .pre-commit-config.yaml:

  - repo: https://github.com/zizmorcore/zizmor-pre-commit
    rev: v1.32.0
    hooks:
      - id: zizmor

Locked! #

OK, commit and push!


Header photo: Slaymaker Barry Co. Sprocket Locks in The Bearings (vol. XII no. 23, 2nd January 1896) in the Smithsonian Libraries and Archives, with no known copyright restrictions.