SBOM & Compliance

SLSA Build L1 to L3 Migration Playbook

Moving from SLSA Build L1 to L3 is less a single upgrade and more a series of hardening steps. Here is the playbook we use with customers, mapped to the v1.0 specification.

Shadab Khan
Security Engineer
7 min read

The SLSA Build track looks linear on paper. You start at L1, you add provenance, you move to L2, you harden the builder, you reach L3. In practice the middle of that journey is where most teams lose a quarter of engineering time. The requirements are precise if you read the specification carefully, but the specification assumes you already know which of your CI systems can meet them and which cannot. This playbook walks through the migration the way we actually do it with customers, mapped to the v1.0 Build track requirements, and biased toward the decisions that matter operationally.

We assume you are starting from somewhere near Build L1, meaning you produce provenance of some form but cannot guarantee its authenticity or completeness. If you are below L1, meaning you generate no provenance at all, the first step is different: pick a generator and wire it into one repository before trying to think about levels. Once a single artifact has a usable in-toto statement attached, the conversation shifts from "can we do this" to "how good does this have to be."

Where does L1 actually live?

L1 requires that the build process generate provenance describing how the artifact was produced. The specification is explicit that the provenance does not need to be authenticated or complete at L1. What it does need is an honest mapping between the artifact and the build, recorded in a machine-readable form, using an attestation framework. The v1.0 specification references in-toto attestations as the expected format, with the SLSA Provenance v1 predicate (https://slsa.dev/provenance/v1) as the predicate type.

Most teams reach L1 the first time they attach a slsa-github-generator output to a GitHub release. The provenance is there, it is structured, it names the workflow and the commit SHA, and it tells a consumer enough to start asking the right questions. That is L1. If your team generates provenance through a homegrown script that writes a JSON file to the release assets, that is also L1, as long as the JSON is actually an in-toto statement with a recognizable predicate.

The gap between L1 and L2 is one of the most misunderstood parts of the framework. L2 does not require a hardened builder. It requires that the provenance be signed, that the signing key be controlled by the build platform rather than the tenant, and that the provenance accurately reflect what the platform did. Teams that are already using Sigstore cosign for artifact signing tend to assume they are at L2; they usually are not, because cosign signs the artifact, not the provenance, and the provenance is still generated by a tenant-controlled script.

What does L2 require in practice?

Build L2 asks three things. First, provenance must be authenticated by the build platform. That means a signature over the in-toto statement, produced by a key the platform controls. GitHub Actions achieves this today through the actions/attest-build-provenance action, which uses the platform's OIDC identity to produce a Sigstore keyless signature via Fulcio and records the attestation in Rekor. The key detail is that the OIDC token is issued by GitHub to the workflow, Fulcio binds the token to a short-lived certificate, and cosign 2.2+ signs the in-toto statement using that certificate. The tenant never holds the key.

Second, the provenance must be "service-generated" rather than user-generated. This is the requirement that catches teams running self-hosted GitHub Actions runners. A self-hosted runner is still the tenant's environment, which means provenance generated inside the build steps cannot be trusted at L2. The fix is either to move to hosted runners or to use a separate, isolated workflow that runs on hosted runners to produce the provenance from the hosted-runner-produced artifacts.

Third, the provenance must be complete enough that a verifier can make a trust decision. At L2 that means the build platform, the top-level build config (the workflow path and commit SHA), and the external parameters (inputs, triggering event) must be present. The slsa-github-generator v1.10 release, which landed in late 2023, makes this easier by populating the buildDefinition.externalParameters and runDetails.builder.id fields correctly without additional configuration.

We have seen teams try to shortcut L2 by signing the provenance themselves with a key in a secrets manager. This is not L2. The specification is explicit that the signing identity must be the build platform, not a tenant-owned identity, because the whole point is to eliminate the tenant's ability to forge provenance. A signature from a tenant KMS key is better than nothing, but it does not meet the L2 bar.

How does L3 change the builder?

L3 is where the builder itself has to change. The specification requires that the build platform isolate tenants from each other, that secrets used to sign provenance be inaccessible to the build steps, and that the platform prevent tampering with the build after it starts. These are platform-level properties, not tenant properties, which is why L3 adoption is largely a question of "which builder do you use" rather than "what does your pipeline look like."

GitHub-hosted runners became eligible to produce L3 provenance when the slsa-github-generator reusable workflows shipped in 2023. The trick is that the reusable workflow runs in a separate isolated environment, the OIDC token is scoped to that workflow, and the signing certificate is issued for the reusable workflow's identity rather than the caller's. A tenant writing code in a normal workflow cannot reach into the reusable workflow's environment, which satisfies the isolation requirement.

Google Cloud Build reaches L3 through a similar pattern: the platform itself signs provenance using a Google-controlled key, and the build steps run in containers that cannot access the signing material. Buildkite and CircleCI have documented their postures and, as of the last audit cycle we ran, both sit between L2 and L3 depending on configuration.

What do verifiers need to check?

The migration is not complete until consumers actually verify the provenance. The SLSA v1.0 specification does not mandate a specific verification flow, but the community has converged on a consistent pattern. Use slsa-verifier v2.5+ to check that the provenance is signed by the expected builder identity, that the predicate type is https://slsa.dev/provenance/v1, that the subject digest matches the artifact, and that the source repository and ref match the expected policy.

A common mistake is treating the Rekor entry as the trust anchor. Rekor is a transparency log: it proves that something was published, but it does not prove that the publisher is authorized. The trust anchor is the combination of the Fulcio certificate's OIDC claims and the verifier's policy. If the policy says "I trust provenance signed by https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml," then the Fulcio certificate must assert that identity, and Rekor must have logged the entry before the certificate expired. Those three together make a valid L3 verification.

How Safeguard Helps

Safeguard ingests SLSA provenance attestations automatically and maps them to the v1.0 Build track requirements so you can see exactly which builds sit at L1, L2, and L3 without reading JSON by hand. Our verifier wraps slsa-verifier and cosign verify-attestation with policy that understands your builder identities, expected source repositories, and predicate types. When a build regresses from L3 to L2 because someone switched to a self-hosted runner, Safeguard flags it before the artifact reaches production. The platform also tracks the Rekor entries over time so audit questions about historical builds have a defensible answer.

Never miss an update

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