Skip to content

The Rust Engineering Handbook / Chapter 92

Dependency Governance, SBOMs, Advisories, and Reproducible Supply Chains

Govern third-party selection, build-time execution, inventory, provenance, updates, and emergency response as one release lifecycle.

The proposed parser crate saves an estimated three engineer-weeks. It also adds 31 resolved packages, two procedural macros, one build script that compiles native code, default features for targets the service never deploys, and a license the product has not approved.

“Popular crate” is not an approval decision. Neither is “no current advisory.” The durable artifact is a release-lifecycle record: why the capability is needed, which exact source and features may execute, who can change that source, how a release identifies what was built, and how an incident can replace it. Every shipped component therefore needs a recorded owner, acceptable license, minimal feature set, exact resolved identity, release-linked inventory and provenance, monitored advisory path, and time-bounded exception process.

Make deletion the first alternative

The lowest-risk dependency is absent. Before evaluating a crate, ask whether the requirement can be removed, delegated to a service boundary, implemented with the standard library, or met by a smaller interface. This is not an argument to rewrite mature cryptography, parsers, or protocols. A well-maintained specialist implementation can be safer than local code. The decision compares total ownership: security expertise, correctness history, update work, transitive graph, compile time, binary size, platform support, and incident response.

Count resolved packages and build-time targets, not only direct manifest lines. One convenience crate may bring a wide graph under default features. Use cargo tree -e features to explain why each feature is active and cargo metadata --locked --format-version 1 to capture machine-readable resolved identities and target kinds. Inspect target-specific dependencies for every supported platform; a Linux-only review does not approve the Windows or embedded graph.

Avoid vanity metrics. Download count does not prove maintainership, security, API fitness, or provenance. Evaluate release cadence without demanding constant churn, responsiveness to serious reports, bus factor and transfer history, ownership protections, repository and registry identity, review discipline, CI coverage, MSRV policy, SemVer practice, unsafe and native surface, and the ability to fork or replace the component. Record evidence and date because health changes.

The funnel is only the entry decision. Read the lower lane from the lockfile through verified acquisition, isolated build, inventory, provenance, signing, deployment, and advisory response; each box answers a different question, and none can substitute for the others.

Dependency-governance lifecycle. A candidate crate narrows through need, maintainer and release health, license, features and targets, and source and execution-surface review to an approved pin. The release lane then joins a lockfile, verified source or vendor tree, isolated build, SBOM and provenance, signed artifact, deployment, and an advisory loop through triage, mitigation, rebuild, and reissued inventory. Red side paths mark registry or Git acquisition, build scripts, procedural macros, native toolchains, CI runners, and signing keys as distinct privileged surfaces.
Approval is one checkpoint in a continuing custody chain: inventory says what shipped, provenance says how it was built, and neither alone establishes trust or incident readiness.

Approve a capability, source, and feature set

An approval should name the direct dependency, business purpose, owner, version policy, source, license conclusion, enabled features, supported targets, unsafe/native/build-time execution, alternatives considered, transitive concerns, review evidence, and renewal date. “Crate X approved” is too broad: the same package with all features, a Git branch, a new major version, or a newly introduced build script is a different risk.

Disable default features when they are broader than the required capability, then enable the narrow set explicitly. Feature minimization reduces code and sometimes build execution, but Cargo features are additive within a resolved package graph. Another dependency may re-enable a feature. Verify the final graph, not the local manifest intention. Exercise supported feature and target combinations in CI; a minimized graph that nobody tests transfers risk from supply chain to integration.

License review is a release obligation, not a one-time string match. Evaluate package metadata against actual included source, generated or vendored files, native components, notices, linking model, distribution method, modifications, and product obligations. Legal acceptance is organizational authority; an automated deny/allow list is evidence and enforcement, not legal advice.

Registry dependencies normally provide immutable published archives identified by version and checksums in the lockfile. Git dependencies can be legitimate for an unreleased fix or controlled source, but pin a full revision, record why a registry release is unsuitable, monitor repository access and deletion risk, and set an expiry or upstreaming plan. A branch or tag is mutable naming, not a release identity. Path dependencies shift integrity and inventory to the enclosing repository and build context.

Treat the build as privileged code execution

Cargo build scripts compile and run on the build host before the dependent package is built. They can inspect environment variables, execute programs, read accessible files, and emit compiler/linker instructions. Procedural macros also execute compiler-host code during compilation and carry similar security concerns. Their output affects the program even when their packages are absent from the runtime call graph.

Review build.rs, proc-macro packages, native tool downloads, code generators, package-manager hooks, linker inputs, and CI actions as execution surfaces. Do not expose production credentials, broad repository tokens, signing keys, cloud metadata, or unrestricted deployment authority to an ordinary dependency build. Use isolated, short-lived workers; minimal read-only source inputs; explicit network policy; pinned toolchains and base images; separate build and signing identities; and auditable artifact promotion.

An offline build proves that inputs were locally available, not that they were trustworthy. A sandbox limits consequence, not malicious output. Code review improves understanding, not immutable identity. Combine controls: review the identified source, verify acquired bytes, constrain execution, preserve provenance, and test the resulting artifact.

The fixture’s teaching policy rejects unreviewed execution:

if (request.build_script || request.proc_macro) && request.execution_review.is_none() {
    return Err(PolicyError::UnreviewedBuildExecution);
}

That check is intentionally incomplete. Production approval must discover transitive build scripts and procedural macros from the resolved graph; trusting a requester to declare them recreates the blind spot.

Lockfiles, vendoring, and registries solve different problems

Cargo.toml describes acceptable requirements. Cargo.lock records the exact resolution Cargo selected, including versions and source identities. Commit the lockfile for applications and other released artifacts; for libraries, the repository lockfile stabilizes local and CI work even though downstream consumers perform their own resolution. Run CI and release commands with --locked so an absent or stale lockfile fails instead of silently selecting a new graph.

--offline prevents network access but may choose among locally cached material if resolution is not locked. --frozen combines locked and offline behavior for commands that support it. These flags do not pin the Rust toolchain, system linker, native libraries, environment, timestamps, or build script behavior.

Vendoring copies registry and Git dependency sources into a controlled directory and configures Cargo source replacement. It improves availability, inspection, and isolated builds. It also creates a mirror that must be updated, scanned, licensed, and protected. Cargo assumes a replacement source contains the same code as the original; modify a dependency through an explicit [patch] or path source with its own identity and review, not by silently editing vendor content. Preserve Cargo’s checksum metadata and verify the vendored build with the exact lockfile.

A private registry can enforce organizational publishing and retention policy. It is not automatically safer than a public registry: account compromise, namespace confusion, malicious insiders, proxy behavior, and stale mirrors still matter. Configure registry identity explicitly, use scoped short-lived credentials, prevent unintentional publication, and record which source supplied every package.

An SBOM answers “what,” not “can we trust it?”

A software bill of materials records components and supply-chain relationships in a product. Generate it from the final resolved graph and shipped artifact process, not from a handwritten direct-dependency list. Include package name, version, source or supplier, checksums or other identifiers, dependency relationships, licenses where supported, and a unique product/release identity in an interoperable format such as SPDX or CycloneDX according to the receiving contract.

Rust binaries may statically link code, while package managers and container layers add non-Rust components. Decide the inventory boundary: Cargo packages, native static and dynamic libraries, operating-system packages, runtime images, embedded assets, build tools, and generated code may all matter to response. Validate SBOM completeness against cargo metadata, linker/package evidence, and the actual release artifact. Preserve the generator name and version because tool interpretation changes.

An SBOM does not prove source quality, absence of malicious code, exploitability, or reproducibility. It makes questions and response faster: which released artifacts contain an affected identity, where are they deployed, who owns remediation, and what replacement release supersedes them? Access policy may differ for public and internal inventories, but responders need durable retrieval even when the normal registry or CI system is unavailable.

Provenance answers how an artifact was produced: source identity, builder identity, invocation and parameters, inputs, dependencies, and resulting digest. Sign an attestation only from a protected identity after the artifact digest is known. Keep signing authority separate from mutable build steps. A signature proves that the named key endorsed bytes; trust still depends on key custody, authorized workflow, transparency or audit evidence, revocation, and verifier policy.

Reproducibility is a diagnostic security property

A reproducible build produces bit-for-bit identical output from declared inputs under a specified environment. It can reveal undeclared inputs and make independent comparison possible. It does not prove that matching source is benign, and one matching rebuild does not prove every distributed artifact followed that process.

Start with a narrower goal: clean build from locked sources without network access. Pin the Rust toolchain, target, Cargo configuration, native compiler/linker, system libraries, environment, locale, timezone, source timestamps, and container or host image. Eliminate embedded current times and absolute workspace paths where feasible. Ensure build scripts write only expected outputs and do not query network or host state. Compare final artifact digests from independent clean workers, then investigate differences using sections, symbols, archive metadata, and embedded paths.

Some products cannot immediately achieve bit reproducibility across hosts. Record the variance instead of claiming reproducibility: perhaps code sections match while signatures or package timestamps differ. Sign after comparison when the signature format is nondeterministic. Reproducibility should be a measured property with an environment recipe and result, not a label attached to Cargo.lock.

