Allow LOCAL_CA SLSA attestations for private projects

What does this MR do and why?

This MR lets private and internal projects generate SLSA provenance attestations when they are signed with LOCAL_CA. SIGSTORE stays limited to public projects, as ADR 007 requires, because Sigstore records every attestation in the public Rekor transparency log.

  • SupplyChain.attest_method_allowed?(build) checks whether a build may be attested with the method it resolves to: the project is public, or the method is LOCAL_CA. The other conditions (feature flag, build stage, ATTEST_* variables) stay in SupplyChain.publish_provenance_for_build?.
  • SupplyChain.publish_provenance_for_build? already let LOCAL_CA builds of private projects through, so Ci::Slsa::PublishProvenanceWorker was enqueued for them, but Ci::Slsa::PublishProvenanceService then rejected every project that was not public. Both now call the shared rule, so the two checks cannot drift apart. The service keeps its own check because a project's visibility can change between the job finishing and the worker running.
  • Internal projects follow the same rule as private projects. They are not visible to everyone, so their attestations must not reach Rekor either.
  • When the service rejects a build, the message is now SIGSTORE attestations are only enabled for public projects.
  • The private SIGSTORE example in spec/lib/supply_chain_spec.rb ran in the factory's default test stage, so it passed on the stage check alone, even with the visibility check removed. It now runs in the build stage.

Who this affects. Everything stays behind the slsa_provenance_statement feature flag (default off, https://gitlab.com/gitlab-org/gitlab/-/work_items/547866). Public projects see no change. A private or internal project reaches the new path only when the flag is enabled for it, its job runs in the build stage with ATTEST_BUILD_ARTIFACTS or ATTEST_CONTAINER_IMAGES, and its attestation method resolves to LOCAL_CA, which is the default for these projects. On GitLab.com the flag is currently enabled only for public projects, so nothing changes there until it is enabled for a private one. On GitLab Self-Managed it applies only where an administrator enables the flag.

Merge order. This MR must merge after !264318 (merged), which signs LOCAL_CA attestations with the project CA. Without it, LOCAL_CA builds are still signed keylessly through Sigstore, so a private project would get a permanent public Rekor entry. The two MRs share no files, so this one is based on master and marked as blocked by 264318. If 264318 is reverted, this MR must be reverted too.

The commits can be reviewed one at a time. The first fixes the weak spec, the second adds attest_method_allowed?, and the third uses it in the gate without changing behaviour. The fourth is the only change in behaviour: the service accepts private and internal projects that use LOCAL_CA.

There are no database, API or UI changes. No changelog entry, as the feature flag is off by default.

Tests: spec/lib/supply_chain_spec.rb covers the new rule for public, internal and private projects with SIGSTORE, LOCAL_CA and no ATTEST_METHOD, on and off GitLab.com. spec/services/ci/slsa/publish_provenance_service_spec.rb checks that private and internal projects are rejected with SIGSTORE without calling any publisher, and publish with LOCAL_CA, explicit or by default. Both changed files have full line and branch coverage, and each new example fails when the line it covers is reverted.

References

Screenshots or screen recordings

Not applicable, as this is a backend-only change.

How to set up and validate locally

This requires cosign on the Sidekiq host, a runner, and the changes from !264318 (merged).

  1. Enable the feature flag for a private project:

    project = Project.find_by_full_path('root/slsa-private-test')
    Feature.enable(:slsa_provenance_statement, project)
  2. Add these jobs to the project's .gitlab-ci.yml and run a pipeline:

    stages:
      - build
    
    build-local-ca:
      stage: build
      image: alpine:latest
      variables:
        ATTEST_BUILD_ARTIFACTS: "true"
      script:
        - echo "local CA, job $CI_JOB_ID" > local-ca.txt
      artifacts:
        paths:
          - local-ca.txt
    
    build-sigstore:
      stage: build
      image: alpine:latest
      variables:
        ATTEST_BUILD_ARTIFACTS: "true"
        ATTEST_METHOD: "SIGSTORE"
      script:
        - echo "sigstore, job $CI_JOB_ID" > sigstore.txt
      artifacts:
        paths:
          - sigstore.txt
  3. After the jobs finish, check the attestations in a Rails console:

    jobs = project.ci_pipelines.order(:id).last.builds.pluck(:name, :id).to_h
    SupplyChain::Attestation.where(build_id: jobs['build-local-ca']).pluck(:status, :signing_method, :signing_certificate_id)
    # => [["success", "local_ca", <id of the project CA>]]
    SupplyChain::Attestation.where(build_id: jobs['build-sigstore']).count
    # => 0
  4. log/application_json.log has a Not publishing attestations for build entry for the build-sigstore job with public_requirement_met: false.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Merge request reports

Loading
Loading