
If your Rust project depends on a crate that lives in a private GitLab repository, the first CI run fails with a wall of failed to authenticate when downloading repository errors. Cargo can clone the dependency on your laptop because your SSH key is loaded. The CI runner has no key, no credentials and no idea who you are.
This post shows the three small pieces that fix it: a before_script that hands Cargo a short-lived token, a Cargo.toml entry that points at the private repo and one line of .cargo/config.toml that makes Cargo use the system git client. I use this setup for every private crate at work, and it also carried over to the GitHub Actions pipeline for CVBlender.
Why Cargo fails in CI
Cargo fetches git dependencies with its built-in libgit2 bindings by default. That works for public repositories, but libgit2 does not read the credential helpers or .netrc entries that the git command line uses, so there is no clean way to give it a token. The fix is to make Cargo shell out to git instead and then give git the token through its normal credential store.
GitLab already issues a token for every job: CI_JOB_TOKEN. It is scoped to the job, expires when the job ends and can clone any repository the triggering user can read, as long as that project allows job-token access from yours. That is the credential we will use.
1. Give git the job token
In .gitlab-ci.yml, store the job token in git’s credential store before the build starts:
builder:
stage: build
image: rust:1.81
before_script:
- git config --global credential.helper store
- echo "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com" > ~/.git-credentials
script:
- cargo build --release
Two details matter here. The username must be the literal string gitlab-ci-token; GitLab recognizes it and treats the password as a job token. And the host must match the one in your dependency URL exactly, so use your self-hosted domain instead of gitlab.com if you run your own instance.
The ~/.git-credentials file is written to the runner’s ephemeral file system and disappears with the job. Do not echo the token to the log, and do not commit the file.
2. Point Cargo at the private repository
In Cargo.toml, reference the dependency by its HTTPS URL, not the git@ SSH form. SSH would require a deploy key on the runner, which is more to manage and easier to leak.
[dependencies]
flasher = { git = "https://gitlab.com/org/proj/repo", branch = "main" }
Pinning matters more in CI than locally. branch = "main" rebuilds against whatever is on main when the job runs; tag = "v0.4.2" or rev = "a1b2c3d" gives you a reproducible build. For anything you ship, pin a tag or a revision and bump it deliberately.
3. Tell Cargo to use the git CLI
Create .cargo/config.toml in the project root and commit it:
[net]
git-fetch-with-cli = true
This is the line most people miss. With it, Cargo runs git fetch as a subprocess, which picks up the credential store from step one. Without it, Cargo keeps using libgit2, ignores ~/.git-credentials and fails exactly as before.
The file also works locally. On your own machine, git will use your SSH agent or OS keychain as usual, so nothing changes for developers.
4. Allow access on the dependency’s side
Since GitLab 15, a project’s job tokens can only clone other projects that have explicitly allowed it. In the private crate’s project, go to Settings > CI/CD > Token Access and add the consuming project to the allowlist. If you skip this, the clone fails with a 403 even though the token is valid.
Checking the result
A successful job log shows Cargo updating the git repository with no prompt and no authentication error:
Updating git repository `https://gitlab.com/org/proj/repo`
Compiling flasher v0.4.2 (https://gitlab.com/org/proj/repo#a1b2c3d4)
If it still fails, check these in order: the host in .git-credentials matches the dependency URL; .cargo/config.toml is committed and at the project root, not inside src/; and the dependency project’s token allowlist includes your project.
Caching for faster builds
Private git dependencies are re-cloned on every job unless you cache Cargo’s registry. Add a cache keyed on Cargo.lock:
cache:
key:
files: [Cargo.lock]
paths:
- .cargo/registry
- .cargo/git
- target/
variables:
CARGO_HOME: $CI_PROJECT_DIR/.cargo
Setting CARGO_HOME inside the project directory is what makes the cache paths work; by default Cargo stores everything under ~/.cargo, which GitLab cannot cache.
Summary
Three files, one line each: a credential store entry using CI_JOB_TOKEN, an HTTPS git dependency in Cargo.toml and git-fetch-with-cli = true in .cargo/config.toml. Add the token allowlist entry on the dependency project, and private crates build in CI with no deploy keys and no long-lived secrets.
For the Rust side of the story, see the introduction to Rust on this site.


