Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 064 / 100

cargo audit and supply chain security: a practical guide

Keep dependencies safe.

Expona Admin7 min read

Introduction

Dependency vulnerabilities have caused more production incidents than we'd like to admit. Whether it's a cryptographic flaw in a transitive dependency or a malicious package slipping into your supply chain, the risk is real and often overlooked until it becomes a crisis. This guide covers cargo audit and supply chain security: a practical guide for teams using Rust in production, with proven patterns we've refined across trading platforms, SaaS backends, and high-scale services.

By the end of this article, you'll know exactly how to scan for known vulnerabilities, enforce license policies, review dependencies before they reach main, and lock versions responsibly. We're not talking theory—these are techniques that have caught real issues before they shipped.

Understanding cargo audit and the advisory database

The first line of defense is cargo audit, which checks your project's dependencies against the Rust Advisory Database. This database is maintained by the Rust Security Response Working Group and contains known security issues in published crates.

Running it is straightforward:

rust
cargo audit

On a clean project, you'll see ✓ Auditing 42 crates: all safe. On a project with vulnerabilities, you get a report like:

rust
Rustsec Advisory Database
=======================

0 vulnerabilities found in 42 crates, sourced from the RustSec Advisory Database

Crate:     openssl
Version:   0.10.42
Title:     OpenSSL X.509 Name handling
Date:      2024-01-15
ID:        RUSTSEC-2024-0001
URL:        https://rustsec.org/advisories/RUSTSEC-2024-0001
Severity:  high

Each advisory tells you the crate, the versions affected, the severity, and a link to details. Your job is then to update, patch, or accept the risk consciously. In production environments, we treat any "high" or "critical" severity as blocking—it goes in your CI/CD gate.

Integrating cargo audit into CI/CD

Don't run cargo audit locally and hope developers remember it. Bake it into your pipeline:

rust
# GitHub Actions example
name: Security
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo install cargo-audit
      - run: cargo audit --deny warnings

The --deny warnings flag turns informational advisories into CI failures. This forces the team to actively acknowledge and resolve them, rather than letting them accumulate.

cargo deny for licenses and supply chain policy

Vulnerabilities are one concern. Licensing and policy violations are another. A GPL dependency can unexpectedly pull in viral licensing obligations. An unmaintained or suspicious crate can silently ship with your binary. This is where cargo deny comes in.

First, install it:

rust
cargo install cargo-deny

Then initialize a config:

rust
cargo deny init

This creates a deny.toml file. Here's a real-world configuration for a SaaS backend:

rust
[advisories]
db-path = "~/.cargo/advisory-db"
db-urls = ["https://github.com/rustsec/advisory-db/raw/main/db.json.gz"]
vulnerability = "warn"
unmaintained = "warn"
yanked = "warn"
notice = "warn"

[licenses]
allow = [
    "MIT",
    "Apache-2.0",
    "Apache-2.0 WITH LLVM-exception",
    "BSD-2-Clause",
    "BSD-3-Clause",
    "ISC",
    "MPL-2.0",
]
deny = [
    "GPL-2.0",
    "GPL-3.0",
    "AGPL-3.0",
]
copyleft = "warn"
allow-osi-fsf-free = true

[bans]
multiple-versions = "warn"
wildcards = "warn"
skip = [
    { name = "openssl", version = "=0.10" },
]
skip-tree = [
    { name = "some-sys-crate", version = "=1.2" },
]

Run it:

rust
cargo deny check

This checks advisories, licenses, and bans. A failing report might look like:

rust
error: GPL-2.0 license detected
  ├─ some-gpl-crate v1.0.0
  └─ used by your-app

error: crate is unmaintained
  ├─ old-lib v0.1.0 (last commit 5 years ago)
  └─ used by your-app

warning: multiple versions of tokio
  ├─ tokio v1.35.0
  └─ tokio v1.30.0

The team then decides: update the transitive dependency, find an alternative crate, or explicitly allow it with documented reasoning.

Reviewing new dependencies before merging

