blog.rust.gay

Cargo Deny Workspace

Introduction to Cargo Deny in a Workspace

cargo-deny is an essential Cargo plugin developed by Embark Studios, designed to lint a project's dependency graph against critical policy requirements. These requirements include checking for known security advisories (advisories), enforcing dependency bans (bans), validating license compliance (licenses), and ensuring dependency source trust (sources). The primary challenge in a Rust workspace is maintaining policy consistency—applying a single, unified policy across multiple member crates—without introducing unnecessary complexity, false positives, or conflicting policy definitions between individual packages. A fragmented policy strategy, where each crate dictates its own security posture, creates organizational risk and severe maintenance overhead.

The core principle of cargo-deny is its ability to start at the manifest provided and walk up through parent directories, checking for deny.toml, .deny.toml, .cargo/deny.toml, or .config/deny.toml, using the first file found. For most organizational setups, placing the deny.toml at the top-level workspace root is the clearest, most maintainable, and most scalable configuration strategy. This single file allows all member crates to automatically inherit the unified supply-chain policy simply by invoking cargo deny check from within the workspace directory, completely avoiding the costly and error-prone practice of manually copying policy settings into every individual crate manifest. This single point of control is fundamental to governance.

Setting Up and Initiating Checks

To begin, one must install the plugin using cargo install --locked cargo-deny and then initialize the configuration via cargo deny init. The subsequent cargo deny check executes the comprehensive primary policy checks. It is vital to understand that this command exits with a specific bitset indicating which checks failed: 0x1 for advisories, 0x2 for bans, 0x4 for licenses, and 0x8 for sources. This bitmask capability is not merely convenient; it is critical for CI/CD scripting, allowing automation pipelines to precisely distinguish between a critical vulnerability failure, a deliberate policy violation, or a simple source registry issue, ensuring failure handling is targeted and correct.

Scoping the Workspace Dependency Graph

The dependency graph analyzed by cargo-deny is not fixed; it is context-dependent on the manifest being analyzed. If the check is run using a virtual workspace manifest, all member crates are treated as roots, leading to a holistic view of the entire graph. Conversely, when run within a specific package manifest, only that package is treated as the root, and the tool only analyzes its direct and transitive dependencies within the workspace.

For comprehensive supply-chain analysis across the entire workspace—the highest standard of security—the --workspace flag must be utilized. This ensures that all workspace members are considered as roots, guaranteeing that an advisory or policy violation in any member crate is detected, regardless of whether it was a direct dependency of the target package. This comprehensive scoping protects the organization against deeply nested, non-obvious vulnerabilities.

Advanced Scoping Options

In deny.toml, the [graph] table offers fine-grained control over this complex scoping:

  • targets: By allowing the definition of exact triples (e.g., i686-unknown-linux-gnu, wasm32-unknown-unknown), the policy can be constrained to only apply where the build environment is relevant. This is vital when a workspace targets disparate architectures or platforms, preventing irrelevant checks from generating noisy warnings or, worse, silently masking critical issues due to over-generalization.
  • exclude: Provides the means for explicit exclusion of specific packages from the graph, which is useful for temporary internal crates under active development or for platform-specific dependencies irrelevant to the core policy scope.
  • all-features, no-default-features, features: These options ensure the policy is evaluated against the intended usage defined by the build target, overriding default Cargo.toml settings and preventing policies from being bypassed by minimal default feature declarations.

Furthermore, the CLI's --manifest-path option allows users to narrowly focus the check on a specific manifest context within a large repository, optimizing debugging efforts without sacrificing global policy visibility.

Managing Workspace Policy Knobs

A robust, production-grade workspace configuration goes beyond just using the tool; it requires leveraging specific policy knobs designed to manage the unique challenges of multi-crate environments and feature lifecycles.

Feature Bloat Prevention and Defaults

The phenomenon of ambient workspace feature union—where a dependency inherits all features enabled across all members—can lead to silent feature bloat. For instance, a package like serde or clap might silently enable a "full" convenience set across the entire workspace without a policy finding it. To combat this risk, configuration knobs like workspace-default-features (set in [bans]) and external-default-features allow setting default lint levels specifically for workspace-internal versus external dependencies. Setting external-default-features = "deny" ensures that third-party libraries adhere to the strictest requirements by default. In contrast, workspace-default-features can be configured to allow or warn to accommodate the rapid, sometimes less polished, development practices inherent to internal team projects, requiring deliberate governance.

