Securing Actions workflows with Immutable Actions

actions, immutable-actions, artifact-attestations, security, supply-chain-security

Table of Contents

Long time no see!

I’ve been sharpening my skills in the DevSecOps peaks ⛰️🏔️⛰️. Now, I’m back stronger, ready for new quests—hope you’ve been practicing too.

Recently, a customer posed an intriguing challenge: Could Immutable Actions disrupt the way workflows reference actions? This led me deep into the lore of GitHub Actions. The verdict? No worries. Why? Immutable actions wildcard versioning and resolution make it seamless.

Before we dive into this enigma, let’s set the stage: What are Immutable Actions? Why are they vital? And how do artifact attestations fit into the picture? Alright, ninja 🥷, let’s resume our journey.

⚠️ Note: At the time of writing, Immutable Actions are in limited public preview and subject to change.

1. The Genesis: Artifact Attestations #

Artifact attestations in GitHub [1] [2] enhance supply chain security by cryptographically linking artifacts such as binaries to their source code and build process. They ensure both integrity and provenance, reducing the risk of tampered or malicious software:

For developers and teams, this means greater confidence in the security of dependencies and reduced risk from compromised build environments. The following table compares the security implications of artifacts with and without cryptographic signatures, and with and without provenance metadata:

 With Cryptographic SignatureWithout Cryptographic Signature
With Provenance Metadata🟢 Secure
- Full traceability.
- Artifact integrity assured.
- Minimal risk (if implemented correctly).
🟡 Limited Security
- Traceability available.
- Artifacts may be tampered post-build.
- Risks of distribution attacks (e.g., artifact substitution).
Without Provenance Metadata🟡 Limited Security
- Artifact integrity assured.
- No traceability.
- Loss of reproducibility.
- Builds from unverified sources.
🔴 Insecure
- No traceability or integrity assurance.
- High risk of malicious artifacts and insecure builds.

As a last note, remember that attestations are only effective if verified, making it crucial to validate signed artifacts, particularly for binaries, packages, and manifests.

2. The Next Step: Immutable Actions #

Now that we know what are artifact attestations, let’s take these concepts a step further and apply them to GitHub Actions. Just like build artifacts, it’s essential to ensure that the actions used in workflows are trustworthy and secure. The good news is that we can achieve this through Immutable Actions.

Immutable Actions are a new feature in GitHub that ensures the actions used in CI/CD pipelines are secured and untampered. Let’s break down how Immutable Actions work with three essential features:

Let’s explore how Immutable Actions compare to the mutable ones and how they mitigate common security risks:

AspectBefore Immutable ActionsWith Immutable Actions
Source & BehaviorActions fetched as code from GitHub using mutable git tags, making them less traceable and prone to unexpected changes.Actions served as packages from ghcr.io, resolved with immutable package versions, ensuring better traceability and consistency.
(Im)mutabilityVulnerable to tag mutations, allowing potentially malicious code changes.Secure resolution with immutable tags and verified publishing flow, preventing tampering.
VersioningWildcard versions (e.g. v1) manually maintained, leading to inconsistencies.Reliable resolution of wildcard versions to the latest matching package (e.g. v1 -> 1.3.2), eliminating versioning issues (✦✦ see next section).

3. A note on versioning and resolution #

Before Immutable Actions, actions in workflows were referenced using mutable tags or branches, e.g.:

uses: my-org/my-action@v1       # tag
uses: my-org/my-action@v1.3.2   # tag
uses: my-org/my-action@main     # branch

While referencing by commit SHA (my-org/my-action@f4b1...73aa) was possible - and the most secure way since not mutable - it was not an ideal solution from a user experience perspective due to its lack of flexibility.

Immutable Actions introduce wildcard versioning to balance stability and flexibility in GitHub workflows. This features allows tag-like references such as v1.3 in workflows to automatically resolve to the latest immutable action package matching the semantic version 1.3.z. As a result, immutable actions in workflows can now be referenced either via their strict package version (e.g. my-org/my-action@1.3.2, note the absence of v prefix), or via tag-like references potentially with wildcard versioning (e.g my-org/my-action@v1.3).

The following diagram demonstrates how GitHub resolves wildcard references to the latest matching package version:

Wildcard resolution example

While wildcard versioning and resolution provides flexibility, it challenges the core principle of immutability. Immutable Actions ensure an action’s content and behavior remain consistent, making dynamic changes from wildcard references potentially problematic. For example, if a workflow references v1, it might unintentionally introduce behavior changes with a new minor or patch release, like moving from 1.2 to 1.3. For maximum stability and immutability, it’s recommended to pin actions to a specific version, such as 1.3.2.

4 - Practical Example #

Enough theory, let’s now see how to use Immutable Actions in practice. We’ll use this demo repository which has Immutable Actions enabled to showcase how they work. It contains a typical structure for a simple custom action written in JavaScript:

