Skip to content

Build and publish auditable container images with SBOM and SLSA attestation to Azure Container Registry using GitHub Actions

Published: | 6 min read

Table of contents

Open Table of contents

Intro

Automating container image builds and publishing to container registry seems pretty obvious thing to do, nothing special there. To enable auditability also should be obvious thing to do, yet it is often not done because it does not happen by default.

Achieving auditability with SBOM and SLSA attestations

What enables auditability in case of container image? Basic questions to answer are What software artifacts the container image contains? and How the image was built? And the answers to these questions are Software Bill of Materials (SBOM) and SLSA Provenance attestations, respectively.

So what exactly is SBOM? To make it short and sweet, SBOM defines what software artifacts are included in the container image, including license information.

And what about SLSA provenance? SLSA provenance defines how the image was built. This includes for example GitHub repository name where the image was built, which configuration (e.g. Dockerfile) was used, all the build parameters and so on.

These attestations can be generated at image build time with Docker’s BuildKit, and become attached to the image as metadata.

Project setup

In this article I will use my janik6n/typescript-starter: Batteries included TypeScript starter for 2025 to get up and running fast. This starter template contains a Dockerfile which is perfect for this tutorial.

Create new GitHub repository. No need to push the code just yet.

Prepare Azure

This tutorial assumes you already have a Container Registry (ACR) which will store the built images. GitHub Actions needs to authenticate to ACR, and for that we will need a Service pricipal.

Create Service principal for authentication

In this scenario the Admin account is not enabled on the Container Registry, which could be used for authentication. Instead we use a more general authentication method with Service Principal.

Unfortunately OIDC is not supported in this scenario, since we need a username and password for ACR login, so we will create a traditional secret.

First, we will need the resource ID for the Container Registry:

registryId=$(az acr show --name "[registry-name]" --resource-group "[resource-group-name]" --query id --output tsv)

This new Service Principal will not have any other privileges than to push images to the ACR instance, so we will give the required role in the creation command:

az ad sp create-for-rbac -n "[name-for-sp]" --scope "$registryId" --role AcrPush --json-auth

This will output the login information as a JSON object. Keep it safe for a while.

Configure GitHub Actions variables and secrets

Now that we have all the information we need and a clean GitHub repository created earlier, let’s add the GitHub Actions variables:

And Secrets:

Create the GitHub Actions workflow

Let’s create a workflow, which will run the build when code is pushed to main branch, and can also be triggered manually. Note: this workflow deliberately contains only the build & push workflow steps. In real projects there might be few other things worth doing before publishing the image.

Create new workflow file .github/workflows/push-to-main.yaml with contents:

name: Publish Main

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  publish:
    runs-on: ubuntu-24.04

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Login to ACR
      uses: docker/login-action@v3
      with:
        registry: ${{ vars.AZURE_REGISTRY }}
        username: ${{ secrets.AZURE_CLIENT_ID }}
        password: ${{ secrets.AZURE_CLIENT_SECRET }}

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3

    - name: Build and push
      uses: docker/build-push-action@v6
      with:
        context: . # Use path context instead of default git context
        file: ./Dockerfile # What is the image build config
        platforms: linux/amd64 # Explicitly define the image architecture(s)
        push: true # Push the image to registry
        provenance: mode=max # By default SLSA mode=min
        sbom: true # Generate SBOM
        tags: |
          ${{ vars.AZURE_REGISTRY }}/${{ vars.CONTAINER_IMAGE }}:latest
          ${{ vars.AZURE_REGISTRY }}/${{ vars.CONTAINER_IMAGE }}:${{ github.sha }}

Points of interest in this workflow:

  1. Login to ACR is done with Service Principal’s clientId and clientSecret.

  2. Docker Buildx is set up as separate step.

  3. A single action docker/build-push-action@v6 is used to build and push the container image to ACR. This is required in order to generate SBOM and SLSA attestations, it all needs to happen in single step.

    We define two container image tags: latest and github.sha. By using git commit SHA as image tag, it is explicitly defined which git commit generated this image version.

Run the GitHub Actions workflow

To run the workflow, commit the code locally and push it to GitHub to branch main and make sure the workflow runs as expected.

Download the built image and validate attestations

In order to download the image from the Container Registry we will need a way to authenticate. Just for the sake of this tutorial, we can use the same Service principal we created earlier.

Let’s add required role for it:

az role assignment create --assignee "[ClientId-from-the-JSON-output-earlier]" --scope "$registryId" --role AcrPull

Assuming we have Docker installed and running, we can login to the Azure Container Registry from the terminal by using the previously obtained credentials:

docker login [acr-login-url] -u [clientId]

Enter the sp-clientSecret as password when prompted.

Get the image ID from ACR, and pull the image. Note: since we built the image with architecture linux/amd64 and I am running MBP with Apple Silicon (ARM), I need to explicitly define the image architecture I want to pull:

docker pull --platform linux/amd64 [image-id]

Next, let’s examine the SBOM attestation:

docker buildx imagetools inspect [image-id] --format "{{json .SBOM}}"

And same with SLSA Provenance:

docker buildx imagetools inspect [image-id] --format "{{json .Provenance}}"

This information can now be used in what ever auditing tool we are using to make sure the container image complies with our requirements.

After we have done our validation, we can logout from the ACR instance with:

docker logout <acr-login-url>

Conclusion

As we can see, adding SBOM and SLSA attestations is not that difficult or complicated, it just is something that should be taken care of in our build pipelines so that the compliance against requirements can be verified later.

Further reading