Skip to content

2026

Harden your GitHub Actions Workflows with zizmor, dependency pinning, and dependency cooldowns

It's been a crazy week (or weeks?) for a lot of people with several supply chain attacks. They all seem to have originated from the compromise of trivy (ironically, trivy is a security scanner). I wanted to understand how they initially gained access to the secrets used in their GitHub organization and found that there was an earlier attack that targeted GitHub Actions workflows of open source projects.

Looking at the details of how the secrets were extracted I noticed that they all used similar techniques. And, unless I missed something, it is through template injection and unsafe pull_request_target triggers. This could be avoided because all of those vulnerabilities (and more) can be found by zizmor, a static analysis tool for GitHub Actions!

The problem is that, unfortunately, GitHub Actions is NOT secure by default1. One would think that when following the official documentation you end up with workflows that are secure and can not be exploited. Last year, I came across zizmor and upon checking my workflows it pointed out several problems that I was quite surprised to find out about.

Of course, GitHub should make Actions more secure by default2. And it seems that the latest attacks have finally helped to make some progress in that direction. GitHub have published a security roadmap for GitHub Actions and are looking for feedback from the community.

Until that happens, what can you do right now to harden your GitHub Actions workflows?

Run npm ci as a pre-commit hook

This week I was updating a dependency in a node-based repository and several times forgot to commit the changes to package-lock.json as well.

The CI pipeline failed each time because it runs npm ci to ensure that the lock file is up to date.

I wondered whether I could catch this problem locally at commit-time via a pre-commit hook.

.pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: npm-ci
        name: npm lockfile up to date
        language: node
        entry: npm ci --dry-run
        files: ^package(-lock)?\.json$

Actually, seeing this now makes me realize that --dry-run is unnecessary. This way, the lock file gets modified (if it wasn't) and you can stage it for your commit.

Now, the CI job that runs pre-commit hooks via prek failed because the system node version is used by default. At the time of this writing it has v20 installed whereas our packages require v22 or even v24. There is a language_version property, however, since npm ci is already run in a job to actually install the dependencies. So in the end, this hook is skipped in CI.

Updating pre-commit additional dependencies using Renovate

I am a huge fan of Renovate Bot to automatically update dependencies in projects. I use it in all my projects. At the time when I discovered it, we were using GitLab so we could not use dependabot.

In general, I quickly realized that Renovate Bot has significant advantages over dependabot (which, if I am not mistaken, is restricted to running on GitHub). Renovate is fully open source (and can be self-hosted), highly configurable, supports many dependency managers, supports custom managers with regex, and much more.

Renovate has been having beta pre-commit support for quite some time which needs to be enabled explicitly. I have been using it for a while and it works great in keeping up-to-date with updates to pre-commit hooks (it is generally recommended to pin dependencies).

pre-commit hooks can have additional dependencies. These additional dependencies are specific to the language the hook uses. For example, let's assume you are using mdformat to format your Markdown files which supports additional plugins. mdformat is a Python tool so the additional dependencies are Python packages.

Here is an example pre-commit config that this website uses as of this writing:

.pre-commit-config.yaml (excerpt)
  - repo: https://github.com/executablebooks/mdformat
    rev: 1.0.0
    hooks:
      - id: mdformat
        language: python
        args: [--number, --sort-front-matter, --strict-front-matter]
        additional_dependencies:
          - mdformat-mkdocs==5.1.4
          - mdformat-front-matters==2.0.0
          - mdformat-footnote==0.1.3
          - mdformat-gfm-alerts==2.0.0
          - mdformat-ruff==0.1.3
          - ruff==0.15.4
          - mdformat-config==0.2.1
  1. Specifying the language is optional but important here as you will see when you keep reading.

How can we ensure that the additional dependencies receive get updated automatically as well?