Appendix P — Security and Supply-Chain Checklist
Audit a Rust release from threat boundary and parser limits through dependency execution, SBOM, provenance, and incident ownership.
The release candidate has no known advisory matches. Its memory-safe parser passes tests. Its SBOM exists, and its provenance statement is signed. The release is still on hold.
The review record contains one vulnerability exception whose feature is asserted to be disabled. The exception has an owner and expiry, but no approver has bound that assertion to this artifact. The absence of a match does not prove safety; memory safety does not define attacker goals; an SBOM does not prove completeness or trust; and a signature does not prove that the signer should have endorsed the bytes.
Appendix O’s experiment dossier established the same custody rule for performance: evidence authorizes only its named decision, and it expires when the artifact, workload, environment, or policy changes. Security review inherits that discipline and widens it. A parser result, dependency graph, exception, SBOM, provenance statement, and response drill must all identify the same release—or state exactly why one piece does not apply.
This checklist follows custody rather than code layout:
attacker and asset model
│
▼
input + identity + secret boundaries
│
▼
safe Rust ──> unsafe inventory ──> FFI boundary
│
▼
dependency + feature graph
│
▼
build scripts + proc macros + native tools
│
▼
locked source ─> isolated build ─> artifact digest
│ │
├────────> SBOM ───────────────┤
└────────> provenance ─────────┤
▼
advisory monitoring + response owner
│
▼
admit, hold, reject, or expire
No downstream artifact repairs a missing upstream decision. A complete SBOM cannot compensate for an undefined trust boundary. Reproducible malicious code remains malicious. A sandboxed build can still produce a malicious artifact. Security comes from linked controls with named authority and retained evidence.
The completed file-backed record is examples/rust-engineering-handbook/appendices/experiment-security-review-pack/security-review.json. The narrow executable policy examples are in examples/rust-engineering-handbook/part-14/security-supply-chain-lab.
Gate 1: threat model the release that actually exists
Begin with the release artifact, deployed role, and environment—not “the Rust service” in the abstract. Record:
- assets whose confidentiality, integrity, availability, authenticity, or tenant separation matters;
- attacker capabilities, including unauthenticated peers, malicious tenants, compromised operator accounts, dependency publishers, and build workers;
- trust boundaries for network input, tenant identity, filesystem or device access, safe-to-unsafe calls, FFI, build execution, signing, and deployment;
- abuse cases and unacceptable outcomes;
- assumptions supplied by a proxy, kernel, sandbox, hardware root, cloud identity, or operator procedure;
- owners and review date.
The relay example protects tenant data, routing policy, service credentials, and release signing identity. It considers both runtime attackers and build-chain compromise. This matters because a build script executing in CI can threaten credentials without ever becoming part of the runtime binary.
Ask “what authority crosses this edge?” rather than “is this component written in Rust?” Safe Rust narrows memory-corruption classes. It does not prevent an authorized-but-wrong file read, an unbounded allocation, a confused-deputy request, secret logging, weak cryptographic policy, malicious dependency behavior, or an overprivileged deployment.
Stop the release when the artifact, assets, actors, boundaries, or responsible owner are unnamed; when a critical control is assumed from infrastructure but not verified; or when the threat model covers only runtime code while the build and signing paths hold greater privilege.
Gate 2: enforce parser and resource limits before expensive work
Every externally influenced parser needs a budget. Record limits in protocol units and verify where they are applied:
| Dimension | Evidence question | Dangerous substitute |
|---|---|---|
| bytes and frame count | is input rejected before proportional allocation or buffering? | “the proxy limits requests” without deployed configuration evidence |
| nesting and recursion | is depth bounded independently of total bytes? | stack safety inferred from small test fixtures |
| fields, tokens, matches | can attacker-controlled multiplicity multiply work? | one total-size cap |
| decompression or expansion | is expanded size and ratio bounded? | compressed byte limit only |
| time and CPU | is cancellation cooperative and observable? | socket timeout that does not stop CPU work |
| concurrency and queues | is admission bounded before spawning or enqueuing? | autoscaling as an overload control |
| malformed inputs | are error paths bounded, redacted, and rate-limited? | detailed echo of attacker input |
The completed example records 65,536 bytes, 128 fields, depth 8, and 50 ms as teaching values. They are not recommendations. Production values derive from protocol contracts, legitimate maxima, memory and CPU budgets, and adversarial measurements. Test exact limits, one below, one above, truncation, repeated delimiters, invalid encodings, and worst-case structural shapes.
Limits must compose. A 64 KiB message accepted on 10,000 simultaneous connections can still exceed the service envelope. A timeout without cancellation can create detached work. A retry can multiply an expensive rejection. Connect parser limits to admission control, backpressure, deadline propagation, and observability.
When a limit is performance-sensitive, use Appendix O’s experiment record: name the adversarial workload, measure rejection cost, retain failures, and set a regression threshold. Performance evidence never replaces the correctness bound.
Gate 3: trace secrets from acquisition to destruction
Create a secret-flow inventory for credentials, keys, tokens, personal data, and sensitive tenant content:
- source and authority that provides the value;
- process, thread, task, or foreign component that receives it;
- memory and storage representations;
- logs, metrics, traces, errors, panic hooks, crash dumps, and benchmark corpora it might enter;
- transmission and cryptographic boundary;
- rotation, revocation, and incident owner;
- lifetime and destruction limits that are actually enforceable.
Prefer workload identity or a dedicated secret service over static values in source, images, or broad environment files. Restrict access by process and purpose. Keep build workers away from production credentials and signing keys. Redaction should be structural: mark sensitive fields and deny their serialization into ordinary diagnostics. Searching logs after release is detection, not prevention.
Rust values may be copied, formatted, retained in allocator pages, captured by async tasks, or duplicated across FFI. Dropping a String does not promise physical erasure of every copy. If zeroization is required, define the threat and use a reviewed mechanism whose optimizer, allocation, swap, core-dump, and foreign-code limitations are understood. Do not advertise “secrets never remain in memory” without an end-to-end proof.
Test redaction with a unique marker across structured events, error chains, debug output, panic hooks, and sampled traces. Test rotation while requests are in flight and failure when the secret provider is unavailable. Record whether old credentials remain valid and who revokes them.
Gate 4: inventory every unsafe and foreign obligation
An unsafe count is a routing index, not a safety proof. Generate or maintain an inventory that maps each unsafe block, unsafe fn, unsafe trait implementation, FFI declaration, allocator hook, and generated unsafe region to:
- the precise safety contract;
- validity, initialization, aliasing, provenance, bounds, alignment, lifetime, and thread assumptions as applicable;
- safe callers or safe abstraction boundary;
- panic, partial-initialization, and drop behavior;
Send/Syncreasoning and concurrent access;- owner and specialist reviewer;
- tests, Miri or sanitizer applicability, fuzz targets, and platform matrix;
- change trigger and last review.
Appendix K supplies the detailed unsafe review. This release gate asks whether its evidence is current for this artifact. New compiler, target, dependency, optimization, or foreign library versions can reopen an obligation even when the Rust source is unchanged.
For FFI, apply Appendix L and retain ABI and ownership evidence:
- exact ABI, calling convention, symbol and header identity;
- layout, width, alignment, nullability, and encoding;
- buffer ownership, borrowing duration, handle lifecycle, and release function;
- callback thread, lifetime, reentrancy, and unregister/quiescence rule;
- error mapping and panic/unwind containment;
- foreign allocator and runtime compatibility;
- C/C++ or other consumer compilation and execution tests;
- supported target matrix and symbol/version policy.
Stop the release for an unowned unsafe block, undocumented safe wrapper precondition, unverified generated binding, unwind that may cross a foreign ABI contrary to its contract, ambiguous allocator ownership, or callback that can outlive its context.
Gate 5: approve the resolved dependency and feature graph
Review what Cargo resolves, not what one Cargo.toml appears to request. For every direct dependency, record purpose, owner, exact source and version policy, supported targets, enabled features, license decision by authorized reviewers, unsafe/native surface, build-time execution, maintainer and ownership evidence, replacement option, and renewal trigger.
Then inspect the transitive graph:
cargo +1.97.0 metadata --locked --format-version 1
cargo +1.97.0 tree --locked -e features
cargo +1.97.0 tree --locked --target all
The last command can be expensive or unsupported by some graph/target combinations; define a supported-target matrix rather than claiming one host inspected every target. Feature unification is additive within a resolved graph. default-features = false in one edge does not prove another edge did not enable the feature. Capture the final graph and compare it with the last accepted release.
Material changes that reopen review include a new direct or transitive package, owner or source transfer, Git dependency, major release, license change, default-feature expansion, native code, unsafe growth, build script, procedural macro, MSRV change, target change, or serious advisory. A lockfile diff should be explained package by package. “Routine update” is not a threat assessment.
Popularity, download count, age, or lack of an advisory can inform investigation but cannot approve a component. Compare deletion, standard-library implementation, a smaller crate, service isolation, and a maintained specialist implementation by total ownership and risk. Do not replace mature cryptography or parsers casually merely to reduce dependency count.
Gate 6: isolate build scripts, procedural macros, and native tools
Cargo compiles and executes build.rs before building the package. Procedural macros execute as compiler-host code and carry comparable access concerns. Native compilers, linkers, code generators, CI actions, package hooks, and image-build steps are also executable inputs.
For each build-time executable, record:
- source identity, owner, version, checksum, and review status;
- why execution is required and which outputs it may create;
- filesystem, environment, process, network, clock, and credential access;
- target-versus-host assumptions for cross compilation;
- generated output and linker instruction evidence;
- sandbox policy and residual consequence;
- material-change detection.
Build on ephemeral, isolated workers with minimal read-only source inputs, denied-by-default network, no production credentials, scoped artifact-write authority, pinned tools and base images, and separate signing identity. A sandbox reduces consequence; it does not make generated code trustworthy. An offline build proves availability of inputs, not their provenance.
Review Cargo output and package metadata for transitive build scripts and proc-macro targets. Do not depend on a requester’s declaration. Generated files should be reproducible or retained and reviewed. Build scripts should write generated artifacts to Cargo’s intended output directory; unexpected repository or home-directory mutation is a stop signal.
Gate 7: bind lockfile, SBOM, provenance, and artifact digest
These artifacts answer different questions:
| Artifact | Answers | Does not prove |
|---|---|---|
| lockfile | which Cargo resolution was selected | exact native environment or benign code |
| acquired-source checksum | whether bytes match an identified package source | source quality or maintainer intent |
| SBOM | which components and relationships are claimed for a product identity | absence of omitted components or exploitability |
| provenance | which builder, source, materials, parameters, and invocation produced an artifact | that the workflow or inputs were trustworthy |
| signature | which key endorsed bytes | correct authorization, safe key custody, or benign content |
| reproducible-build comparison | whether declared inputs can produce matching output under stated environments | security of matching source or every distributed artifact |
Generate the SBOM from the final release process and reconcile it with cargo metadata, native libraries, container or OS packages, embedded assets, generated code, and the delivered artifact boundary. Record format and version, generator and version, product/release identity, relationships, licenses as available, source identifiers, checksums, and document digest. SPDX and CycloneDX are formats, not completeness guarantees.
Provenance should bind the subject artifact digest to source, builder identity, invocation, parameters, and materials. Verification policy must authorize the expected builder and source, not merely validate a signature. Protect signing after the artifact digest exists and separate signing authority from mutable build steps. Define revocation, transparency or audit retention, and recovery.
Attempt clean locked builds without network, then pursue bit reproducibility where the product needs it. Record toolchain, linker, native libraries, timestamps, locale, paths, environment, and signing stage. Investigate byte differences. If only code sections match while packaging metadata differs, say exactly that; do not label the whole build reproducible.
Gate 8: make advisory handling an owned response path
Monitor RustSec, OSV, relevant vendors, operating-system and native-library channels, the Rust toolchain, and private disclosure routes. Preserve scan time, database or advisory revision, package/source match, artifact and deployment query, feature and target reachability, attacker prerequisites, consequence, compensating controls, owner, deadline, and superseding release.
An advisory match begins triage. Reachability can prioritize; it should not silently erase the finding. Build-time execution, FFI, configuration, dynamic dispatch, and future feature changes can defeat shallow reachability models. Conversely, a matched package is not automatically exploitable in the deployed artifact. Record the reasoning and retain the finding.
Every exception requires:
- exact affected artifacts, deployments, package/version, features, and targets;
- risk statement and authorizing role;
- compensating control with executable or observable evidence;
- remediation owner and deadline;
- absolute expiry and automatic stop or renewal behavior;
- tested exit: upgrade, feature removal, patch, fork, isolation, or dependency deletion.
The fixture intentionally holds the release because exception SEC-EX-104 has not been re-approved for the artifact. This demonstrates fail-closed review: a plausible disabled-feature assertion is evidence to inspect, not authority to release.
Gate 9: rehearse response ownership
Write names or accountable roles before an incident:
| Response job | Required proof |
|---|---|
| advisory triage | monitored intake, severity and reachability process, on-call handoff |
| inventory query | time-bounded query from package/source identity to artifacts and deployments |
| dependency repair | authority to update, patch, fork, disable, or remove |
| replacement build | isolated emergency lane retaining review, tests, SBOM, provenance, and signing |
| release and rollback | artifact promotion, staged deployment, rollback, and verification ownership |
| communication | customer, regulator, supplier, and internal criteria appropriate to the organization |
| evidence retention | advisory revision, decisions, logs, artifact identities, and supersession chain |
Run a drill. Choose a dependency and version from a recent SBOM, simulate a high-consequence advisory, locate affected artifacts and deployments, produce a replacement under normal controls, verify the new SBOM excludes the identity, and exercise rollback. Record elapsed times and broken links. “Security team owns it” is not a routing plan.
Response speed must not create a second supply-chain incident. Emergency work may compress scheduling, but it still needs identified source, review, testing, artifact custody, protected signing, and rollback. Pre-authorized roles and rehearsed lanes create speed more safely than bypassing controls.
Release-board ledger
Use one disposition per gate. pass means evidence is current and artifact-bound; conditional means an explicit expiring condition still blocks admission until satisfied; fail means the contract is violated; not applicable requires a reason and approver.
| Gate | Minimum retained evidence | Stop condition |
|---|---|---|
| threat model | artifact, assets, actors, boundaries, abuse cases, assumptions, owner | missing critical boundary or owner |
| parser limits | limits, exact-boundary tests, cost/load evidence, rejection telemetry | unbounded attacker-controlled work |
| secrets | source-to-destruction map, redaction and rotation tests | secret in source/build/log or unowned rotation |
| unsafe/FFI | inventories, safety/ABI contracts, specialist evidence, target matrix | undocumented or unowned obligation |
| dependencies/features | locked metadata and feature graph, approvals, license authority, diff | unexplained or unapproved material change |
| build execution | script/macro/tool inventory, isolation and output evidence | ambient credentials or unidentified executable input |
| SBOM/provenance | release-bound documents and digests, reconciliation, verifier policy | identity mismatch or unverified builder/source |
| advisories/exceptions | dated sources, artifact mapping, owners, expiry, exit | expired/unapproved exception or unowned critical finding |
| response | contacts, inventory query, replacement lane, rollback and drill record | inability to locate or replace affected release |
The final record names admit, hold, or reject; rationale; artifact digest; approvers; unresolved conditions; expiry; and the evidence bundle location. Approval belongs to organizational authority, not this checklist or its verifier.
Exercise: handle a compromised procedural macro
Assume a procedural macro maintainer account is compromised. The malicious version was available for six hours. Your lockfile contains the previous version, but a developer CI job ran without --locked and had a repository write token. No runtime SBOM lists build-only packages.
Produce a response packet that:
- identifies every build, cache, generated output, source branch, and artifact that may have encountered the version;
- explains why the runtime SBOM and the old lockfile are insufficient evidence;
- revokes and scopes the exposed token, quarantines caches and artifacts, and preserves forensic evidence;
- pins and verifies a known source, rebuilds on a clean isolated worker, and compares generated outputs;
- regenerates an inventory that includes build dependencies and binds provenance to the replacement digest;
- defines customer or operator communication criteria without inventing legal authority;
- records an exception only if removal is impossible, with owner, compensating controls, expiry, and exit;
- adds a control that makes unlocked CI resolution fail and tests that control.
Then run the same exercise with no advisory and no known malicious release, only an unexplained generated-code diff. The absence of an advisory should not change artifact quarantine and custody work.
Printable stop-ship card
Do not admit a release when any answer is unknown:
- What artifact and deployment role are under review, and what digest identifies the bytes?
- Which assets, attackers, trust boundaries, abuse cases, and external assumptions govern it?
- Where are attacker-controlled bytes, multiplicity, nesting, expansion, time, concurrency, and queues bounded?
- Can secrets reach source, builds, logs, errors, traces, dumps, corpora, or foreign code?
- Is every unsafe and FFI obligation inventoried, owned, current, and tested on supported targets?
- Does the resolved dependency and feature graph match current approvals and license authority?
- Which build scripts, proc macros, native tools, and CI actions execute, with what access?
- Do lockfile, acquired sources, SBOM, provenance, signature policy, and artifact digest refer to the same release story?
- Are advisory findings and exceptions artifact-bound, owned, approved, time-limited, and removable?
- Can the team query affected deployments, build a replacement, sign it, roll it out or back, and communicate under rehearsed ownership?
A “no” can be a repair task. An “unknown” is a control failure. Record both rather than converting them to a green checkbox.
Security custody determines whether a named artifact has defensible evidence and unresolved stop conditions. It does not decide what compatibility the product promises, how a change is classified, or which release and recovery authorities apply. Those are separate policy decisions.
Sources and version notes
- The Cargo Book: build scripts documents that Cargo compiles and runs build scripts before building a package and describes their inputs and outputs.
- The Rust Reference: procedural macros notes that procedural macros carry build-script-like security concerns because they execute during compilation.
cargo metadata,cargo tree, and Cargo lockfiles provide graph and resolution evidence; their flags and output formats are version-sensitive.- RustSec and OSV provide advisory data. Their coverage and a clean query are not complete safety verdicts.
- NIST SP 800-218 SSDF describes outcome-oriented secure development practices, including protecting development environments and collecting provenance.
- SLSA provenance, SPDX specifications, and CycloneDX specifications define interoperable provenance and inventory structures; organizational policy must select versions and verification requirements.
The fixture targets Rust 2024, records Rust 1.97.0 for its teaching evidence, and declares no external package as trusted merely because it appears in a record. The Node verifier checks presence, internal consistency, expiry, and fail-closed disposition; it does not perform threat modeling, legal review, advisory scanning, SBOM generation, provenance verification, cryptographic validation, or release approval. Revalidate Cargo behavior, advisory sources, schema versions, supported targets, and organizational authority before applying the checklist to a release.
Continue reading
Full table of contents