security
May 01, 2026 · 8 min read · 0 views

Cargo-Deny: Securing Rust Supply Chain Dependencies

Master cargo-deny to audit dependencies, detect vulnerabilities, and enforce supply chain policies in Rust projects with practical examples and best practices.

Understanding Cargo-Deny and Why It Matters

Rust’s package ecosystem has grown exponentially, with crates.io now hosting hundreds of thousands of libraries. While this growth enables rapid development, it introduces significant security and compliance risks. Malicious packages, unmaintained dependencies, license violations, and supply chain attacks have become genuine threats to production Rust applications.

Cargo-deny is a powerful tool that audits your Rust project’s dependency tree, checks for known vulnerabilities, enforces license policies, and detects unusual patterns that might indicate compromised packages. Unlike basic cargo audit, which only checks against the RustSec advisory database, cargo-deny provides layered security controls for modern supply chain management.

If you’re building production systems in Rust, implementing cargo-deny should be non-negotiable. Let’s explore how to set it up and integrate it into your development and CI/CD workflows.

Why Cargo-Deny Matters for Your Projects

The Real Risks

  1. Vulnerability Exposure: Dependencies can contain CVEs that propagate through your entire application stack
  2. License Compliance: Accidentally including GPL code in proprietary software creates legal liability
  3. Abandoned Crates: Unmaintained dependencies leave you vulnerable to emerging attack vectors
  4. Typosquatting: Attackers publish packages with names similar to popular crates
  5. Yanked Versions: Published crates can be yanked from crates.io, breaking builds if not handled properly

Cargo-deny addresses all these concerns with a declarative, auditable approach.

Installing and Initializing Cargo-Deny

Installation

Start by installing cargo-deny as a development dependency or global tool:

# Install globally (recommended for CI/CD)
cargo install cargo-deny

# Or add to your project
cargo add --build cargo-deny

Verify installation:

cargo deny --version

Initialize Configuration

Generate a default configuration file:

cargo deny init

This creates a deny.toml file in your project root with sensible defaults for four main checks:

  1. Advisories — vulnerability database checks
  2. Licenses — license compatibility validation
  3. Bans — prevent specific crates from being used
  4. Sources — whitelist/blacklist dependency sources

Step-by-Step Configuration Guide

1. Configuring Vulnerability Checks (Advisories)

The advisory database is maintained by RustSec and includes CVEs and security vulnerabilities.

