If you're running containerized workloads or distributing binaries, SLSA Level 2 gives you a baseline guarantee that your artifacts weren't tampered with after build. In my GitLab CI pipelines, I enforce this by combining SBOM generation, in-toto provenance attestation, and cosign-based signing. It's not just about checking a box — it's about making sure what you ship is exactly what you built.
Generating SBOMs with Syft in GitLab CI
The first step is creating a Software Bill of Materials. I use Syft because it's fast, supports multiple formats, and integrates cleanly into Docker-based jobs. Here's a typical snippet from my .gitlab-ci.yml:
sbom:
stage: test
image:
name: ghcr.io/anchore/syft:latest
entrypoint: [""]
script:
- syft $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -o spdx-json > sbom.spdx.json
- syft $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -o cyclonedx > sbom.cdx.json
artifacts:
paths:
- sbom.spdx.json
- sbom.cdx.json
expire_in: 1 week
This runs after the image is built and pushed to the registry. I generate both SPDX and CycloneDX formats because different consumers expect different things — internal tooling might prefer SPDX, while external partners often ask for CycloneDX. The artifacts are retained for a week so downstream jobs or manual reviews can access them.
Creating Provenance Attestations with in-toto
SLSA Level 2 requires provenance that shows how the artifact was produced, including build parameters and source location. I use slsa-github-generator (adapted for GitLab) to create in-toto attestations. The key is capturing the build environment immutably.
provenance:
stage: build
image:
name: gcr.io/slsa-framework/slsa-github-generator:latest
entrypoint: [""]
script:
- SLSA_GITHUB_GENERATOR_NAME=gitlab-ci SLSA_GITHUB_GENERATOR_VERSION=$CI_PIPELINE_ID \
slsa-github-generator -source=$CI_PROJECT_URL -commit=$CI_COMMIT_SHA \
-workflow=.gitlab-ci.yml -output=provenance.attestation
artifacts:
paths:
- provenance.attestation
expire_in: 1 week
This attestation gets stored alongside the image and SBOM. Later, verification tools can check that the build matches the claimed source and pipeline definition. One gotcha: make sure your runner has network access to gitlab.com during the attestation step — otherwise it can't fetch the source or CI config.
Signing Artifacts with cosign
Finally, I sign the container image using cosign with a keypair stored in GitLab CI variables (COSIGN_KEY and COSIGN_PASSWORD). This gives you a verifiable signature that can be checked at deployment time.
sign:
stage: deploy
image:
name: gcr.io/projectsigstore/cosign:latest
entrypoint: [""]
script:
- echo "$COSIGN_KEY" | cosign key load -
- echo "$COSIGN_PASSWORD" | cosign sign --key - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
only:
- main
- merge_requests
I limit signing to protected branches to reduce key exposure risk. The signature is stored in an OCI registry alongside the image — no extra infrastructure needed. When verifying later, cosign verify --key $COSIGN_PUBKEY $IMAGE checks both the signature and, if enabled, the transparency log.
Tying It All Together for SLSA Level 2
To claim SLSA Level 2, you need:
- A hosted build platform (GitLab CI qualifies)
- Provenance that includes build parameters and source
- Generated SBOM
- Artist signed artifacts
My pipeline ensures all four are produced and stored as artifacts or attached to the image. In production, I use admission controllers (like Kyverno or OPA/Gatekeeper) to block deployment unless:
- The image has a valid cosign signature
- The SBOM is present and parseable
- The provenance attests to a trusted source and unmodified .gitlab-ci.yml
This isn’t theoretical — I’ve caught misconfigured runners and forked MRs trying to push unsigned images this way. If you’re already using GitLab CI, adding these steps costs little but raises your supply chain posture significantly. Start with SBOM generation, then layer on provenance and signing. As I mentioned before in my post about securing CI with systemd-nspawn, isolating build environments helps — but SLSA gives you the audit trail to prove it.
Cover image: HD Wallpapers · CC0 (Openverse / kamu malı) · https://stocksnap.io/photo/light-abstract-V9L6XXK3LB
