AI Supply Chain Security
AI supply chain security is the practice of protecting every artifact that flows into a model before it reaches production: the base weights you download, the datasets you fine-tune on, the Python packages your training and inference code pulls in, and the container images you ship. If any one of those links is tampered with, your deployed model inherits the compromise, and traditional application scanners will not catch it. This guide walks through the concrete controls, commands, and code that engineers can apply today to make an AI supply chain verifiable end to end.
The reason AI supply chain security needs its own playbook is that a model is not just code. It is a large binary blob of learned parameters, trained on data you often did not produce, using frameworks that deserialize arbitrary objects by default. A poisoned checkpoint or a backdoored pickle file looks identical to a clean one until it runs. So the goal is not "scan for bad strings" but "prove that every artifact is the one you expected, from a source you trust, unchanged since it was produced."
Why AI supply chain security is different from app security
A normal application supply chain has source code, dependencies, and a build. You can read the source, pin the dependencies, and reproduce the build. AI adds three artifact classes that break those assumptions.
- Model weights: opaque tensors you cannot code-review. You can only verify their hash and provenance, not their intent.
- Training and fine-tuning data: often scraped, licensed, or crowd-sourced. Poisoning a small fraction of it can plant a backdoor that survives fine-tuning.
- Serialized model formats: Python
pickle, PyTorch.bin, and older Keras formats can execute code on load. Loading a model can be equivalent to running an untrusted script.
On top of that, the actual attack surface is wide. The main threats you are defending against:
- Model tampering: someone swaps or patches weights on a public hub or your internal registry.
- Data poisoning: malicious samples in the training set create targeted misbehavior or a trigger phrase that flips outputs.
- Malicious serialization: a
.pklor.binthat runsos.systemduringtorch.load. - Dependency attacks: a typosquatted or compromised package in your
requirements.txt(thinktorchtriton-style incidents where a public index shadows a private one). - Compromised model hub accounts: a maintainer token leaks and a popular model gets a poisoned revision.
Application security tooling handles the last two partially. The first three are AI-specific and need AI-specific controls, which is the core of AI supply chain security.
Pin and verify every model you pull
The single highest-value habit is to stop trusting "latest" for anything. Model hubs are mutable by default: a repo tag can move, a file can be replaced. Pin to an immutable commit hash and verify the file digest.
With the Hugging Face Hub client, pin the exact revision:
from huggingface_hub import snapshot_download
local_dir = snapshot_download(
repo_id="org/base-model",
revision="9f3c1b7e2a0d5f4c8b1e6a2d7c9f0b3e4d5a6c7b", # full commit SHA, not a tag
allow_patterns=["*.safetensors", "*.json", "tokenizer*"],
)
print("Pulled to", local_dir)A branch name or tag like main or v1.0 can be re-pointed by anyone with write access. A full commit SHA cannot. Record that SHA in your repo alongside the code that uses it.
Then verify the actual bytes. Compute a digest and compare it to a value you stored the first time you vetted the model, not a value fetched fresh from the same server:
import hashlib, pathlib
def sha256_file(path, chunk=1 << 20):
h = hashlib.sha256()
with open(path, "rb") as f:
for block in iter(lambda: f.read(chunk), b""):
h.update(block)
return h.hexdigest()
expected = {
"model.safetensors": "3b1f...c9a2", # pinned once, reviewed, committed to git
}
for name, want in expected.items():
got = sha256_file(pathlib.Path(local_dir) / name)
if got != want:
raise SystemExit(f"DIGEST MISMATCH for {name}: {got} != {want}")
print("all digests verified")The important nuance: the checksum only means something if you got the expected value through a different trust path than the file itself. Pin it in your source repository after a human reviews the model once. A checksum served next to the file by the same host proves only that the download was not corrupted in transit, not that the file is honest.
Prefer safetensors and never blindly load pickles
The most direct code-execution risk in the AI supply chain is deserialization. Python's pickle format, which PyTorch .bin checkpoints and many .pkl artifacts use, can run arbitrary code during load. A malicious checkpoint can define a __reduce__ that shells out the moment you call torch.load.
Two defenses, in order of preference.
First, use the safetensors format. It stores only tensors and metadata, with no code path, so loading it cannot execute anything:
from safetensors.torch import load_file
state_dict = load_file("model.safetensors") # data only, no code execution
model.load_state_dict(state_dict)Second, when you are forced to load a legacy pickle checkpoint, scan it before loading and never load it in an environment with secrets or network access. The picklescan tool inspects the opcodes for dangerous imports without executing them:
pip install picklescan
picklescan --path ./legacy_checkpoint.binIf a scan flags os, subprocess, builtins.eval, or similar, treat the artifact as hostile. For unavoidable loads, newer PyTorch versions default torch.load to weights_only=True, which refuses arbitrary globals. Keep that default on:
import torch
# weights_only=True blocks arbitrary object reconstruction during unpickling
state = torch.load("checkpoint.bin", weights_only=True, map_location="cpu")The rule of thumb for AI supply chain security: convert everything to safetensors at ingestion time, quarantine the original pickle, and let nothing downstream ever call an unrestricted torch.load again.
Scan model files with dedicated tooling
Standard SAST and dependency scanners do not open a .safetensors or .bin and reason about it. You need model-aware scanners in your pipeline. Several open tools exist for this class of check, and they run headlessly in CI.
ModelScan (from Protect AI) inspects model files across formats for embedded code and unsafe operators:
pip install modelscan
modelscan -p ./models/candidate_model.safetensors
modelscan -p ./models/legacy_checkpoint.bin --reporting-format json > scan.jsonWire it into CI so no model artifact is promoted without a clean scan. A minimal GitHub Actions step:
- name: Scan model artifacts
run: |
pip install modelscan
modelscan -p ./models/ --reporting-format json > modelscan.json
python -c "import json,sys; r=json.load(open('modelscan.json')); \
issues=r.get('summary',{}).get('total_issues',0); \
sys.exit(1) if issues else print('model scan clean')"Fail the build on any issue. Scanners will not catch a subtle statistical backdoor, but they reliably catch the loud stuff: shell calls, network beacons, and known-bad serialization tricks. That closes off the cheapest attacks.
Lock down your ML dependency chain
Model code sits on a deep stack: torch, transformers, accelerate, CUDA wheels, tokenizers, and dozens of transitive packages. This is where classic supply chain attacks land, and the AI ecosystem has two extra hazards.
The first is index confusion. If you host private packages, a public index can shadow your internal name and get installed instead. Pin your index explicitly and never let pip fall back:
pip install --index-url https://your-private-index/simple/ \
--no-index-url-fallback \
-r requirements.txtThe second is that GPU and framework wheels are large native binaries you cannot audit by reading. So pin exact versions and hashes. Generate a fully hashed lockfile:
pip install pip-tools
pip-compile --generate-hashes --output-file requirements.lock requirements.inThen install in hash-checking mode, which refuses any wheel whose bytes do not match the lock:
pip install --require-hashes -r requirements.lockRun a vulnerability audit on the resolved set as a gate:
pip install pip-audit
pip-audit -r requirements.lockFor teams standardizing on faster tooling, uv produces a hashed uv.lock and installs from it deterministically:
uv lock
uv sync --frozenThe judgment call here: pin hashes for everything you ship, but keep a separate, looser dev environment so security patches are easy to test. The locked file is the contract for production; the dev file is where you evaluate upgrades.
Generate an SBOM that includes models and datasets
A software bill of materials lists what is in your build. For AI supply chain security you want that bill to include the model and the dataset, not just the pip packages, so an auditor or an incident responder can answer "what exactly was in the thing we shipped."
Generate a component SBOM for your Python environment with a standard tool:
pip install cyclonedx-bom
cyclonedx-py environment -o sbom.jsonThen extend it with an AI-specific manifest that records the model identity, its source, and its digest. There is a growing convention of a model card plus an "AI BOM" that captures model and data provenance. A pragmatic version you control:
{
"model": {
"repo_id": "org/base-model",
"revision": "9f3c1b7e2a0d5f4c8b1e6a2d7c9f0b3e4d5a6c7b",
"sha256": "3b1f...c9a2",
"format": "safetensors",
"license": "apache-2.0"
},
"datasets": [
{
"name": "internal-support-tickets-2026q1",
"sha256": "a17c...44e9",
"record_count": 128934,
"source": "s3://data-lake/curated/support/2026q1/"
}
],
"scanned_with": ["modelscan", "picklescan"],
"produced_by": "ci-pipeline@commit d4e5f6"
}Store this next to the model in your registry. When a CVE or a poisoning report lands for some upstream model or dataset months later, an SBOM turns "are we affected" from a week of archaeology into a grep.
Sign and verify artifacts with provenance
Hashes prove a file did not change. Signatures prove who produced it and let you build a chain of trust. The modern, keyless way to do this is Sigstore's cosign, which signs artifacts against an OIDC identity and records them in a transparency log, so there are no long-lived private keys to leak.
Sign a model or its SBOM as a blob:
cosign sign-blob --yes model.safetensors \
--output-signature model.sig \
--output-certificate model.pemVerify it in your deploy step, asserting the exact identity that was allowed to produce it:
cosign verify-blob model.safetensors \
--signature model.sig \
--certificate model.pem \
--certificate-identity "https://github.com/your-org/model-pipeline/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"If you ship models as OCI artifacts (which many registries now support), you can attach a signed SLSA provenance attestation that records how and where the artifact was built:
cosign attest --yes --predicate provenance.json \
--type slsaprovenance \
registry.example.com/models/base-model:1.4.0The point of provenance is to move from "I trust this hash" to "I trust this build pipeline, and here is cryptographic proof this artifact came out of it." That is what stops a compromised registry from serving you a swapped model: even with write access, an attacker cannot forge the signing identity.
Defend the training pipeline against poisoning
Everything above protects artifacts you consume. If you train or fine-tune, you also produce artifacts, and your inputs need the same rigor. Data poisoning is the AI-specific threat with no equivalent in normal software: a handful of crafted samples can implant a trigger that makes the model misbehave only on a secret input.
Practical controls that fit into a data pipeline:
- Freeze and hash datasets. Compute a digest over the exact serialized dataset used for a run and record it in the SBOM. A training run should be reproducible from a pinned data hash, not "whatever was in the bucket that day."
- Track provenance per source. Tag every record with where it came from so you can excise one bad source without retraining on everything.
- Gate third-party data. Treat scraped or vendor data as untrusted input. Deduplicate, filter, and hold out a clean, curated evaluation set the training data never touches.
- Detect drift and triggers. Before promoting a fine-tuned model, run it against a behavioral test suite that includes adversarial and trigger-probe prompts, and compare metrics against the base model. A sudden jump in a narrow slice is a red flag.
A minimal dataset-freeze helper you can drop into a pipeline:
import hashlib, json
def freeze_dataset(records, out_path):
# canonical, order-independent digest of the training set
h = hashlib.sha256()
for r in sorted(records, key=lambda x: x["id"]):
h.update(json.dumps(r, sort_keys=True).encode())
digest = h.hexdigest()
with open(out_path, "w") as f:
for r in records:
f.write(json.dumps(r, sort_keys=True) + "\n")
return digest
digest = freeze_dataset(training_records, "train.frozen.jsonl")
print("dataset digest:", digest) # record this in the AI BOMYou cannot prove a dataset is free of poisoning the way you can prove a hash matches. The realistic goal is containment: know exactly what went in, be able to attribute and remove a bad source, and catch behavioral regressions before they ship.
Put it together in CI
None of these controls matter if they are manual. The pattern that holds up is a single promotion gate: an artifact does not move from "candidate" to "approved" until it passes every check, and the results are recorded. A representative pipeline order:
- Resolve and install dependencies with
--require-hashesso the build environment itself is pinned. - Download the model at a pinned commit SHA and verify its digest against the committed value.
- Convert any pickle checkpoint to safetensors, and quarantine the original.
- Run
modelscanandpicklescan; fail the build on any finding. - Freeze and hash the dataset for any training or fine-tuning step.
- Generate the SBOM plus the AI BOM with model and dataset digests.
- Sign the model and SBOM with
cosign, attach provenance. - At deploy time, verify the signature and identity before the model loads.
The through-line of AI supply chain security is boring on purpose: pin everything, verify by digest through an independent trust path, prefer formats that cannot execute code, scan what you cannot read, and sign what you produce. None of it requires exotic tooling. It requires refusing to trust "latest" and making verification a build gate rather than a good intention.
FAQ
What is AI supply chain security in one sentence? It is the discipline of guaranteeing that every model, dataset, and dependency entering your ML system is the exact artifact you expected, from a source you trust, unchanged since it was produced, and free of embedded code execution.
How is it different from regular software supply chain security? Regular supply chain security covers source, dependencies, and builds you can read and reproduce. AI adds opaque model weights you cannot code-review, training data that can be poisoned invisibly, and serialization formats like pickle that execute code on load. Those three need model-specific controls: digest pinning, provenance, format conversion, and model-aware scanning.
Is downloading a model from a public hub dangerous? It can be. Loading a pickle-based checkpoint can run arbitrary code, and mutable tags can be re-pointed to a poisoned revision. Pin to a full commit SHA, verify the file digest against a value you reviewed and committed yourself, prefer safetensors, and scan any pickle with picklescan or modelscan before loading.
Why is safetensors safer than a `.bin` checkpoint? A .bin PyTorch checkpoint is usually a Python pickle, which can define reconstruction hooks that execute code during torch.load. The safetensors format stores only tensors and metadata with no code path, so loading it cannot run anything. Convert to safetensors at ingestion and quarantine the original.
What tools should I actually run? For dependencies: pip-compile --generate-hashes, pip install --require-hashes, and pip-audit (or uv lock / uv sync --frozen). For models: modelscan and picklescan. For provenance: cosign sign-blob and verify-blob with a certificate identity. For inventory: cyclonedx-py plus a hand-written AI BOM that records model and dataset digests.
How do I defend against data poisoning specifically? You cannot prove data is clean by hashing, so aim for containment. Freeze and hash the exact dataset per run, tag every record with its source so you can remove a bad one without full retraining, treat third-party data as untrusted, and run adversarial and trigger-probe evaluations before promoting any fine-tuned model.
Do signatures replace checksums? No, they complement them. A checksum proves a file did not change; a signature proves who produced it and, with SLSA provenance, how. Use digests for integrity and cosign signatures with a verified certificate identity for authenticity, so a compromised registry cannot serve you a swapped model even with write access.
Where should verification live? In CI, as a promotion gate, not in a runbook. An artifact should not move from candidate to approved until dependency hashes, model digests, scans, dataset freezes, SBOM generation, and signing all pass. At deploy time, verify the signature and identity before the model is ever loaded into a process with secrets or network access.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.