[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db/raw/main/db.json.gz"]
# How to handle crates with security vulnerabilities
vulnerability = "deny"
# How to handle unmaintained crates
unmaintained = "warn"
# How to handle crates that have been yanked from crates.io
yanked = "warn"
# How to handle crates with security notices
notice = "warn"
# A list of advisory IDs to ignore
ignore = [
    # "RUSTSEC-2020-0001",
]

Run the advisory check:

cargo deny check advisories

Example output for a project with a vulnerable dependency:

error: security vulnerability detected

Advisory: RUSTSEC-2020-0001
Crate: example-lib
Version: 0.1.0
Date: 2020-01-01
URL: https://rustsec.org/advisories/RUSTSEC-2020-0001
Title: Buffer overflow in example-lib
Description:
    An integer overflow in example-lib before 0.2.0 could lead to a buffer 
    overflow when processing untrusted input.
Whitelist: ignore this advisory for the ID

2. License Compliance Configuration

Define which licenses are acceptable in your dependencies:

[licenses]
# How to handle crates which do not have a detectable license
unlicensed = "deny"
# List of explicitly allowed licenses
allow = [
    "MIT",
    "Apache-2.0",
    "Apache-2.0 OR MIT",
    "BSD-2-Clause",
    "BSD-3-Clause",
    "ISC",
    "Unicode-DFS-2016",
]
# Lint level for licenses considered copyleft
copyleft = "warn"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
allow-osi-fsf-free = "both"
# Lint level used when no other predicates are matched
default = "deny"
# The confidence threshold for detecting a license from license text.
confidence-threshold = 0.8
# Some crates don't have a license field in their Cargo.toml.
# This option allows you to specify the license for those cases.
exceptions = [
    { name = "some-crate", allow = ["MIT"] },
]

Check licenses:

cargo deny check licenses

Example output:

error: GPL-like license detected

Crate: gpl-library
Version: 1.0.0
License: GPL-2.0-only

Issue: GPL-2.0-only licenses are not allowed
Resolution: Either remove the dependency or add the license to the allow list

3. Banning Specific Crates

Prevent problematic crates from being used:

[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when an unmaintained crate is detected
unmaintained = "warn"
# Lint level for when a crate with notices is detected
notice = "warn"
# A list of explicitly disallowed crates
denied = [
    # Each entry can be just a crate name, or a crate name and version range
    { name = "openssl" },
    { name = "openssl-sys", version = "*" },
]
# Certain crates/versions that will be skipped when doing duplicate detection
skip = [
    # { name = "ansi_term", version = "=0.11.0" },
]
# Similarly named crates that are allowed to coexist
skip-tree = [
    # { name = "windows-sys", version = "=0.42.0" },
]

For example, if you want to enforce use of rustls over openssl for TLS:

[bans]
denied = [
    { name = "openssl", version = "*" },
    { name = "openssl-sys", version = "*" },
]

Run the ban check:

cargo deny check bans

4. Source Validation

Control where dependencies can come from:

[sources]
# Lint level for what to happen when a crate from a crate registry that is not in the allow list is detected
unallow-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not in the allow list is detected
unallow-git = "warn"
# The lint level used when no other predicates are matched
allow = [
    "https://github.com/rust-lang/crates.io-index",
]

To allow internal registries:

[sources]
allow = [
    "https://github.com/rust-lang/crates.io-index",
    "https://registry.company.com/git/index",
]

Running All Checks

Execute all four checks with a single command:

cargo deny check

For detailed output:

cargo deny check --verbose

To generate a JSON report for parsing or integration with other tools:

cargo deny check --format json

Integration with CI/CD Pipelines

GitHub Actions Example

Create .github/workflows/supply-chain.yml:

name: Supply Chain Security

on:
  push:
    branches: [main, develop]
    paths: [Cargo.lock, deny.toml]
  pull_request:
    branches: [main]
  schedule:
    # Run daily at 2 AM UTC
    - cron: '0 2 * * *'

jobs:
  cargo-deny:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
      
      - name: Install cargo-deny
        run: cargo install cargo-deny
      
      - name: Run cargo deny
        run: cargo deny check

GitLab CI Example

Add to .gitlab-ci.yml:

supply-chain-check:
  stage: security
  image: rust:latest
  script:
    - cargo install cargo-deny
    - cargo deny check
  only:
    - merge_requests
    - main

Common Pitfalls and Solutions

Pitfall 1: Over-Permissive License Allow-Lists

Problem: Adding GPL to your allow-list because one dependency uses it.

Solution: Instead, use exceptions for specific crates:

[licenses]
allow = ["MIT", "Apache-2.0"]
exceptions = [
    { name = "gpl-crate", allow = ["GPL-2.0-only"] },
]

Pitfall 2: Ignoring Advisories Without Review

Problem: Reflexively adding vulnerability IDs to the ignore list.

[advisories]
ignore = ["RUSTSEC-2024-0001"]

Solution: Always investigate and document why an advisory is being ignored:

[advisories]
ignore = [
    # RUSTSEC-2024-0001: Only affects use case X which we don't use
    # See: https://github.com/myorg/myrepo/issues/1234
    "RUSTSEC-2024-0001",
]

Pitfall 3: Multiple Versions of the Same Crate

Problem: Dependency trees pulling in v1 and v2 of the same crate.

Solution: Use the ban check and resolve with cargo-tree:

cargo tree --duplicates

Then either upgrade incompatible dependencies or use skip-tree in deny.toml:

[bans]
skip-tree = [
    { name = "some-crate", version = "=1.0.0" },
]

Pitfall 4: Outdated Advisory Database

Problem: Missing recent CVEs because the advisory database wasn’t updated.

Solution: Fetch the latest database explicitly:

cargo deny fetch

Add to your CI pipeline to ensure fresh checks:

cargo deny fetch && cargo deny check

Practical Example: Securing a Web Service

Here’s a real-world deny.toml for a production web service:

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

[licenses]
unlicensed = "deny"
allow = [
    "MIT",
    "Apache-2.0",
    "Apache-2.0 OR MIT",
    "BSD-2-Clause",
    "BSD-3-Clause",
    "ISC",
    "Unicode-DFS-2016",
]
copyleft = "warn"
allow-osi-fsf-free = "both"
default = "deny"
confidence-threshold = 0.8

[bans]
multiple-versions = "warn"
unmaintained = "warn"
notice = "warn"
denied = [
    { name = "openssl", version = "*" },
    { name = "openssl-sys", version = "*" },
]

[sources]
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []

Automation: Pre-Commit Hooks

Automatically run cargo-deny before commits:

Create .githooks/pre-commit:

#!/usr/bin/env bash
set -e

echo "Running cargo-deny..."
cargo deny check advisories licenses bans sources

if [ $? -ne 0 ]; then
    echo "cargo-deny check failed. Fix issues before committing."
    exit 1
fi

echo "✓ cargo-deny checks passed"

Enable the hook:

chmod +x .githooks/pre-commit
git config core.hooksPath .githooks

Monitoring and Maintenance

Regular Audits

Schedule weekly scans:

# Create a simple script to email reports
#!/bin/bash
DATE=$(date +%Y-%m-%d)
cargo deny check --format json > /tmp/deny-report-$DATE.json
# Send to your security team

Keeping Dependencies Updated

Use cargo outdated alongside cargo-deny:

cargo install cargo-outdated
cargo outdated

Then combine updates with supply chain checks:

cargo update
cargo deny check
cargo test

Additional Resources and Tools

When working with configuration files and formats, you might find these tools helpful:

  • YAML/JSON Converter — Convert your deny.toml to JSON for parsing or validation
  • Regex Tester — Test patterns if you’re writing custom deny.toml rules
  • Diff Checker — Compare deny.toml versions across environments
  • JSON Formatter — Validate JSON exports from cargo deny check --format json

Conclusion

Cargo-deny transforms supply chain security from an afterthought into an integrated part of your development workflow. By combining it with CI/CD pipelines, version control hooks, and regular audits, you establish multiple layers of defense against vulnerable, malicious, or non-compliant dependencies.

Start with the basic four checks (advisories, licenses, bans, sources), customize the configuration for your organization’s needs, and automate everything. Your future self—and your security team—will thank you when cargo-deny catches a critical vulnerability before it reaches production.

Make supply chain security a feature, not an afterthought. Your Rust projects deserve it.

This post was generated with AI assistance and reviewed for accuracy.