Open Source Security

cargo-audit and cargo-deny: A Real Workflow

A senior-engineer-grade workflow for using cargo-audit and cargo-deny together, with realistic policy decisions and the mistakes teams repeat.

Shadab Khan
Security Engineer
7 min read

cargo-audit and cargo-deny are the two tools that every serious Rust shop is running by now, and most teams are running them wrong. Not dramatically wrong, just in the thousand small ways that turn security tooling into noise: policies too strict to ship, policies too loose to catch anything, exceptions that outlive the problems they were supposed to work around, and CI jobs that nobody believes because they fail for reasons unrelated to security. Getting these two tools right is mostly a matter of treating them as a workflow, not a checkbox.

What does each tool actually do, and how do they overlap?

cargo-audit reads the RustSec advisory database and tells you which of your dependencies have known vulnerabilities or yanked versions. Its job is narrow and focused: known-bad version detection. cargo-deny is a broader policy engine that checks licenses, dependency sources, banned crates, duplicate dependencies, and, yes, RustSec advisories. Its job is enforcement of whatever supply chain policy you encode in its configuration file.

The overlap is advisory checking, which both tools can do. The clean division of labor is: use cargo-audit for fast, frequent advisory scans, and use cargo-deny for the broader policy gate. Running both is not wasteful; they serve different roles in the workflow. Treating cargo-deny's advisory check as a superset and skipping cargo-audit is tempting and sometimes defensible, but I usually keep both because cargo-audit is lighter weight and runs faster in tight feedback loops like pre-commit.

What should a minimal-but-real deny.toml look like?

A deny.toml that catches real problems without drowning the team in noise has four sections that matter. Advisories should deny known vulnerabilities with an explicit exception list that names specific advisory IDs and has an expiration date for each exception. Licenses should have an allowlist of acceptable licenses, not a denylist of bad ones, because the universe of good licenses is smaller and more stable than the universe of bad ones. Sources should restrict crates to a specific set of registries, usually just crates.io and your internal mirror. Bans should name specific crates you do not want in the graph, which is mostly useful for deprecated crypto libraries or crates you know conflict with your runtime.

The section that teams skip, and shouldn't, is duplicate checking. Multiple versions of the same crate in your dependency graph is not always a security problem, but it is frequently a symptom of poorly-maintained dependencies, and keeping the count low forces you to pay attention when new duplicates appear. Set the duplicate check to warn initially and tighten it once you have the graph under control.

How do you handle the flood of findings the first time you run this?

The first run of cargo-deny against a real codebase produces a list of findings that will exhaust an afternoon. The mistake most teams make is trying to get to zero on day one, which requires either unrealistic cleanup work or a pile of permanent exceptions that the team forgets about. The mistake some teams make is ignoring the first run and deferring the effort, which means the tooling never actually lands.

The productive pattern is a time-boxed triage. Give yourself a day to categorize findings into: fix now (things that are quick), defer with expiration (things that need real work and should be revisited), and permanently acceptable (things that reflect conscious design decisions). Every deferred item gets a ticket with an owner and a date. Every permanent exception gets a comment in the config explaining why. After triage, your CI policy enforces the current state, and findings that show up after that point are, by construction, new problems.

When should cargo-audit run, and when should it fail the build?

cargo-audit is cheap enough to run on every CI build. Whether it should fail the build depends on the advisory's severity and your release cadence. For most shops the right policy is: advisories marked as security-critical fail the build unconditionally, advisories marked as lower severity warn for a grace period and then fail, and yanked-only advisories (no security content, just withdrawn) warn indefinitely.

The grace period matters because RustSec advisories sometimes land before a fixed version is available. Blocking all builds on a fresh advisory with no fix available forces the team to either downgrade the dependency, which may or may not be possible, or add a config exception under pressure, which is how permanent exceptions get created. A 48-to-72-hour grace period with visible notification gives the team time to plan a response without an artificial deadline.

How do you keep exceptions from rotting?

Exceptions are the main failure mode of both tools in long-running projects. The exception was added for a real reason, the reason was documented somewhere that is now lost, and the exception has outlived the vulnerability it was meant to bypass. Six months later somebody asks why cargo-deny is permitting a known-critical advisory and nobody knows.

The fixes are cultural and mechanical. Culturally, treat exceptions as technical debt with the same urgency as any other debt. Mechanically, require every exception to have an expiration date and a ticket reference, enforce the expiration at CI time (the build starts failing when the exception expires), and review the full exception list at a regular interval. A quarterly exception audit takes an hour and surfaces more useful signal than most security metrics.

How does this fit with cargo-vet and the broader supply chain story?

cargo-vet is a different kind of tool: it tracks human-reviewed audits of crate versions and enforces that every crate in your graph has been audited by you or by someone you trust. It complements cargo-deny rather than replacing it. Where cargo-deny enforces machine-readable policy, cargo-vet enforces human judgment.

Running cargo-vet at production scale is a significant organizational commitment. Google, Mozilla, and a handful of other large Rust consumers have made it work by pooling audits across teams and organizations. For most shops, adopting the full cargo-vet model is overkill, but the lightweight use case of auditing your most sensitive dependencies manually and recording those audits in a shared format is worth the effort. If you are building a product whose security properties are load-bearing, invest in cargo-vet for the crates that carry the weight.

How should the tooling integrate with your dependency update flow?

Dependabot and Renovate both work fine with Rust, and the integration point worth thinking about is the interaction between the update bot and your policy tooling. An auto-generated PR that bumps a dependency and fixes a cargo-audit finding should be easy to merge; an auto-generated PR that introduces a new cargo-deny violation should block until a human reviews it. This requires your CI to actually run the policy tools on PR branches, which sounds obvious but is surprisingly often skipped in favor of running them only on main.

The other integration worth mentioning is the advisory-to-PR latency. When a new RustSec advisory drops, you want the update bot to pick it up fast. Renovate and Dependabot have different cadences for advisory refresh; Renovate is generally faster. If your threat model cares about the window between advisory publication and remediation, configure the update cadence accordingly rather than accepting the default.

How Safeguard.sh Helps

Safeguard.sh generates and maintains cargo-deny policies that match your stated risk tolerance, tracks exception expirations across repositories, and surfaces the cases where a time-boxed exception is about to lapse before your team notices. We correlate cargo-audit findings across your organization so you can see the dependency graph view rather than the per-repo view, and we flag the drift between policy and reality when teams quietly add exceptions to ship. The result is advisory and policy tooling that actually gets used, rather than falling into the pile of half-maintained CI jobs every engineering organization accumulates.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.