Advisories begin an investigation

Monitor RustSec and relevant OSV, registry, vendor, native-library, toolchain, operating-system, and private disclosure channels. An advisory match identifies a package/version relationship; it does not alone prove reachable exploitation or acceptable risk. Conversely, no published advisory is not evidence of safety.

For each alert, preserve the advisory revision and time, map it to released artifact identities and deployments, determine affected features/targets/code paths, assess attacker prerequisites and consequence, identify compensating controls, choose an owner and deadline, and document the decision. Reachability can prioritize work but should not silently suppress a vulnerable component: dynamic calls, FFI, build-time execution, and configuration may evade simple analysis.

Prefer a narrow compatible update, re-run the complete quality and security gates, regenerate SBOM and provenance, sign a new artifact, deploy through normal rollback-capable promotion, and verify removal from inventory. When upstream has no release, options include disabling a feature, removing the dependency, isolating the path, applying an explicit reviewed patch, pinning a vetted fork, or temporarily blocking attacker reach. Every exception needs scope, rationale, owner, compensating control, approval, expiry, and a tested exit.

Emergency speed comes from rehearsal. Maintain an out-of-band maintainer and security contact path, a way to query SBOMs by package and version, an isolated patch lane that still preserves review and tests, signing-key recovery, rollback, customer communication criteria, and evidence retention. Do not bypass provenance during an incident; that creates a second supply-chain uncertainty while resolving the first.

Use a regular cadence for grouped routine updates so drift stays small, plus immediate handling for actively exploited or high-consequence issues. Update one or a few related packages at a time when possible, inspect lockfile and feature-graph differences, and test the deployed behaviors. Freezing dependencies forever accumulates risk; continuous indiscriminate updates transfer it into production.

Write the direct-dependency approval policy

Create a policy with these required fields and gates:

  1. Request: capability, business owner, alternatives, expected lifetime, supported targets, performance and MSRV constraints.
  2. Identity: exact package/source, version range, resolved pin, registry or full Git revision, and namespace/ownership verification.
  3. Health: maintainers, release and disclosure practice, ownership changes, review/CI evidence, replacement feasibility.
  4. Rights: license decision, notices, native/generated content, distribution obligations, approving authority.
  5. Surface: enabled features, transitive graph, unsafe/FFI, native code, build scripts, proc macros, network/tool execution.
  6. Controls: lockfile, acquisition verification, build isolation, SBOM/provenance coverage, advisory monitors, update owner.
  7. Decision: approver, evidence links, date, conditions, renewal trigger, and rejection rationale.
  8. Exception: exact scope, risk acceptance, compensating controls, named owner, expiry, removal or upstream plan.

Define material-change triggers that reopen approval: source or owner transfer, new build-time execution, license change, major version, significant transitive growth, default-feature expansion, native component, unsupported MSRV, serious advisory, or dormant maintenance. Automate discovery and enforcement, but keep accountable humans for contextual need, legal authority, and risk acceptance.

Supply-chain review questions

  • Can every direct dependency defend its necessity against deletion, a smaller crate, or a service boundary?
  • Does approval name exact source, version policy, features, targets, unsafe/native code, and build-time execution?
  • Are maintainer health, ownership transfers, licenses, and replacement options reviewed with dated evidence?
  • Does CI verify the final feature and target graph rather than manifest intent?
  • Are registry and Git sources pinned, acquired with integrity evidence, and protected against namespace confusion?
  • Do build scripts, proc macros, native tools, and CI actions execute without valuable ambient credentials?
  • Do locked, isolated builds fail on undeclared network or resolution changes?
  • Does the SBOM identify the actual release, transitive Rust graph, and relevant non-Rust components?
  • Does provenance bind source, builder, inputs, invocation, and artifact digest under protected signing policy?
  • Is reproducibility tested from a written environment recipe with differences investigated?
  • Can an advisory be mapped quickly to artifacts, deployments, reachability, owner, deadline, and superseding release?
  • Are routine updates small and tested, emergency patches rehearsed, and exceptions time-bounded?

Dependency governance is not a gate that ends at merge. It is a lifecycle from need through selection, build, inventory, release, monitoring, replacement, and retirement. Those records belong beside capacity, profiling, observability, resilience, and security evidence whenever a team decides whether one named release is ready.

Sources and version notes

The fixture targets Rust 2024, declares MSRV 1.85, and uses no third-party packages, build script, or procedural macro. Its approval function demonstrates a few fail-closed gates; it is not an SBOM generator, advisory scanner, provenance system, license decision, maintainer assessment, or reproducible-build proof.