BytesafeDependency Firewall

Cargo

BETA

Point cargo at your firewall as a sparse registry. Source replacement, credential providers, publishing and yanking, CI, and troubleshooting.

A Cargo firewall is a crate registry that your Rust projects install from instead of crates.io. The standard cargo command builds through it, with no plugin or wrapper involved: it asks the firewall which versions of a crate exist and downloads the crate file from it, and your rules decide what the firewall is willing to answer. The operations it covers are listed in Client operations.

The firewall fetches from crates.io on your behalf, so nothing you depend on has to move. Setup is one config file in the project, and Cargo.toml does not change: the dependencies you already have start going through the firewall as they are.

The examples use <namespace-id> and <firewall-id> placeholders. Replace them with IDs, not with namespace or firewall names. The firewall's Setup tab shows these snippets with both values already filled in.

Index endpoint

sparse+https://eu-sov-1.bytesafecloud.eu/v1/<namespace-id>/cargo/<firewall-id>/

Two details in that URL are easy to lose and both stop it working:

  • sparse+ tells cargo to read the index over plain HTTP requests, which is what the firewall serves. Without it, cargo tries to clone a git index instead.
  • The trailing slash. Cargo builds every request by appending to this URL, so dropping the slash replaces the last part of the path instead of extending it.

The endpoint needs Rust 1.74 or newer, the release where cargo learned to authenticate to a registry for reads as well as publishes. Older cargo does not send your token at all, so every request comes back 401 with nothing in the message to explain why, and no configuration changes that.

Cargo authenticates with an access token, which it sends exactly as you stored it, with no Bearer prefix. That is the Cargo registry convention rather than a Bytesafe one.

