CI without secrets

GitHub Actions gives each job a short-lived OIDC token. The credential provider uses it, so there is nothing to create, store or rotate: no App, no secret, no action of ours.

The workflow

Add id-token: write to the job’s permissions and install the credential provider. The prebuilt binary takes seconds with cargo binstall; cargo install --locked works too but compiles for about a minute unless cached.

.github/workflows/build.yml
name: build
on: [push, pull_request]
permissions:
  id-token: write   # for the OIDC token
  contents: read
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: cargo-bins/cargo-binstall@<commit sha>   # pin to a commit
      - run: cargo binstall --no-confirm cargo-credential-privatecrates
      - run: cargo build --locked

The same .cargo/config.toml your developers use is all Cargo needs. cargo-binstall is not preinstalled on GitHub’s runners; pin its action to a commit SHA, as you would any third-party action.

What happens

  1. The provider notices it is in Actions (from ACTIONS_ID_TOKEN_REQUEST_URL) and asks GitHub for an OIDC token whose audience is your registry, https://acme.privatecrates.dev.
  2. It exchanges that token at the registry for a read-only registry token, valid for one hour and cached until it expires.
  3. Cargo uses it for index reads and downloads. The registry checks the job’s repository belongs to your organisation and applies your ci_read setting.

Permission checks for CI use our App’s installation tokens, so CI never consumes a developer’s GitHub rate limit.

If id-token: write is missing

The provider fails straight away with a message naming the line to add, rather than falling back to a browser sign-in that would hang the job:

workflow or job
permissions:
  id-token: write

Pull requests from forks

GitHub does not give workflows triggered from forks an OIDC token, so they cannot read private crates. That is intended: a stranger’s pull request should not be able to download your code.

Docker builds and CI outside GitHub Actions

At launch only GitHub Actions can read the registry, so that no long-lived credential exists anywhere. Other environments get their dependencies from an Actions job, in one of two ways.

Build the image in GitHub Actions

Let the Actions job fetch the dependencies with cargo vendor, which also writes the Cargo configuration that points at the vendored copies. The image then builds offline, and no registry token ever reaches Docker. Push the result to the container registry your platform deploys from.

workflow steps
      - uses: actions/checkout@v5
      - uses: cargo-bins/cargo-binstall@<commit sha>   # pin to a commit
      - run: cargo binstall --no-confirm cargo-credential-privatecrates
      # Fetch private and public dependencies here, where the OIDC token is available.
      - run: mkdir -p .cargo && cargo vendor --locked vendor >> .cargo/config.toml
      # The build context now holds every dependency; the image build needs no registry access.
      - run: docker build -t ghcr.io/acme/story-app:${{ github.sha }} .
Dockerfile
FROM rust:1 AS build
WORKDIR /src
COPY . .
RUN cargo build --release --locked --offline

Or hand the vendored tree to another system

The same vendor directory and configuration can be uploaded as an artifact for an external build, which then needs no registry access at all.

If a build outside Actions must ever receive a registry token, pass it through a BuildKit secret mount (RUN --mount=type=secret), never a build argument: build arguments can be recorded in the image history.

Platforms that only offer build arguments

Some hosting platforms expose build-time variables only as Docker build arguments. Never give such a build a registry token; use one of the two routes above instead.