Skip to content

Trusted Publishing first-class: OIDC-based crates.io publishing in CI #96

Description

@EffortlessSteven

Problem

Long-lived API tokens are the wrong default for a CI publishing tool in 2026. Shipper's value proposition is safe CI publishing, yet today's release.yml relies on secrets.CARGO_REGISTRY_TOKEN—a broad, persistent credential stored in GitHub Secrets that grants permanent publish authority. If leaked, it stays valid until manually rotated. For a tool whose entire mission is publishing safety, this is backwards.

What Trusted Publishing is

Trusted Publishing replaces long-lived tokens with ephemeral credentials. During a GitHub Actions workflow, the platform issues a short-lived OIDC identity token that proves workflow provenance; Shipper exchanges it for a temporary crates.io token (valid minutes, not years). No token stored in secrets; no permanent publish authority needed.

Reference: RFC 3691: Trusted Publishing for crates.io

Current state in Shipper

Token resolution (crates/shipper/src/ops/auth/):

  • mod.rs lines 18–31: Resolution order (env vars → credentials.toml → legacy credentials)
  • oidc.rs: OIDC detection (is_trusted_publishing_available()) checking for ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN
  • resolver.rs: Resolves actual tokens; integrates with Cargo's credential chain

Workflow (.github/workflows/release.yml):

  • Line 229: Preflight uses env: { CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} }
  • Line 259: Publish uses the same long-lived token secret
  • Lines 407, 485: Rehearse and resume modes also depend on the secret
  • No permissions: { id-token: write } (required for OIDC)

Proposed change

  1. Detect Trusted Publishing in preflight — Check ACTIONS_ID_TOKEN_REQUEST_URL + ACTIONS_ID_TOKEN_REQUEST_TOKEN presence. If both set, route through OIDC exchange instead of falling back to CARGO_REGISTRY_TOKEN.

  2. Validate workflow permissions — If OIDC env is detected but id-token: write is not explicitly set in the workflow, preflight should emit a clear, actionable error ("Workflow has OIDC env but missing permissions: { id-token: write }; token exchange will fail at publish time") rather than silently proceeding to a 401 at publish time.

  3. Diagnostic clarity — When Trusted Publishing is configured but misfire (missing repo trust link on crates.io, missing workflow permissions, malformed ID token, etc.), Shipper must surface the specific failure mode. Generic "401 Unauthorized" is unhelpful; we need "crates.io has not linked this GitHub repo; re-publish one crate manually to establish trust" or similar.

  4. Fallback ordering — Try OIDC first (via the rust-lang/crates-io-auth-action in the workflow); if unavailable/absent, fall back to existing token resolution (CARGO_REGISTRY_TOKEN → env-registry vars → credentials.toml).

  5. Documentation — Runbook (likely docs/operator-release-runbook.md) gets a new "Trusted Publishing Setup" section covering: (a) one-time crate registration on crates.io, (b) one-time repo link in crates.io UI, (c) updated release.yml with action invocation + permissions block, (d) troubleshooting common setup mistakes.

Cargo's role

Cargo itself does not yet handle OIDC exchange; per RFC 3691, this is delegated to a separate rust-lang/crates-io-auth-action GitHub Action (already exists and widely adopted, e.g. pyo3, cc crate). The action exchanges the ID token for a short-lived access token and writes it to the environment or a temp file. Shipper (and Cargo) consume the token via standard CARGO_REGISTRY_TOKEN env var.

Future: RFC notes that Cargo may eventually embed the exchange, but that's not the default today. Shipper should not block on Cargo integration; the action-based flow is stable and production-ready.

Acceptance criteria

  • Preflight detects Trusted Publishing context (both OIDC env vars present)
  • Preflight validates permissions: { id-token: write } is set (or emits clear error)
  • Diagnostic error messages distinguish OIDC-specific failures (no repo link, permissions, token validity) from generic auth failures
  • Updated release.yml workflow uses rust-lang/crates-io-auth-action@v1 (or latest stable)
  • Updated release.yml has permissions: { contents: write, id-token: write }
  • Runbook documents one-time setup + troubleshooting
  • Backward compatibility preserved: workflows without id-token: write or the action still work via CARGO_REGISTRY_TOKEN
  • Tests cover OIDC detection, permission validation, and fallback to legacy tokens

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions