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.
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 --lockedThe 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
- 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. - It exchanges that token at the registry for a read-only registry token, valid for one hour and cached until it expires.
- Cargo uses it for index reads and downloads. The registry checks the job’s repository belongs to your
organisation and applies your
ci_readsetting.
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:
permissions:
id-token: writePull 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.
- 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 }} .FROM rust:1 AS build
WORKDIR /src
COPY . .
RUN cargo build --release --locked --offlineOr 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.