A Cargo firewall created without upstreams gets crates-io (https://index.crates.io) as its default upstream; point it elsewhere on the firewall's Upstreams tab. One thing to get right when you do: a cargo upstream's endpoint is the registry's index, not its web API. The firewall reads the registry's config.json from the index and takes the download host from that file, which is how a crates.io download ends up at static.crates.io.

Configure the project

Put this in .cargo/config.toml in the project, and commit it:

[registries.firewall]
index = "sparse+https://eu-sov-1.bytesafecloud.eu/v1/<namespace-id>/cargo/<firewall-id>/"

[source.crates-io]
replace-with = "firewall"

[registry]
global-credential-providers = ["cargo:token"]

firewall is a name you pick for the registry entry, not a cargo keyword. It is the name you pass to --registry later, and the name in the credentials file below.

All three blocks are needed, and each does a different job:

  • [registries.firewall] introduces the firewall to cargo, and says where its index is.
  • [source.crates-io] replace-with is the line that sends your existing dependencies there. It tells cargo that anything it would have fetched from crates.io comes from the firewall instead, which is why Cargo.toml needs no edit. Cargo.lock still records crates.io as the source, so the same lockfile works for someone building without the firewall.
  • [registry] global-credential-providers lets cargo use the token you store in the next step. Cargo will not authenticate to a registry without it, and fails with authenticated registries require a credential-provider to be configured.

There is a tempting shortcut that quietly does the wrong thing: pointing [registries.crates-io] index at the firewall. Cargo ignores that and fetches the real crates.io index, so the build succeeds having skipped every rule. Replacing the source is the only setting that actually reroutes crates.io.

Then add the token. Cargo keeps credentials in a separate file, so config.toml stays committable:

# ~/.cargo/credentials.toml
[registries.firewall]
token = "<your token>"

Store the token on its own, with nothing in front of it: cargo sends the value exactly as written. Keep credentials.toml out of version control.

Verify

cargo fetch

That downloads every dependency in the lockfile, which is enough to prove both halves work: cargo found the versions through the firewall and got the files from it. If a rule matched along the way, it has a firewall log entry:

bsfw logs --firewall <firewall-id>

CI

Use a Service Access Token (SAT) instead of a PAT. A SAT identifies the CI system rather than a person. Create one as described in Access Tokens and store it as a CI secret.

Rather than writing credentials.toml in the pipeline, pass the token in the environment variable cargo reads for a named registry:

export CARGO_REGISTRIES_FIREWALL_TOKEN="$BYTESAFE_TOKEN"

cargo build --locked

The variable name comes from the registry name in config.toml, uppercased: a registry named firewall reads CARGO_REGISTRIES_FIREWALL_TOKEN. It is the cargo:token provider that reads it, which is the second reason that line belongs in config.toml.

To correlate firewall log entries from one pipeline run, append :: and the run ID to the token. The firewall validates the token before the suffix and records the suffix as the interaction ID. Use the same value for every request in the run.

One trap in containers: if the image sets CARGO_HOME, and the official rust images do, then a config written to ~/.cargo/config.toml is ignored, with no error. Cargo just goes to the real crates.io. Write the config under $CARGO_HOME, or keep it in the project at .cargo/config.toml.

What the firewall serves

PathPurpose
/config.jsonRegistry settings, which the firewall writes itself: it points downloads back at the firewall, and tells cargo the registry needs a token
/{prefix}/{crate}The crate's index file, one line per version. Versions rules remove blocked versions before the firewall returns it
/api/v1/crates/{crate}/{version}/downloadCrate download. Download-phase rules run here
/api/v1/crates/newcargo publish. Upload-phase rules run here
/api/v1/crates/{crate}/{version}/yank and /unyankcargo yank and cargo yank --undo. Upload-phase rules run here

{prefix} is a directory layout cargo works out from the crate name itself, which is why cfg-if is requested as cf/g-/cfg-if. Each surviving version's line is passed through unchanged, so anything in it the firewall does not use is preserved rather than dropped.

Owner management and search are not part of a firewall endpoint. cargo owner and cargo search answer 501 Not Implemented, with a message naming what is supported instead. Neither one moves a package, so there is no rule that could apply to them; see Client operations.

How rules apply

Each rule runs on one execution phase, and the phase decides how a block reaches the developer:

  • Versions rules take the version out of the index before cargo's resolver reads it, so it is never picked in the first place.
  • Download rules let the version be picked and then refuse the crate file.
  • Upload rules apply to publishing: cargo publish, and yanking or unyanking a version.

Selectors match the crate name and the version as the registry published them. Crate names match case-insensitively, so a rule naming inflector also matches the crate's canonical Inflector spelling. Hyphens and underscores are not folded: cfg-if and cfg_if are different crates, and a selector must be written the way the crate is actually named.

versionRange selectors use semver, the same notation Cargo.toml uses, including comma-separated AND terms such as ">=1.2, <1.5".

Yanked crates

A crate version is yanked when its publisher withdraws it from new resolution without deleting it. The deprecated selector function matches yanked versions, which lets you block them for existing lockfiles too, not just for new resolution.

Delay rules and publication time

A delay rule holds a version back until it has been public for a while, which means the firewall has to know when it was published. Cargo indexes can carry that date per version, and crates.io does, including for crates released long before the field existed.

Worth checking before you rely on one against a private registry: if its index leaves the date out, the firewall has no age to compare against, and the rule lets the version through rather than holding it back.

Selector function support

Three selector functions are ecosystem-specific and not available for Cargo. See the selector function matrix:

  • install-scripts is not available. It is implemented for npm, PyPI, and Composer.
  • unlisted is implemented for NuGet only, and trust-downgrade for npm only.

Everything else applies: vulnerabilities, vulnerability-fix, deprecated, malware, secrets, and license.

Publishing

A Cargo firewall accepts cargo publish, cargo yank, and cargo yank --undo. First set a publish target: the upstream that should receive them. Without one the firewall is install-only and answers firewall has no publish target configured.

Replacing the source only reroutes dependency fetches, so for anything else cargo has to be told which registry you mean:

cargo publish --registry firewall
cargo yank --registry firewall --version 1.0.0 <crate>

Upload rules run before anything is sent onward. This is where secrets scanning applies, so a crate carrying a leaked credential is stopped before it leaves your organization. Yanks and unyanks are checked as well, since both change what other people's builds resolve to.

The firewall stores nothing of its own. A publish that passes the rules is forwarded to the publish target, and that registry's reply is passed straight back, so any warnings it produces still reach the person publishing. Where the publish goes is read from the target registry's own settings, so a registry that offers no publishing endpoint cannot accept one: the firewall then answers the upstream registry advertises no web API and cannot accept publishes or yanks.

What a blocked install looks like

A versions rule removes the version from the index file, so cargo's resolver never sees it. Cargo reports that no version matches your requirement, and lists the ones it did find. Nothing in that message mentions the firewall, which is worth knowing before you go looking for a broken dependency.

A download rule lets cargo pick the version and then refuses the file. The firewall answers 404 and cargo prints its message:

serde@1.0.200 is blocked. Check firewall log entries for request <request-id> for details

An upload rule blocks a publish with 403 and <crate>@<version> upload blocked, and a yank or unyank with yank of <crate>@<version> blocked. Each of these names a request ID; take it to the logs to see which rule fired. See Investigate a blocked install.

Troubleshooting

The [registry] global-credential-providers line is missing from the config cargo actually read. Every cargo version needs it, and without it nothing else on this page works.

Either cargo is older than 1.74, in which case it never sends a token at all and there is no configuration that changes that, or the token in credentials.toml is unset, expired, or revoked. Check cargo --version first.

One 401 is normal and not an error: cargo probes config.json unauthenticated, sees the firewall require authentication, and retries with the token. Only a 401 that ends the command is a failure.

If a crate that should match a block rule resolves with no matching log entry, cargo did not read the config you edited. Check CARGO_HOME: when it is set, ~/.cargo/config.toml is ignored. Then check that the config uses [source.crates-io] replace-with rather than a [registries.crates-io] index override, which cargo ignores. The firewall log is the authoritative check: a fetch that produced no entries for the crate did not go through the firewall.

Source replacement routes dependency resolution, not publishing. Pass --registry firewall on publish, yank, and any other command that contacts the registry directly.

Most likely a versions rule filtered every version that satisfies your requirement. Check the firewall logs before assuming an upstream problem. Also check the spelling: hyphens and underscores are distinct in crate names, and the firewall serves the name the registry uses.

On this page