---
key: "osps-baseline-maintainers"
title: "Meet the OpenSSF OSPS Baseline as a maintainer: the levels, every Level 1 control on GitHub, and the evidence"
url: "https://softwaresecurity.com/domains/supply-chain/ci-cd/osps-baseline-for-maintainers/"
api: "https://softwaresecurity.com/api/v1/articles/osps-baseline-maintainers.json"
domain: "supply-chain"
topic: "Software supply chain"
type: "Guide"
identifiers: []
tags: ["Dependency management","Supply chain attacks","CI/CD security","Secure development lifecycle","Supplier and open source risk"]
stages: ["build","release","govern"]
roles: ["developer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Meet the OpenSSF OSPS Baseline as a maintainer: the levels, every Level 1 control on GitHub, and the evidence

Walk through the eight control families and three maturity levels of OSPS Baseline v2026.08.28, meet each Level 1 requirement on a GitHub repository, and keep evidence a consumer can check.

Canonical URL: https://softwaresecurity.com/domains/supply-chain/ci-cd/osps-baseline-for-maintainers/

## In short

Pin the version first: OSPS Baseline v2026.08.28 is current, with 64 active requirements in eight families, each first required at Level 1 (any project), Level 2 (a code project with at least two maintainers and a small number of consistent users) or Level 3 (a code project with a large number of consistent users). Level 1 on GitHub is mostly settings and files: MFA for everyone with access, a ruleset that blocks direct commits and deletion of the default branch, workflows that don't trust pull-request input, push protection for secrets, an OSI or FSF license, CONTRIBUTING.md, SECURITY.md with contacts, a committed dependency manifest and no binaries in Git. Keep evidence per requirement, publish a Security Insights file, and state the claim with its date, version and level. No tool checks all of it for you.

- Requirement IDs look like `OSPS-AC-03.01`: family `AC`, control `03`, requirement `01`. Version v2026.08.28 has 41 controls and 64 active requirements: 24 first required at Level 1, 19 at Level 2 and 21 at Level 3.
- The Baseline is self-attested and point in time. Its FAQ suggests a claim such as: as of a date, this project complies with OSPS Baseline version X, level N.
- Rulesets are readable by anyone with read access, so ruleset-based controls make public evidence; classic branch protection and secret-scanning settings are visible only to admins.
- Level 2 adds least-privilege CI tokens, required status checks and tests, signed releases with change logs, a disclosure policy with private reporting, and a security assessment.
- Level 3 adds non-author approval of every change, blocking SCA and SAST checks, SBOMs with releases, verification instructions that name the signer, a VEX feed and a support policy.
- Scorecard's own analysis finds its probes fully satisfy only a few Baseline controls; Minder ships a Level 1 profile; the Privateer GitHub scanner, used by LFX Insights, covers every requirement but marks some as needing review.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| Baseline version to claim | v2026.08.28 | Only the version labeled current is for new compliance efforts. | baseline.openssf.org |
| Default branch ruleset | Require a pull request, restrict deletions, block force pushes; empty bypass list | Meets OSPS-AC-03.01 and OSPS-AC-03.02; add required status checks for OSPS-QA-03.01. | — |
| Workflow permissions (Settings, Actions, General) | Read repository contents and packages permissions | Meets OSPS-AC-04.01; set job-level `permissions` for OSPS-AC-04.02. | — |
| Secret scanning and push protection | On | Free on public repositories; supports OSPS-BR-07.01. | — |
| Private vulnerability reporting | On | One way to meet OSPS-VM-03.01; name it in SECURITY.md. | — |
| Security Insights file | `security-insights.yml`, schema 2.2.0 | Scanners read it for controls the API can't see. | — |

## Verify it

1. Run the evidence script against the repository and confirm every Level 1 row it reports is `pass`, with `needs-admin` rows re-run by an admin.
2. Push a commit straight to the default branch and try to delete it in a test copy of the repository; both must be refused.
3. Validate `security-insights.yml` against the Security Insights 2.2.0 schema and check that every URL in it resolves.
4. Open a pull request that edits a workflow to echo `${{ github.event.pull_request.title }}` in a `run:` step and confirm review or Scorecard's Dangerous-Workflow check catches it.
5. Read the published claim and confirm it names the Baseline version, the level and the date it was assessed.

## What you will learn

- Name the eight OSPS Baseline families and what each maturity level asks of a project.
- Meet every Level 1 requirement on a GitHub repository and record the evidence for it.
- Plan the Level 2 and Level 3 controls that change how the project reviews, releases and handles vulnerabilities.
- Read what Scorecard, Minder and the Privateer scanner do and do not check.

Audience: Maintainers of open-source projects on GitHub, and the engineers in companies that publish them

Before you start:

- Admin access to a GitHub repository, or a test repository you can change.
- Know how branch rulesets and GitHub Actions permissions work.

Time: About 45 minutes: 20 to read and 25 for the exercise

## Which version should you claim, and what do the levels mean?

The Open Source Project Security Baseline (OSPS Baseline) is a list of MUST statements for open-source projects, maintained by the OpenSSF. Versions are dated: v2025.02.25, v2025.10.10, v2026.02.19 and the current v2026.08.28, released on 28 August 2026. The project says previous versions are for historical reference and only the current one should be used for new compliance efforts, so pin the version in every claim. The v2026.08.28 release added no controls; it let `OSPS-LE-03.01` accept a `LICENSES/` directory and moved the mappings to other frameworks into machine-readable Gemara documents. The previous release retired `OSPS-BR-01.02` and added `OSPS-BR-01.03`, `OSPS-DO-07.01` and `OSPS-BR-01.04`.

Each requirement belongs to a control and a family: `OSPS-AC-03.01` is the first requirement of control 03, Protect the Primary Branch from Accidental Modification, in the Access Control family. Each requirement lists the maturity levels it applies to. Level 1 is for any code or non-code project with any number of maintainers or users; Level 2 is for a code project with at least two maintainers and a small number of consistent users; Level 3 is for a code project with a large number of consistent users. The published checklist files each requirement under the first level it applies to: 24 at Level 1, 19 at Level 2 and 21 at Level 3.

The Baseline is not a score and not a comparison between projects; its FAQ says so directly. Nothing requires you to meet it unless a sponsoring organization such as a foundation does, but every project is encouraged to meet Level 1. Compliance is self-attested and point in time, so write the claim the way the FAQ suggests: as of 11 October 2026, this project complies with OSPS Baseline version 2026.08.28, Level 1. Some controls depend on admin-only settings that a consumer can't see, which is why the evidence you keep matters more than a badge.

## What do the eight control families cover?

The table counts the active requirements in each family by the level at which they first apply. Most Level 1 requirements also list Levels 2 and 3; if you script against the catalog YAML, note that in v2026.08.28 `OSPS-BR-07.01` and `OSPS-VM-02.01` list only Level 1, although the published checklist and the requirements' purpose apply them at every level.

| Family | What it covers | Level 1 | Level 2 | Level 3 |
| --- | --- | --- | --- | --- |
| AC: Access Control | MFA, collaborator permissions, primary branch protection, CI/CD token privileges | 4 | 1 | 1 |
| BR: Build and Release | Untrusted input in pipelines, version identifiers, encrypted channels, change logs, dependency tooling, signed releases, secrets | 5 | 4 | 3 |
| DO: Documentation | User guides, defect reporting, verification instructions, support and security-update scope, dependency policy, build instructions | 2 | 2 | 4 |
| GV: Governance | Roles and members with sensitive access, public discussion, contribution guide, review of permission grants | 2 | 3 | 1 |
| LE: Legal | Right to commit (DCO or CLA), OSI or FSF licenses, license location | 4 | 1 | 0 |
| QA: Quality | Public source and history, dependency list and SBOM, status checks, subprojects, no binaries, automated tests, merge approvals | 6 | 2 | 5 |
| SA: Security Assessment | Design documentation, external interfaces, security assessment and threat model | 0 | 3 | 1 |
| VM: Vulnerability Management | Disclosure policy, security contacts, private reporting, published vulnerabilities and VEX, SCA and SAST policies | 1 | 3 | 6 |

## How do you meet each Level 1 requirement on GitHub, and what proves it?

Level 1 is the floor the Baseline asks every project to reach. On GitHub most of it is a setting or a file. The evidence column says what to keep: an API export, a file at a path, or a scan result, dated. /domains/supply-chain/ci-cd/source-repository-protection/ has the ruleset JSON and the commands to apply it, and /domains/supply-chain/ci-cd/github-actions-hardening/ covers the workflow patterns behind `OSPS-BR-01.01` and `OSPS-BR-01.03`.

| Requirement | What it asks | How to meet it on GitHub | Evidence to keep |
| --- | --- | --- | --- |
| OSPS-AC-01.01 | MFA to read or change sensitive resources in the authoritative repository | Organization settings, Authentication security: require two-factor authentication; prefer passkeys or security keys. For a personal-account repository, every maintainer enables 2FA. | `two_factor_requirement_enabled: true` from `GET /orgs/{org}`, read with an owner token |
| OSPS-AC-02.01 | New collaborators get manual or lowest-privilege permissions | Set the organization's base permission to Read or No permission and add collaborators with the lowest role that works. | `default_repository_permission` from `GET /orgs/{org}`; the collaborator list with roles |
| OSPS-AC-03.01 | Direct commits to the primary branch are prevented | A branch ruleset on the default branch with Require a pull request before merging, active, with an empty bypass list. | Rules from `GET /repos/{owner}/{repo}/rules/branches/{branch}`, which anyone with read access can call |
| OSPS-AC-03.02 | Deleting the primary branch needs explicit confirmation | The same ruleset with Restrict deletions; add Block force pushes so history can't be rewritten. | The same export, showing `deletion` and `non_fast_forward` |
| OSPS-BR-01.01 | Untrusted metadata is sanitized before a pipeline uses it | Never expand `${{ github.event.* }}` text such as titles, branch names or commit messages inside `run:`; pass it through `env:` and quote it. | The workflow files and Scorecard's Dangerous-Workflow result |
| OSPS-BR-01.03 | Untrusted code snapshots can't reach privileged credentials | Build pull-request code in `pull_request` jobs with read-only tokens; never check out the pull request head in `pull_request_target` or `workflow_run` jobs that hold secrets. | Workflow review record and Dangerous-Workflow result |
| OSPS-BR-03.01 | Official project channels use encrypted transport only | List only `https://` URLs for the website, documentation and chat; on GitHub Pages turn on Enforce HTTPS. | The list of official channels, such as Security Insights `project.homepage` and documentation links |
| OSPS-BR-03.02 | Distribution channels resist adversary-in-the-middle attacks | Publish through GitHub Releases or the ecosystem's registry over HTTPS, and sign releases when you can. | Security Insights `repository.release.distribution-points` |
| OSPS-BR-07.01 | Unencrypted secrets can't be stored by accident in version control | Turn on secret scanning and push protection (free on public repositories), and ignore files such as `.env` in `.gitignore`. | `security_and_analysis` from `GET /repos/{owner}/{repo}`, readable only by admins; the push protection bypass log |
| OSPS-DO-01.01 | User guides for all basic functionality, once there is a release | A README or documentation site covering install, configuration and use, with warnings on destructive actions. | The documentation link at the release tag |
| OSPS-DO-02.01 | A guide for reporting defects | Enable Issues and add a bug report template, or a Reporting bugs section in CONTRIBUTING.md. | The template path and `has_issues` |
| OSPS-GV-02.01 | A public place to discuss changes and usage problems | Issues, Discussions or a public mailing list. | `has_issues` and `has_discussions` from the repository API |
| OSPS-GV-03.01 | An explanation of the contribution process | CONTRIBUTING.md at the root, in `.github/` or in `docs/`, or a clear statement that contributions aren't accepted. | `files.contributing` from `GET /repos/{owner}/{repo}/community/profile` |
| OSPS-LE-02.01 | The source license meets the OSI Open Source Definition or the FSF Free Software Definition | Choose an OSI-approved or FSF-free license. | The SPDX ID from `GET /repos/{owner}/{repo}/license`, checked against the SPDX License List's `isOsiApproved` and `isFsfLibre` |
| OSPS-LE-02.02 | The license for released assets meets the same definitions | Release under the same license, or state the release license if it differs. | The license inside the release archive; Security Insights `repository.release.license` when it differs |
| OSPS-LE-03.01 | The source license is in a LICENSE or COPYING file or a LICENSES/ or LICENSE/ directory | Keep the license file at the repository root; an extension such as `.md` is allowed. | The `path` from the license API |
| OSPS-LE-03.02 | The license ships with the released assets | Include the license in source archives, or publish it next to each release asset. | A listing of the release archive showing the license file |
| OSPS-QA-01.01 | The source repository is publicly readable at a static URL | A public repository; if you mirror it, say which copy is primary. | `visibility: public` |
| OSPS-QA-01.02 | A public record of every change, who made it and when | Keep history public and don't rewrite it; the force-push block above enforces that. | The commit history and the `non_fast_forward` rule |
| OSPS-QA-02.01 | A list of direct dependencies where the package manager supports one | Commit the manifest, such as `package.json`, `go.mod`, `pyproject.toml` or `Cargo.toml`. | The manifest path in the dependency graph |
| OSPS-QA-04.01 | Projects with several repositories list their codebases | List every repository that is part of the project. | Security Insights `project.repositories` |
| OSPS-QA-05.01 | No generated executable artifacts in version control | Build binaries in CI instead of committing them. | Scorecard's Binary-Artifacts result |
| OSPS-QA-05.02 | No unreviewable binary artifacts in version control | Keep libraries and executables out of Git; images and audio are allowed. | Scorecard's Binary-Artifacts result |
| OSPS-VM-02.01 | Security contacts in the project documentation | SECURITY.md at the root, in `.github/` or in `docs/`, naming how to reach the security contacts. | The file path and Security Insights `project.vulnerability-reporting.contact` |

## How do you publish the facts a scanner can't see?

Several requirements depend on things an API can't infer: which channels are official, which repositories belong to the project, where the support policy and the self-assessment are. The Baseline's maintainer guidance points to the OpenSSF Security Insights specification for this, and the Privateer scanner reads the file for many controls. Put `security-insights.yml` at the repository root or in `.github/`. The example below uses schema 2.2.0 (January 2026) and records the evidence for `OSPS-BR-03.02`, `OSPS-QA-04.01`, `OSPS-VM-02.01`, `OSPS-DO-05.01` and the self-assessment behind `OSPS-SA-03.01`.

Validate it before you publish it. With CUE 0.17.1 and the specification's `schema.cue` from the v2.2.0 tag, `cue vet -c -d '#SecurityInsights' schema.cue security-insights.yml` exits 0 for this file and fails with the field name when a required value such as `reports-accepted` is missing or `status` is not one of the allowed values. Update `last-reviewed` whenever you review it, because a stale file is evidence of nothing.

```yaml
header:
  schema-version: 2.2.0
  last-updated: '2026-10-11'
  last-reviewed: '2026-10-11'
  url: https://github.com/example-org/tidepool/raw/main/security-insights.yml
project:
  name: tidepool
  administrators:
    - name: Ana Ruiz
      affiliation: Example Org
      email: ana@example.org
      primary: true
  repositories:
    - name: tidepool
      url: https://github.com/example-org/tidepool
      comment: The library and its command-line tool; the only codebase in this project.
  vulnerability-reporting:
    reports-accepted: true
    bug-bounty-available: false
    policy: https://github.com/example-org/tidepool/blob/main/SECURITY.md
    contact:
      name: tidepool security team
      email: security@example.org
      primary: true
  documentation:
    quickstart-guide: https://github.com/example-org/tidepool/blob/main/README.md
    support-policy: https://github.com/example-org/tidepool/blob/main/SUPPORT.md
    signature-verification: https://github.com/example-org/tidepool/blob/main/docs/verify.md
repository:
  url: https://github.com/example-org/tidepool
  status: active
  accepts-change-request: true
  accepts-automated-change-request: true
  core-team:
    - name: Ana Ruiz
      primary: true
    - name: Ben Okafor
      primary: false
  license:
    url: https://github.com/example-org/tidepool/blob/main/LICENSE
    expression: Apache-2.0
  documentation:
    contributing-guide: https://github.com/example-org/tidepool/blob/main/CONTRIBUTING.md
    dependency-management-policy: https://github.com/example-org/tidepool/blob/main/docs/dependencies.md
    governance: https://github.com/example-org/tidepool/blob/main/GOVERNANCE.md
    security-policy: https://github.com/example-org/tidepool/blob/main/SECURITY.md
  release:
    automated-pipeline: true
    distribution-points:
      - uri: https://github.com/example-org/tidepool/releases
        comment: Signed release archives with SLSA provenance and an SBOM.
    changelog: https://github.com/example-org/tidepool/releases/tag/v{version}
  security:
    assessments:
      self:
        comment: Threat model and security self-assessment, reviewed at each minor release.
        evidence: https://github.com/example-org/tidepool/blob/main/docs/security-assessment.md
        date: '2026-09-30'
    tools:
      - name: osv-scanner
        type: SCA
        rulesets: [default]
        integration: {adhoc: false, ci: true, release: true}
        results: {}
```

## Which Level 2 controls matter most?

Level 2 assumes at least two maintainers and regular users, so it adds the controls that keep a release trustworthy when more than one person can ship. The table covers the ones that change settings or pipelines. The rest are documents: `OSPS-DO-06.01` (how you select, obtain and track dependencies), `OSPS-DO-07.01` (build instructions), `OSPS-GV-01.01` and `OSPS-GV-01.02` (members with access to sensitive resources, and roles), `OSPS-GV-03.02` (requirements for acceptable contributions), `OSPS-SA-01.01` and `OSPS-SA-02.01` (design documentation of actors and actions, and external interfaces), `OSPS-BR-02.01` (a unique version for each release) and `OSPS-BR-05.01` (standard dependency tooling).

| Requirement | How to meet it on GitHub | Evidence to keep |
| --- | --- | --- |
| OSPS-AC-04.01: tasks default to the lowest permissions | Settings, Actions, General, Workflow permissions: Read repository contents and packages permissions, set at the organization when you have one. | `default_workflow_permissions: read` from `GET /repos/{owner}/{repo}/actions/permissions/workflow` |
| OSPS-QA-03.01: status checks pass before a commit reaches the primary branch | Add Require status checks to pass to the default-branch ruleset, with the check pinned to the GitHub Actions app. | The `required_status_checks` rule in the ruleset export |
| OSPS-QA-06.01: CI runs at least one automated test suite before a change is accepted | A `pull_request` workflow that runs the tests, required by the ruleset. | The workflow and a sample of merged pull requests with passing runs |
| OSPS-BR-06.01: releases are signed or listed in a signed manifest with hashes | Attest release artifacts with `actions/attest`, which signs SLSA provenance with a short-lived Sigstore certificate, or sign a checksums file. | The attestation for each release, verifiable with `gh attestation verify` |
| OSPS-BR-04.01: each release has a log of functional and security changes | Release notes that say what changed and why it matters, not only commit titles. | The release notes at each tag |
| OSPS-LE-01.01: contributors assert the right to contribute on every commit | Require a `Signed-off-by:` trailer (the DCO) and enforce it with a status check such as the DCO GitHub App, or use a CLA. GitHub's sign-off policy covers only commits made in the web interface. | The DCO check on merged pull requests |
| OSPS-VM-01.01: a coordinated disclosure policy with a response timeframe | SECURITY.md that says how to report, what happens next and how quickly you respond. | SECURITY.md at the release tag |
| OSPS-VM-03.01: private reporting directly to the security contacts | Turn on private vulnerability reporting, or publish a monitored security address. | `enabled: true` from `GET /repos/{owner}/{repo}/private-vulnerability-reporting` |
| OSPS-VM-04.01: discovered vulnerabilities are published | Publish GitHub security advisories, and request CVE IDs for them, with affected and fixed versions. | The advisory list |
| OSPS-SA-03.01: a security assessment of the most likely and impactful problems | A short self-assessment kept in the repository and reviewed for new features, such as a threat model of the parser or the network surface. | The assessment with its date, linked from Security Insights `repository.security.assessments.self` |

## What do least-privilege pipelines and signed releases look like?

These workflows meet `OSPS-AC-04.02` (every job asks for only the permissions it needs, under an empty top-level default), `OSPS-QA-06.01`, `OSPS-BR-06.01` and `OSPS-QA-02.02`. The pull-request workflow runs the tests and the dependency review action, which fails on a newly added dependency with a high or critical advisory or a license outside the allowlist; it runs on public repositories, and on private ones with GitHub Code Security. The release workflow attests the archives twice with `actions/attest`: once for SLSA build provenance and once for the CycloneDX SBOM. Actions are pinned to full commit SHAs with the version in a comment. Both files pass actionlint 1.7.12.

Require the `test` and `dependency-review` checks in the default-branch ruleset, and add the ruleset rule Require code scanning results with CodeQL so a new high-severity finding blocks the merge. That pair is the automated enforcement `OSPS-VM-05.03` and `OSPS-VM-06.02` ask for at Level 3, alongside the written thresholds in `OSPS-VM-05.01`, `OSPS-VM-05.02` and `OSPS-VM-06.01`.

```yaml
name: pull-request
on:
  pull_request:
    branches: [main]
permissions: {}
jobs:
  test:
    runs-on: ubuntu-24.04
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - run: make test
  dependency-review:
    runs-on: ubuntu-24.04
    permissions:
      contents: read
    steps:
      - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
        with:
          fail-on-severity: high
          allow-licenses: Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, MIT
---
name: release
on:
  push:
    tags: ['v*']
permissions: {}
jobs:
  release:
    runs-on: ubuntu-24.04
    permissions:
      contents: write
      id-token: write
      attestations: write
      artifact-metadata: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - run: make dist
      - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
        with:
          subject-path: dist/*.tar.gz
      - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
        with:
          subject-path: dist/*.tar.gz
          sbom-path: dist/sbom.cdx.json
      - run: gh release create "$GITHUB_REF_NAME" dist/* --verify-tag --notes-file dist/release-notes.md
        env:
          GH_TOKEN: ${{ github.token }}
```

## Which Level 3 controls change how the project works?

Level 3 is for projects with a large number of consistent users, where a compromise reaches many people. Its requirements mostly turn Level 2 practices into enforced rules and published commitments.

- `OSPS-QA-07.01`: at least one non-author human approval before merging to the primary branch. Require one approval in the ruleset, with Require approval of the most recent reviewable push, so the approved revision is the merged one; a single-maintainer project can't meet this.
- `OSPS-VM-05.03` and `OSPS-VM-06.02`: every change is evaluated against a documented policy for vulnerable and malicious dependencies and for code weaknesses, and blocked on violation unless declared non-exploitable. The required checks above do this; the policy documents are `OSPS-VM-05.01`, `OSPS-VM-05.02` and `OSPS-VM-06.01`.
- `OSPS-QA-02.02`: every compiled release asset ships with an SBOM. /domains/supply-chain/sbom/generate-release-inventory/ covers generating one per release.
- `OSPS-DO-03.01` and `OSPS-DO-03.02`: documentation explains how to verify release integrity and authenticity, and the expected identity of the signer. Name the repository and the signing workflow, not only the tool, and keep the instructions outside the release pipeline so one breach can't change both.
- `OSPS-VM-04.02`: vulnerabilities in components that don't affect the project are recorded in a VEX document. /domains/supply-chain/vex/producer-and-consumer-decisions/ covers writing the statements.
- `OSPS-DO-04.01` and `OSPS-DO-05.01`: a statement of the scope and duration of support for each release, and when releases stop getting security updates, in SUPPORT.md or SECURITY.md.
- `OSPS-SA-03.02`: threat modeling and attack surface analysis of critical code paths, updated for new features.
- `OSPS-GV-04.01`, `OSPS-BR-07.02` and `OSPS-BR-01.04`: a policy to review collaborators before granting escalated permissions, a policy for storing, accessing and rotating secrets, and sanitizing even trusted collaborators' manual workflow inputs such as `workflow_dispatch` values.
- `OSPS-AC-04.02`, `OSPS-BR-02.02`, `OSPS-QA-04.02`, `OSPS-QA-06.02` and `OSPS-QA-06.03`: job-level least privilege, every release asset tied to the release identifier, subprojects held to the same rules, and documented testing practice and policy.

## How do you write verification instructions that name the signer?

For `OSPS-DO-03.02`, the instructions must tell users who is expected to have produced the release. With GitHub artifact attestations that identity is the repository and the workflow file that signed the provenance. The commands below are the shape to publish; the same commands verify the GitHub CLI's own release: with gh 2.94.0, `gh attestation verify gh_2.94.0_linux_arm64.tar.gz --repo cli/cli --signer-workflow cli/cli/.github/workflows/deployment.yml` exits 0, and naming any other workflow file makes it fail. Add `--source-ref` when you know which branch or tag the release workflow runs from.

```bash
# Verify a tidepool release (example-org/tidepool) before you use it.
# 1. The archive is the one the release workflow built and attested:
gh attestation verify tidepool-1.4.0.tar.gz \
  --repo example-org/tidepool \
  --signer-workflow example-org/tidepool/.github/workflows/release.yml \
  --deny-self-hosted-runners
# 2. The SBOM attestation for the same archive:
gh attestation verify tidepool-1.4.0.tar.gz \
  --repo example-org/tidepool \
  --predicate-type https://cyclonedx.org/bom
```

## What evidence should you keep, and how do you collect it?

Keep one dated record per requirement: its ID, its status, the evidence, and the date. The script below collects what GitHub's API shows for the Level 1 and Level 2 settings it can read, as JSON lines, and marks admin-only settings `needs-admin` instead of guessing. Run it with an authenticated `gh` and `jq`. On 11 October 2026 it reported `pass` for all ten readable rows on `ossf/scorecard`, whose default branch has an active ruleset with `pull_request`, `deletion`, `non_fast_forward` and `required_status_checks` rules, and `needs-admin` for its workflow permissions and push protection. On `ossf/security-baseline`, whose branch uses classic protection, the three branch rows were `needs-admin`; on `octocat/Hello-World` they failed, as did the license, CONTRIBUTING and SECURITY.md rows.

Keep the rest by hand: the organization MFA and base-permission exports, the workflow review behind `OSPS-BR-01.01`, links to documentation at the release tag, and the scanner output. Store the records with each release, and repeat the collection on a schedule so a quietly weakened setting shows up.

```bash
#!/usr/bin/env bash
# Record the OSPS Baseline v2026.08.28 evidence GitHub's API shows for one repository.
# Usage: ./osps-evidence.sh owner/repo > evidence.jsonl   (needs an authenticated gh and jq)
# "needs-admin" means the setting is only visible to a repository or organization admin.
set -euo pipefail
repo="$1"
emit() { jq -cn --arg id "$1" --arg status "$2" --arg evidence "$3" \
  '{baseline: "v2026.08.28", id: $id, status: $status, evidence: $evidence, checked: (now | todate)}'; }

meta=$(gh api "repos/$repo")
branch=$(jq -r .default_branch <<<"$meta")
rules=$(gh api "repos/$repo/rules/branches/$branch" --jq '[.[].type] | unique')
has_rule() { jq -e --arg t "$1" 'index($t) != null' <<<"$rules" >/dev/null && echo pass || echo fail; }
if [ "$rules" = "[]" ] && [ "$(gh api "repos/$repo/branches/$branch" --jq .protected)" = "true" ]; then
  # Classic branch protection is only readable with admin rights; rulesets are public.
  for id in OSPS-AC-03.01 OSPS-AC-03.02 OSPS-QA-03.01; do
    emit "$id" needs-admin "$branch is protected by a classic rule; export it with an admin token"
  done
else
  emit OSPS-AC-03.01 "$(has_rule pull_request)" "rules on $branch: $rules"
  emit OSPS-AC-03.02 "$(has_rule deletion)" "rules on $branch: $rules"
  emit OSPS-QA-03.01 "$(has_rule required_status_checks)" "rules on $branch: $rules"
fi

visibility=$(jq -r .visibility <<<"$meta")
emit OSPS-QA-01.01 "$([ "$visibility" = public ] && echo pass || echo fail)" "visibility: $visibility"
emit OSPS-GV-02.01 "$(jq -r 'if .has_issues or .has_discussions then "pass" else "fail" end' <<<"$meta")" \
  "issues: $(jq .has_issues <<<"$meta"), discussions: $(jq .has_discussions <<<"$meta")"

if license=$(gh api "repos/$repo/license" 2>/dev/null); then
  spdx=$(jq -r '.license.spdx_id // "NOASSERTION"' <<<"$license")
  emit OSPS-LE-03.01 pass "license file: $(jq -r .path <<<"$license")"
  free=$(curl -fsS https://spdx.org/licenses/licenses.json | jq -r --arg id "$spdx" \
    '[.licenses[] | select(.licenseId == $id and (.isOsiApproved or .isFsfLibre))] | length')
  emit OSPS-LE-02.01 "$([ "$free" = 1 ] && echo pass || echo manual)" "SPDX ID $spdx; OSI or FSF approved: $free"
else
  emit OSPS-LE-03.01 fail "no license file GitHub can find"
  emit OSPS-LE-02.01 fail "no license file GitHub can find"
fi

contributing=$(gh api "repos/$repo/community/profile" --jq '.files.contributing.html_url // empty')
emit OSPS-GV-03.01 "$([ -n "$contributing" ] && echo pass || echo fail)" "contributing guide: ${contributing:-none}"

policy=none
for path in SECURITY.md .github/SECURITY.md docs/SECURITY.md; do
  if gh api "repos/$repo/contents/$path" --silent 2>/dev/null; then policy=$path; break; fi
done
emit OSPS-VM-02.01 "$([ "$policy" != none ] && echo pass || echo fail)" "security policy: $policy"

pvr=$(gh api "repos/$repo/private-vulnerability-reporting" --jq .enabled)
emit OSPS-VM-03.01 "$([ "$pvr" = true ] && echo pass || echo manual)" \
  "private vulnerability reporting: $pvr (another private channel in SECURITY.md also counts)"

perm=$(gh api "repos/$repo/actions/permissions/workflow" --jq .default_workflow_permissions 2>/dev/null) || perm=unknown
case "$perm" in
  read) emit OSPS-AC-04.01 pass "default GITHUB_TOKEN permissions: read" ;;
  write) emit OSPS-AC-04.01 fail "default GITHUB_TOKEN permissions: write" ;;
  *) emit OSPS-AC-04.01 needs-admin "workflow permissions are not readable with this token" ;;
esac

push=$(jq -r '.security_and_analysis.secret_scanning_push_protection.status // "unknown"' <<<"$meta")
case "$push" in
  enabled) emit OSPS-BR-07.01 pass "secret scanning push protection: enabled" ;;
  disabled) emit OSPS-BR-07.01 fail "secret scanning push protection: disabled" ;;
  *) emit OSPS-BR-07.01 needs-admin "security_and_analysis is only returned to admins" ;;
esac
```

## What do Scorecard, Minder and the Privateer scanner actually check?

Automated tools help, but none of them can see every requirement, and the Baseline FAQ warns against treating tool output as the true security posture. Know what each one reads before you cite it as evidence.

| Tool | What it checks against the Baseline | Limits |
| --- | --- | --- |
| Privateer plugin for GitHub repositories (`ossf/pvtr-github-repo-scanner`, v0.32.0), also as the OSPS Baseline Scanner action | Has a step for every requirement in the bundled catalogs, including `osps-baseline-2026-08`, and powers LFX Insights' security results. The action v1.5.2 runs scanner v0.31.0 and can upload failures as SARIF. | Many steps rely on the Security Insights file; some return Needs Review; Level 2 and 3 steps are less tested than Level 1. It needs a personal access token, and the MFA check runs only with `admin:org`. |
| Minder (`mindersec/minder-rules-and-profiles`, `security-baseline` directory) | A `security-baseline-level-1` profile for GitHub with rule types for 22 Level 1 requirements, written for v2026.08.28; several have remediations that open a pull request or create a ruleset. | Level 1 only, rules marked alpha, no MFA or subproject check, and GitLab coverage is partial. Its `osps-ac-03-01` rule checks force pushes rather than direct commits. |
| OpenSSF Scorecard v5.5.0 | Scores checks from 0 to 10. The Baseline maps Branch-Protection, Dangerous-Workflow, Signed-Releases, Pinned-Dependencies, License, Binary-Artifacts, CI-Tests, Code-Review, Security-Policy, Vulnerabilities and SAST to Baseline controls as related, not equivalent. | Scorecard's own coverage analysis, written against v2026.02.19, rates most controls partial or uncovered and MFA and collaborator permissions not observable. A pass or fail per control is proposed for Scorecard v6, not shipped. |

## How will consumers read your claim?

The Baseline is written for the people who run a project, but its readers include the companies that depend on it. Its FAQ says a consumer can accept the project's self-attestation or make another arrangement, because some controls depend on settings only maintainers see. Make that easy: publish the claim with its version, level and date in the README or SECURITY.md, link the Security Insights file and the scanner output, and keep the per-requirement records available on request.

On the consumer side, the OpenSSF's Secure Supply Chain Consumption Framework describes how organizations take in open source, and /domains/supply-chain/dependencies/s2c2f-adoption/ walks through it. /domains/supply-chain/dependencies/project-health-and-intake/ shows how a consumer weighs signals such as Scorecard results when deciding to adopt a package.

## Guided practice

tidepool (fictional) is a parsing library with three maintainers and a few hundred regular users. The maintainers want to claim OSPS Baseline v2026.08.28 Level 1 and plan Level 2. Use the facts below to find the gaps, fix the risky workflow and write the claim.

### Supplied material

```text
Repository: github.com/example-org/tidepool, public, default branch main.
Organization: two-factor authentication required; base permission Write.
Ruleset on main: Require a pull request before merging, active, bypass list: org admins.
No rule restricting deletions or force pushes.
Workflow permissions setting: Read and write.
Secret scanning: on. Push protection: off.
Files: README.md (install and usage), LICENSE (Apache-2.0), SECURITY.md ("email security@example.org"), CONTRIBUTING.md.
Issues: enabled. Discussions: disabled.
Committed: go.mod, go.sum, testdata/fixtures.bin (a crafted input file), tools/protoc (a compiled binary).
Releases: tags v1.0.0 to v1.4.0 with GitHub release archives, not signed.
Workflow .github/workflows/label.yml:
  on: pull_request_target
  steps:
    - uses: actions/checkout@<sha>  # with: ref: ${{ github.event.pull_request.head.sha }}
    - run: ./scripts/label.sh "${{ github.event.pull_request.title }}"
  secrets used: LABEL_BOT_TOKEN
```

### 1. Find the Level 1 gaps

Go through the Level 1 table and list each requirement tidepool doesn't meet yet, with the evidence that shows it.

Worked answer: OSPS-AC-02.01: base permission Write means a new member gets write access to every repository; set it to Read. OSPS-AC-03.02: no deletion rule, so deleting main isn't blocked by the ruleset; add Restrict deletions and Block force pushes. OSPS-AC-03.01 holds only partly while org admins can bypass the pull-request rule; empty the bypass list. OSPS-BR-01.01 and OSPS-BR-01.03 fail on label.yml (next step). OSPS-BR-07.01 is weak without push protection; turn it on. OSPS-QA-05.01 fails on tools/protoc, a generated executable; build or download it in CI instead. testdata/fixtures.bin is not an executable, but OSPS-QA-05.02 still asks whether it can be reviewed: the xz-utils backdoor hid in binary test files, so generate it from a script in the repository or document exactly how it was made. The other Level 1 rows are met.

### 2. Fix the workflow

Explain what is wrong with label.yml and rewrite the risky part.

Worked answer: It runs on pull_request_target, so it has secrets and a write token, yet it checks out and runs the pull request's own code (scripts/label.sh at the head SHA), which breaks OSPS-BR-01.03. It also expands the pull request title straight into a shell command, which breaks OSPS-BR-01.01: GitHub substitutes the title before the shell runs, so a title containing a double quote and a semicolon ends the argument and starts a new command. Fix: don't check out the head SHA in this job, run only the base branch's script, pass the title through `env: TITLE: ${{ github.event.pull_request.title }}` and use "$TITLE" quoted, and give the job only `pull-requests: write`.

### 3. Plan the first Level 2 changes

Pick the four Level 2 changes that matter most for tidepool's releases and say what evidence each produces.

Worked answer: OSPS-AC-04.01: set workflow permissions to read, evidence from the workflow permissions API. OSPS-QA-03.01 and OSPS-QA-06.01: a test workflow required by the ruleset, evidence in the rule export and merged pull requests. OSPS-BR-06.01: attest release archives with actions/attest, evidence verifiable with gh attestation verify. OSPS-VM-01.01 and OSPS-VM-03.01: expand SECURITY.md with a response timeframe and turn on private vulnerability reporting. The documentation controls (DO-06.01, DO-07.01, GV-01.01, GV-01.02, GV-03.02, SA-01.01, SA-02.01, SA-03.01) follow as files.

### 4. Write the claim

Write the sentence tidepool can publish once the Level 1 gaps are closed, and list what backs it.

Worked answer: As of &lt;date of the evidence run&gt;, tidepool complies with OSPS Baseline version 2026.08.28, Level 1. Backing: the dated evidence records for all 24 Level 1 requirements, the org MFA and base-permission exports read by an owner, the ruleset export, the workflow review, and security-insights.yml validated against schema 2.2.0. It is a self-attestation for that date, not a score or a certification.

Deliverable: A list of tidepool's Level 1 gaps with fixes, a corrected label workflow, a four-item Level 2 plan with evidence, and a dated Baseline claim.

Scope: tidepool, its settings and its people are fictional. Checking these settings on a real repository needs the admin access the evidence script marks as needs-admin.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [OpenSSF OSPS Baseline v2026.08.28 (controls, levels and recommendations)](https://baseline.openssf.org/versions/2026-08-28). v2026.08.28 (28 August 2026).
- [OSPS Baseline v2026.08.28 checklist by level](https://baseline.openssf.org/versions/2026-08-28-checklist). v2026.08.28.
- [OSPS Baseline: versions and guiding principles](https://baseline.openssf.org/)
- [OSPS Baseline release notes](https://baseline.openssf.org/release_notes). 2026-08-28 and 2026-02-19 entries.
- [OSPS Baseline FAQ (self-attestation, point-in-time claims, consumers)](https://baseline.openssf.org/faq)
- [OSPS Baseline: implementation guidance for maintainers](https://baseline.openssf.org/maintainers)
- [ossf/security-baseline: control catalog YAML at tag v2026.08.28](https://github.com/ossf/security-baseline/tree/v2026.08.28/baseline). v2026.08.28.
- [OpenSSF Security Insights specification: schema.cue](https://github.com/ossf/security-insights/blob/v2.2.0/spec/schema.cue). v2.2.0 (31 January 2026).
- [Privateer plugin for GitHub repositories (OSPS Baseline scanner)](https://github.com/ossf/pvtr-github-repo-scanner). v0.32.0 (9 October 2026).
- [OSPS Baseline Scanner GitHub Action](https://github.com/revanite-io/osps-baseline-action). v1.5.2 (1 October 2026).
- [Minder rules and profiles: OpenSSF Security Baseline](https://github.com/mindersec/minder-rules-and-profiles/tree/main/security-baseline)
- [OpenSSF Scorecard: OSPS Baseline coverage analysis](https://github.com/ossf/scorecard/blob/main/docs/osps-baseline-coverage.md). Written against Baseline v2026.02.19.
- [OpenSSF Scorecard v6 proposal: OSPS Baseline conformance](https://github.com/ossf/scorecard/blob/main/openspec/changes/osps-baseline-conformance/proposal.md)
- [GitHub Docs: Available rules for rulesets](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets)
- [GitHub Docs: Managing GitHub Actions settings for a repository (workflow permissions)](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository)
- [GitHub Docs: Configuring private vulnerability reporting for a repository](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/configure-vulnerability-reporting/configure-for-a-repository)
- [GitHub blog: Push protection is generally available, and free for all public repositories](https://github.blog/news-insights/product-news/push-protection-is-generally-available-and-free-for-all-public-repositories/). May 2023.
- [GitHub Docs: Managing the commit signoff policy for your organization](https://docs.github.com/en/organizations/managing-organization-settings/managing-the-commit-signoff-policy-for-your-organization)
- [actions/attest: generate signed attestations (provenance and SBOM)](https://github.com/actions/attest). v4.2.2.
- [GitHub Docs: About dependency review](https://docs.github.com/en/code-security/concepts/supply-chain-security/dependency-review)
- [SPDX License List (isOsiApproved and isFsfLibre)](https://spdx.org/licenses/). 3.29.0 (16 September 2026).