The most effective security practice is prevention. Before a new dependency lands in your codebase, someone should review it. This doesn't mean reading all the source code, but it means checking:

  • Maintenance status: Recent commits, active issue resolution, and responsiveness from maintainers.
  • Download history: Consistent growth or sudden spike (potential compromise).
  • Author reputation: Is this from a known organization or a brand-new account?
  • Alternatives: Does a more mature, popular crate solve the same problem?

For example, if a PR adds some-random-crypto v0.1.0 as a dependency, a code review flag is appropriate. Check crates.io:

rust
cargo search some-random-crypto --limit 5

Then look at its repository, issue tracker, and download stats. If it's a one-person project with no tests and a cryptography focus, escalate it.

We've seen teams adopt a policy: any new external dependency over 1000 lines of code, or touching security-sensitive paths (crypto, auth, database), needs explicit approval. Add a DEPENDENCIES.md file to your repo:

rust
## Key Dependencies

### tokio v1.35
Async runtime. Widely used, actively maintained, backed by Tokio Foundation.

### serde v1.0
Serialization. De-facto standard in Rust. Production-proven.

### rustls v0.21
TLS without OpenSSL. Chosen to reduce C dependencies and simplify deployment.
Rationale: audited by Cure53, no system library required.

### hyper v1.0
HTTP client/server. Considered tonic for gRPC, but hyper covers both REST and gRPC.

This makes dependency choices explicit and auditable over time.

Pinning versions and lock files

Cargo.lock is often misunderstood. Here's the rule: always commit Cargo.lock for binary projects, never for libraries.

For a service or CLI, Cargo.lock pins every transitive dependency to an exact version. When you run cargo build on CI, it pulls the exact same versions your local machine used. This prevents supply chain surprises like:

rust
Local: tokio 1.35.0 works fine
CI: tokio 1.36.0 has a regression, build fails

Or worse, if a crate is compromised and pushed with a new version, your lock file ensures you don't auto-update into the malicious code.

When you do want to update dependencies, make it deliberate:

rust
cargo update

This updates your lock file to the latest semver-compatible versions and should trigger a full test run on CI. If you want to update a specific crate:

rust
cargo update -p tokio

This updates tokio to the latest within your Cargo.toml constraint (e.g., 1.35.x if you specified tokio = "1.35") and bumps the lock file.

For libraries, never commit Cargo.lock. Users of your library have their own lock files, and yours would constrain them unnecessarily. Use Cargo.toml to specify ranges:

rust
[dependencies]
serde = "1.0"
tokio = { version = "1", features = ["full"] }

This allows users to pull in compatible versions without conflicts.

Common mistakes

Ignoring warnings in cargo audit: Teams often run cargo audit locally, see warnings, and move on. Integrate it into CI with --deny warnings so every advisory is tracked and resolved.

Not reviewing transitive dependencies: You add one crate, but it brings in ten others. Use cargo tree to see the full dependency graph:

rust
cargo tree --depth 3

This helps you spot unnecessary deep chains or duplicates.

Wildcard versions in Cargo.toml: Writing tokio = "*" in a library defeats the purpose of version ranges. Use semantic versioning: tokio = "1" pins the major version but allows patches.

Forgetting to update deny.toml: New advisories are published weekly. Your deny.toml config is static. Make sure CI runs cargo deny fetch regularly to pull the latest advisory database, or set your CI to always run without a cached database.

Accepting every dependency without cost-benefit analysis: A small utility crate might pull in a dozen transitive dependencies. Before adding it, ask: is this worth the supply chain risk? Can we write the code ourselves, or use a more minimal alternative?

Key takeaways

  • cargo audit checks known vulnerabilities in your dependencies against the RustSec Advisory Database; integrate it into CI with --deny warnings to enforce compliance.
  • cargo deny enforces licensing policies, bans copyleft licenses if needed, and warns about unmaintained crates; configure it once and run it on every build.
  • Review new dependencies before merging: check maintenance status, download trends, and whether simpler alternatives exist.
  • Commit Cargo.lock for binaries and services; don't commit it for libraries. Lock files prevent auto-update surprises and lock out malicious versions.
  • Use cargo tree to understand your full dependency graph, including transitive dependencies.
  • Document why key dependencies were chosen; this makes future upgrades and alternatives easier to evaluate.
  • Treat high and critical advisories as CI blockers; low-severity items should still be tracked and prioritized.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles