ci: keyless OIDC release pipeline + SLSA L3 provenance (closes #24524) - #28025
ci: keyless OIDC release pipeline + SLSA L3 provenance (closes #24524)#28025lee-mcfaul2 wants to merge 1 commit into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR replaces the existing static-key cosign signing setup with a fully keyless, OIDC-based supply-chain hardening layer for both PyPI and GHCR distribution channels.
Confidence Score: 5/5Safe to merge — the core publishing and signing logic is sound and no unauthorized PyPI or GHCR publish is possible. All three distribution pipelines correctly isolate build from publish permissions, use ephemeral OIDC tokens rather than long-lived secrets, and include consumer-style post-publish verification. The two observations are minor: fork-specific routing code that is inert on the canonical repo, and an incomplete regex-escape for tag characters that only matters for non-standard tag formats litellm does not currently use. publish_to_pypi.yml retains fork-specific lee-mcfaul2/litellm routing that is dead code on the canonical repo and was already removed from release-docker.yml.
|
| Filename | Overview |
|---|---|
| .github/workflows/_publish-container.yml | New reusable workflow: builds multi-arch images, keyless-signs with cosign, attaches SLSA L3 provenance via actions/attest-build-provenance, and self-verifies. SHA-pinned throughout; provenance: false intentional and documented inline. |
| .github/workflows/release-docker.yml | New orchestrator workflow: dispatches to _publish-container.yml for three images, verifies tag→commit binding, runs consumer-style verify-all matrix job. Canonical-repo guard correctly applied at preflight. |
| .github/workflows/publish_to_pypi.yml | New PyPI publishing workflow with OIDC Trusted Publisher, PEP 740 attestations, SLSA provenance, and keyless cosign signing. Fork-specific lee-mcfaul2/litellm references remain in seven places unlike release-docker.yml which had its fork allowlist removed. |
| .github/workflows/create-release.yml | Release body updated from keyful cosign instructions to keyless verify commands. No structural workflow changes. |
| README.md | Verification instructions updated from keyful to keyless. Change is minimal and accurate. |
| cosign.pub | Static ECDSA public key deleted — replaced by keyless Sigstore signing. Correct and intentional. |
| tests/test_litellm/test_release_workflow_hardening.py | New regression test suite for supply-chain invariants. All assertions read local YAML files only — no network calls. |
Reviews (2): Last reviewed commit: "ci: add OIDC-rooted keyless release pipe..." | Re-trigger Greptile
PR overviewKeyless release signing pipeline addedThis PR adds OIDC-based PyPI and container release workflows with SHA-pinned actions, tag-bound cosign verification, SLSA attestations, and scoped job permissions. I checked the workflow dispatch inputs, tag/commit validation, privileged job boundaries, artifact handoff, registry publishing, and generated verification instructions and did not find a concrete new security issue. Security review
Risk: 2/10 |
|
🤖 litellm-agent: This PR is currently BLOCKED from merge. Score: 2/5 ❌ Why blocked:
Details: Score docked for: 1 PR-related CI failure (Greptile gate: score 3/5 below required 4/5 — request a Greptile review ( Fix the issues above and push an update — the bot will re-review automatically.
|
af2af21 to
579ef8a
Compare
|
@greptileai retrigger request |
|
🤖 litellm-agent: Auto-merge skipped — the staging branch Please rebase your branch onto |
579ef8a to
8085995
Compare
Publishes LiteLLM to PyPI and GHCR entirely over OIDC, with no long-lived signing keys or registry credentials: - PyPI: OIDC Trusted Publisher upload with PEP 740 attestations, SLSA L3 build provenance via actions/attest-build-provenance, and detached keyless cosign signatures on the sdist and wheel. - Docker: a reusable build-push-sign workflow for the three images (litellm, -database, -non-root), keyless cosign signing, and SLSA L3 provenance attached as an OCI referrer. - Consumer-style verify jobs that re-check every signature and attestation the way a downstream user would (gh attestation verify, cosign verify), so a broken pipeline fails loudly. - A regression test enforcing the supply-chain invariants: SHA-pinned actions, keyless-only, OIDC-only, no static key. The static cosign.pub is removed; keyless verification roots in Fulcio/Rekor, not a checked-in public key.
8085995 to
ab81dd4
Compare
|
Thanks for taking the time to look at this, and apologies for the noise on what's a busy project — a short status to make a maintainer's review as easy as possible. Where this stands. This is a small, self-contained change: 4 CI workflows, the README, removal of the static The one failing check looks unrelated to this change. About the second PR (#28038). The auto-merge agent on this PR asked for the change to be opened against the dated daily staging branch, so I opened #28038 against Next steps. Since the staging branches roll daily, I'll hold off on further pushes or rebases until someone has a chance to weigh in — purely to avoid adding churn against a moving target on your side. The change is complete and verified (including independent reproduction on a public fork, linked above), thanks again for the project and for taking a look. |
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. |
Summary
This adds a fully public, keyless release pipeline for both distribution channels:
litellm,litellm-database, andlitellm-non-root.gh attestation verify,cosign verify), so a broken or unsigned release fails loudly instead of shipping.tests/test_litellm/test_release_workflow_hardening.py) that locks the supply-chain invariants in place (SHA-pinned actions only, keyless only, OIDC only, tag-scoped identities, no static key).Closes #24524 (which requested cosign + SLSA for the GHCR images). This PR delivers that and extends the same guarantees to the PyPI side.
Why this way?
This is meant to complement the post-incident CI/CD v2 hardening, not relitigate the March 2026 incident. Per the project's own security advisory and the incident timeline, the root cause was: a compromised CI dependency exfiltrated the publishing credential, which was then used to upload
1.82.7/1.82.8to PyPI directly, bypassing the official workflow.Two structural properties follow from that root cause, and this pipeline is built around both:
There is no publishing credential to steal. PyPI upload uses an OIDC Trusted Publisher and GHCR uses the ephemeral, job-scoped
GITHUB_TOKEN. There is no long-livedPYPI_PUBLISHtoken, API key, or password in repository secrets or the workflow environment. The exact asset that was exfiltrated in the incident does not exist in this design — auth is a short-lived OIDC token minted at runtime and scoped to the specific workflow run.An out-of-band artifact is detectable. Even setting credentials aside, every artifact carries SLSA provenance and a keyless signature that cryptographically bind it to a specific public workflow run, repository, and commit SHA. A package that was not built by the public pipeline fails
gh attestation verify/cosign verifyfor downstream users who check.This is the same class of vector that enabled the xz-utils backdoor (CVE-2024-3094): a maintainer directly published a doctored release source tarball that did not correspond to the git tree — the obfuscated payload sat in test fixtures, and the trigger was an autotools macro (
build-to-host.m4) carried only in the tarball, not in the repository, that extracted and linked it during./configure. Source review of the repo could not catch it because the released artifact was never required to match a provenance-attested build of a specific public commit. Provenance over the build output — the property this PR adds — makes "a published artifact that doesn't match a public build from a known commit" a verification failure rather than something invisible until someone diffs the tarball by hand.On the current signing approach: the post-incident control is a long-lived static cosign key (
cosign.pub, introduced in0112e53, documented ascosign verify --key …/cosign.pub). A static key proves only that a holder of the key signed the blob — it does not bind the artifact to a commit or a build, and a key holder is not necessarily the public CI. It is, in that sense, equivalent to a detached GPG signature. It also reintroduces the same category of liability as the incident: a long-lived secret that is valuable to exfiltrate, with no built-in revocation path. Keyless signing removes the key entirely — the signing identity is an ephemeral Fulcio certificate tied to the workflow's OIDC identity, logged in the public Rekor transparency log, and verifiable offline against the Sigstore TUF root. This PR therefore also removescosign.puband updates the README to the keyless verification commands.Taken together, these are two independent exposures that the current posture leaves open at the same time: without artifact-to-build provenance, an xz-class substitution — a published artifact that never corresponded to a public build — is invisible to anyone verifying downstream; and a long-lived signing key alongside a long-lived publish token keeps the credential-theft path that caused the March incident available. Neither control substitutes for the other, which is why this is one cohesive change: OIDC removes the stealable credential, and keyless + provenance makes any out-of-band artifact fail verification. Closing only one of the two leaves the other fully open.
What this delivers
PyPI (
publish_to_pypi.yml)pypa/gh-action-pypi-publish— nopassword:/token.attestations: true), and the workflow asserts the attestation actually landed on PyPI.actions/attest-build-provenance.verify-slsajob that re-verifies provenance + signatures consumer-style.GHCR (
release-docker.yml+ reusable_publish-container.yml)push-to-registry).verify-allmatrix job that re-verifies each image (cosign verify+gh attestation verify oci://…).Both
curl | bash. (This is also the class of weakness — an unpinned CI tool — that was the incident's entry point.)Verify it yourself
During development this pipeline was run end-to-end on a public fork, using temporary repository-gated test scaffolding that has since been removed from the final workflows. Those runs, logs, and signed artifacts remain public and independently re-verifiable:
lee-mcfaul2/litellm@80859952da(rebased onto currentlitellm_internal_staging).Anyone can independently verify a published fork image:
Integration notes
publish_to_pypi.yml, which was removed in3f6c0090c0("remove unused GitHub Actions workflows"). The new file is a hardened replacement, not the old one — flagging explicitly that the modify/delete is intentional, not an accident.litellm_internal_staging; opens with zero merge conflicts.litellm_internal_staging(the repository default). The datedlitellm_oss_staging_<MM_DD_YYYY>branch thatcreate_daily_staging_branch.ymlis meant to produce twice daily is not currently available — the most recent in that series islitellm_oss_staging_04_27_2026, and the scheduled workflow has been failing on every run over at least 2026-05-13 → 2026-05-15. Happy to retarget to whatever branch the current release flow expects; flagging it here since the documented daily target isn't being generated right now.pypi-publish, projectlitellm, default (real) PyPI, workflow filenamepublish_to_pypi.yml, and the existinglitellm-proxy-extraspre-check. The Trusted Publisher binding is(owner, repo, workflow-filename, environment)— all four match the prior configuration, so this slots into the existing PyPI-side setup with no target changes. The only deltas are additive hardening.litellmmust still list workflowpublish_to_pypi.yml+ environmentpypi-publish; thepypi-publish/docker-releaseGitHub environments must exist. GHCR works automatically viaGITHUB_TOKEN.BerriAI/litellm— nothing to strip, gate, or refactor on merge.README.mdis updated to the keyless verification commands. The user-facing docs site is maintained in the separateBerriAI/litellm-docsrepository, so updating any keyful instructions there is a small follow-up doc PR against that repo rather than part of this change.Scope (intentionally not included)
create-release.yml's release-branch flow beyond updating the embedded verification instructions to match the as-built pipeline.enable-docker-hub: false); GHCR is the primary channel.I'm happy to discuss and clarify.