Safeguard
Software Supply Chain•

How do you sign artifacts and record provenance in GitLab CI?

Use an id_tokens entry with aud sigstore for Cosign keyless signing, enable runner metadata for SLSA provenance, and verify with a pinned identity.

Safeguard Security Team
4 min read

In GitLab CI you can sign artifacts keylessly with Cosign by requesting an ID token with the sigstore audience, and you can have the runner write SLSA provenance by setting one variable. Verify later with the exact certificate identity and issuer you expect.

How does keyless signing work in a job?

GitLab's signing examples need GitLab.com, a project local CI configuration and Cosign 2.0.1 or later. The job asks for an identity token and Cosign picks it up from the SIGSTORE_ID_TOKEN variable.

build_and_sign_artifact:
  stage: build
  image: alpine:latest
  variables:
    COSIGN_YES: "true"
  id_tokens:
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  before_script:
    - apk add --update cosign
  script:
    - echo "This is a build artifact" > artifact.txt
    - cosign sign-blob artifact.txt --bundle cosign.bundle
  artifacts:
    paths:
      - artifact.txt
      - cosign.bundle

The bundle holds the signature, certificate and transparency log proof. Keep it with the artifact.

How do you verify?

Verification needs the artifact, the bundle, the signing identity and the issuer:

cosign verify-blob artifact.txt --bundle cosign.bundle \
  --certificate-identity "https://gitlab.com/my-group/my-project//path/to/.gitlab-ci.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://gitlab.com"

The identity is the instance URL, project path, a double slash, the CI config path, then at sign and the ref. The docs stress that the double slash is required, and a single slash gives a "none of the expected identities matched" error.

Pinning the identity is the point. Without it you only learn that someone signed it. With it you learn that this project's pipeline, on this ref, signed it.

What are the known limits?

The id_tokens configuration must live in the project being built. AutoDevOps, CI files included from other repositories, and child pipelines are not supported. Build and sign in the same job so nothing can alter the artifact in between. For container images, sign the immutable digest, not the tag.

How do you get SLSA provenance?

Set RUNNER_GENERATE_ARTIFACTS_METADATA to "true" globally or per job. The runner then generates an SLSA provenance and a statement binding it to the build artifacts. It is stored as a JSON file named after the artifact, with the suffix -metadata.json, or artifacts-metadata.json if no name is set.

The format is an in-toto v0.1 statement with a SLSA 1.0 provenance predicate. Subject entries carry each artifact's file name and sha256. Build details include external parameters, where environment variable names appear with empty values so secrets are not exposed, plus runner name, job ID, the project URL and commit, and start and finish times. The docs page did not state a minimum runner version, so confirm it for your fleet.

Does signing equal trust?

No. A constructed example: an attacker with permission to edit the CI file on a feature branch adds a step that signs a tampered build. The signature is real and the identity is the project's. Your verification identity should name the protected branch, for instance refs/heads/main, so the feature branch signature fails.

What to do

  1. Add an id_tokens entry with aud set to sigstore to the build job, and install Cosign 2.0.1 or later.
  2. Sign in the same job that builds the artifact.
  3. Publish the bundle beside the artifact.
  4. Turn on RUNNER_GENERATE_ARTIFACTS_METADATA for release jobs.
  5. Verify in the deploy job with a pinned identity on your protected branch and the GitLab issuer.
  6. Sign container image digests, not tags.
  7. Protect the branch and CI file so only reviewed changes can produce signatures you will accept.

Safeguard can scan the dependencies feeding the build and enforce policies before release.

Quick check

Why does the certificate identity use a double slash?

GitLab's docs say it separates the project path from the CI config path, and a single slash fails verification.

Do child pipelines work with keyless signing?

According to the docs, no. The id_tokens configuration must be in the project being built, and child pipelines and included files from other repositories are unsupported.

Sources

Last reviewed: 18 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.