Dependency Management Specifics

The [bans.workspace-dependencies] table provides the ultimate set of controls for internal policy enforcement:

  • duplicates: Enforces that direct workspace dependencies use workspace = true when multiple instances of the same package appear across the workspace. This prevents subtle version conflicts that cargo tree --duplicates highlights but that cargo-deny can automatically flag for resolution.
  • unused: Flags dependencies declared in [workspace.dependencies] that are never actually referenced by any crate in the workspace. This ensures the central dependency list remains clean, accurate, and reflective of actual product usage, which is vital for onboarding new developers.
  • include-path-dependencies: Critically, this controls whether dependencies resolved via local path (e.g., path = "../other_crate") are subjected to the same policy checks as registry dependencies. This is a non-negotiable detail for internal code review, ensuring that internal projects adhere to the same standards as external crates.

Private Licenses and Exceptions

License enforcement often touches internal, non-published crates. The [licenses.private] section, particularly the ignore = true setting, provides a necessary mechanism for policy adherence without unnecessary friction. This ensures that private workspace components—which might use internal tooling or specialized licenses—do not trigger license failures simply because they are not listed in public registries, which is a verified, practical exemption that maintains governance without impeding internal development.

Advanced Policy Deep Dives and Caveats

It is crucial to understand that cargo-deny configuration is tightly bound to the tool's version and the underlying Cargo metadata resolution mechanisms.

Advisory Unsoundness Policy Caveat

The behavior of [advisories].unsound (which controls whether advisories are checked against direct or transitive dependencies) is explicitly version-dependent. While documentation might suggest unsound = 'workspace' means only direct dependencies, community reports indicate that the actual behavior is complex and depends on how cargo metadata resolves the graph. Users must verify their cargo-deny version; if inconsistencies are found, consulting later, corrected releases (e.g., 0.24.0+) is safer than relying solely on outdated documentation or assumptions about the workspace setting. Note: If strict 'workspace' scoping is absolutely required, rigorous version checking is mandatory, as relying on transitive checks ('all') may introduce false noise.

License Policy Technical Limitation

A key, verified technical fact that must guide policy setting is the [licenses] section's inherent limitation in certain versions (e.g., 0.19.x): there is no deny field for explicit license prohibition. Policy is enforced solely by omission. If a license is not listed in the [licenses].allow list, it is denied by the tool's hardcoded default. This technical constraint means the policy cannot be inverted from "allow-all, deny-GPL" to "deny-all, allow-non-GPL" using a configuration knob. Therefore, the list must be meticulously curated, focusing on "real security-known-unsafe" advisories rather than just license identity, while retaining a broad, safe allow-list (e.g., including MIT, Apache-2.0, BSD-2-Clause) to minimize necessary maintenance toil.

Integrating Cargo Deny into CI/CD

Effective CI integration elevates deny.toml from a configuration file to version-controlled, reviewable policy code. While tools like the EmbarkStudios/cargo-deny-action simplify deployment, manual CI steps running cargo deny check (leveraging the bitset exit code) provide superior control over failure logic. Crucially, the cargo-deny sources check can enforce registry trust (unknown-registry = "deny") and Git repository trust (unknown-git = "deny"), preventing the accidental introduction of dependencies from untrusted or unverified external sources—a crucial security measure that goes far beyond basic license compliance.

Conclusion

A modern, high-integrity Rust workspace requires more than just initial configuration. It necessitates a nuanced, version-aware, and deeply detailed configuration that scopes the dependency graph precisely using [graph], manages internal policy with [bans.workspace-dependencies], and explicitly acknowledges the technical limitations of the tool, such as the absence of a deny field in [licenses]. By integrating these advanced concepts—from feature bloat prevention and precise scoping to version-specific advisory policies—the workspace transforms from a simple dependency checker into a cohesive, verifiable, and robust supply-chain governance mechanism. This ensures that the integrity of the entire project is upheld by a single, auditable, and scalable deny.toml.

Sources


← back to posts