In addition, we have the publish-immutable-actions.yml workflow file, triggered when a new released is created to publish the immutable action:

name: "Publish Immutable Action Version"
on:
  release:
    types: [created]
jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      packages: write
    steps:
      - name: Checking out
        uses: actions/checkout@v4
      - name: Publish
        id: publish
        uses: actions/publish-immutable-action@0.0.1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

All the magic happens in the publish-immutable-action action.

I created and pushed the tag v1.0.0, created a release from this tag and the immutable action has been published accordingly (you can see the attestation has been published too in the job’s log). The corresponding package in GHCR has the label Immutable.

Immutable action package version 1.0.0

I can verify the attestation of the immutable action using the GitHub CLI:

# General verification
$ gh attestation verify oci://ghcr.io/ghsioux/immutable-action-demo:1.0.0 --bundle-from-oci --owner ghsioux
Loaded digest sha256:59ec50c64e24213772a37b17715946a133de7b1bef33885a49bf691ea03f5ae5 for oci://ghcr.io/ghsioux/immutable-action-demo:1.0.0
Loaded 1 attestation from oci://ghcr.io/ghsioux/immutable-action-demo:1.0.0
✓ Verification succeeded!

sha256:59ec50c64e24213772a37b17715946a133de7b1bef33885a49bf691ea03f5ae5 was attested by:
REPO                           PREDICATE_TYPE                  WORKFLOW
ghsioux/immutable-action-demo  https://slsa.dev/provenance/v1  .github/workflows/publish-immutable-actions.yml@refs/tags/v1.0.0

# Retrieve the associated commit
$ gh attestation verify oci://ghcr.io/ghsioux/immutable-action-demo:1.0.0 --bundle-from-oci --owner ghsioux --format json | jq '.[0].verificationResult.statement.predicate.buildDefinition.resolvedDependencies[0].digest.gitCommit'
"653ed94bd9425e1e50c8de8002f16bbe22e84f38"

I’ve then updated the action code to print a new message and published a new version 1.0.1 of the immutable action following the same process.

Let’s now test the action with the following workflow:

name: "Test Immutable Action"
on:
  workflow_dispatch:
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Test immutable action (strict SemVer) # Using a specific immutable package version
        uses: ghsioux/immutable-action-demo@1.0.0 
        # Resolves directly to the immutable action package version 1.0.0,
        # ensuring the exact behavior of the action as defined at that version.

      - name: Test tag resolution to package version (full tag) # Resolves a tag to the corresponding package version
        uses: ghsioux/immutable-action-demo@v1.0.0
        # The tag v1.0.0 resolves to the immutable action package version 1.0.0.
        # This verifies that full tags correctly map to their corresponding immutable packages.

      - name: Test tag resolution to package version (wildcard versioning patch version)
        uses: ghsioux/immutable-action-demo@v1.0
        # The tag v1.0 resolves to the latest immutable action package matching version 1.0.x.
        # This tests the patch-level wildcard resolution in Immutable Actions.

      - name: Test tag resolution to package version (wildcard versioning minor version)
        uses: ghsioux/immutable-action-demo@v1
        # The tag v1 resolves to the latest immutable action package matching version 1.x.y.
        # This tests the minor-level wildcard resolution in Immutable Actions.

In the workflow logs, we can see the following sequence of printed messages:

Hello immutable shinobi!
Hello immutable shinobi!
Goodbye immutable shinobi!
Goodbye immutable shinobi!

Proving the action has been correctly resolved from tag to package version, supporting also wildcard versioning (v1 and v1.0 tags both resolving to action package version 1.0.1).

That’s nice. Now let’s test some corner cases. What if I create a tag v1.0.2 without publishing a corresponding immutable action and try to use it in a workflow? As expected, that simply does not work because there is no immutable action matching the version 1.0.2:

Action resolution error

Now let’s tackle immutability. I’ve initially created a new tag v1.0.3 from commit 231b99f and published the corresponding immutable action 1.0.3. I then updated the code of the action (commit be580c03) and made the remote tag v1.0.3 to point to this new commit. I removed the original v1.0.3 release, created it again, and guess what? Trying to create a new immutable action with that same tag failed with the error:

Action publish error

5 - Conclusion #

In this journey, we’ve explored how Immutable Actions and artifact attestations fortify GitHub Actions workflows. By cryptographically signing actions and verifying their provenance, we can enhance the security of CI/CD pipelines, bolstering the overall supply chain’s integrity.

But this is just the beginning. Immutable Actions lay the groundwork for a more robust future in DevSecOps. By adopting these practices, you not only protect your workflows but also contribute to a more secure ecosystem for everyone.

In the dojo, we learn the value of collaboration. A big shoutout to the amazing team that helped unravel the inner workings of Immutable Actions: @tinaheidinger, @thyeggman, @konradpabjan, @conorsloan, @jcambass and @steve-glass.

Stay vigilant, and keep your workflows secure, shinobi! 🥷

Gasshō 🙏