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:
AZURE_REGISTRY
: ACR login urlCONTAINER_IMAGE
: Container image name, e.g.my-app
And Secrets:
AZURE_CLIENT_ID
:clientId
from the SP JSON outputAZURE_CLIENT_SECRET
:clientSecret
from the SP JSON output
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:
-
Login to ACR is done with Service Principal’s
clientId
andclientSecret
. -
Docker Buildx is set up as separate step.
-
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
andgithub.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
- Software Bill of Materials (SBOM) | CISA
- SLSA • Supply-chain Levels for Software Artifacts
- Build attestations | Docker Docs
- docker/login-action: GitHub Action to login against a Docker registry
- docker/setup-buildx-action: GitHub Action to set up Docker Buildx
- docker/build-push-action: GitHub Action to build and push Docker images with Buildx