Published
- 38 min read
Sanctions, Distillation, and Open Weights: The Supply Chain Risk in Your Model Choice
Stay Safe Online Without Making It Your Second Job
The Digital Fortress (Second Edition)
A warm, plain-English guide for people with real lives and finite patience. Learn the handful of habits that genuinely protect your money, accounts, and family, and get honest permission to ignore the rest.
For People Who Cannot Afford to Get Privacy Wrong
The Anonymity Playbook (Second Edition)
A practitioner’s field manual for journalists protecting sources, whistleblowers, and activists. It explains how the surveillance actually works, what each technique costs you, and exactly where it fails.
Write, Ship, and Maintain Code Without Shipping Vulnerabilities
Secure Software Development
A hands-on security guide for developers and IT professionals who ship real software. Build, deploy, and maintain secure systems without slowing down or drowning in theory.
Use AI Coding Agents Without Losing Control of Your Codebase
The Secure Harness
A calm, practical guide to letting agents do useful work inside boundaries you set, enforce, and audit. Ships with 15 copy-pasteable artifacts: hook scripts, permission configs, release gates, and MCP templates.
Stop Shipping Demos. Start Shipping Systems.
The AI Native Engineer
Sixteen hands-on chapters, one real product. Grow it from a single model call into a retrieved, tool-using, observable, production-grade system, with evaluation treated as a habit from the first feature.
A dependency that can be sanctioned
On 22 July 2026, US Treasury Secretary Scott Bessent threatened sanctions against Chinese AI companies following accusations that Moonshot had improperly distilled Anthropic’s Fable model. The technical claim is contested: several researchers pointed out that Fable only became publicly available on 1 July, which makes it a thin basis for a model of Kimi K3’s maturity. Distillation itself is a standard optimisation technique, taught in every course on model compression, and it becomes an intellectual property question only when the teacher model’s terms forbid it.
Set the merits aside. For anyone running these models in production, the important fact is structural. A model you downloaded, evaluated, integrated, and shipped can become a compliance problem through a policy action you had no part in and no warning of.
Software dependencies have always carried licence risk, and teams have processes for it. Model weights are a newer category, and most organisations that would never ship a GPL-licensed library without review will happily deploy a set of weights whose terms nobody read and whose training data nobody can characterise.
The four questions nobody asks before deploying weights
A model is a dependency. Treating it like one means asking the questions you would ask of any other component before it reaches production.
The first is what the licence actually permits. Terms in this space vary far more than in open-source software, and the labels are misleading. Laguna S 2.1, released in July as a 118B mixture-of-experts model with 8B active parameters and a one-million-token context, ships under OpenMDW-1.1. Others use variants of Llama’s community licence with user-count thresholds, or research-only terms that quietly forbid commercial use, or bespoke licences with acceptable-use clauses that constrain what your product may do. Microsoft’s Mage family was released research-only. Deploying a research-only model in a revenue-generating product is a licence violation that a competitor’s lawyer can find in an afternoon.
The second is where the weights came from and whether you can prove it. A model downloaded from a hub is a large binary artefact from a third party. The supply-chain questions that apply to any binary apply here: was it signed, does the hash match a published value, and would you notice if the file you pulled differed from the file the publisher intended. Most teams cannot answer the last one because they never recorded the hash.
The third is what the model was trained on, to whatever degree that is knowable. The Suno breach disclosed in July, which exposed data on roughly 55.3 million users, also surfaced source code indicating extensive scraping of music and lyrics from major platforms. Training-data provenance is becoming a litigation surface, and a model whose training set is entirely opaque carries a risk you cannot size.
The fourth is jurisdictional. Which country’s export controls and sanctions regimes touch the publisher, and what happens to your deployment if that changes. This is the question the Treasury announcement made concrete.
Licence families, side by side
Those four questions become tractable once you know what the answers can look like, and the licence question is the one where guessing goes wrong most often. “Open weights” describes how the file reaches you. It says nothing about what you may do with the file after it arrives. Model releases cluster into five licence families, and the gap between the first and the last is the gap between a shippable dependency and a letter from someone’s counsel.
| Family | Examples | Commercial use | Redistribute weights | Fine-tunes and derivatives | The clause that catches people |
|---|---|---|---|---|---|
| Permissive software licence applied to weights | Apache-2.0, MIT | Yes | Yes | Yes | Written for source code, so it is silent on training data, model cards, and outputs |
| Purpose-built model licence | OpenMDW-1.1 | Yes | Yes | Yes | The grant covers the bundle actually shipped, so check what was included in the release |
| Community licence with thresholds | Llama-style community terms | Yes below a stated user threshold | Yes, with attribution and naming rules | Yes, often with a naming requirement on the derivative | An acceptable use policy incorporated by reference and updated by the publisher |
| Responsible-AI use terms | RAIL-style and vendor-specific terms | Yes | Yes | Yes | Use restrictions that bind your product features, not merely your infrastructure |
| Research or non-commercial only | Various research releases | No | Sometimes | Research only | Nothing in the download flow warns you, and the tag on the hub page is easy to skim past |
OpenMDW came out of the Linux Foundation as a direct response to that fragmentation, and it takes the position that a model release is a bundle: weights, architecture, inference and training code, documentation, and whatever data ships alongside, all under one permissive grant. NVIDIA adopted it across several of its model families, which gives it standing in procurement conversations. Laguna S 2.1 shipping under OpenMDW-1.1 is a good signal, and that deserves saying, because most of this article is about what goes wrong.
Two properties of model licences have no clean analogue in software dependency management, and both catch teams who assume the rules carry over.
Licence text is a property of a revision, not a repository. A publisher can change the LICENSE file in a later commit, and the hub page will show you the current text next to weights you pulled a year ago under different terms. Store the licence file with the weights, at the revision you pulled, or you will end up arguing about which version applied.
Acceptable use policies are usually incorporated by reference and can change without a new model release. A clause forbidding a category of application can appear months after you shipped. Someone needs to own re-reading those, on a schedule, for every model in the inventory.
The output of all this is three booleans per model: may we use it commercially, may we redistribute it or expose it to customers, and does any use restriction touch what our product does. Anything you cannot answer with a yes or a no is a no, until someone in legal writes down otherwise.
Scanning licences before the model reaches main
Three booleans per model is a policy. Making it a control means running it where models actually enter the codebase, which is a pull request adding a repository id, a config entry, or a Dockerfile line that pulls a checkpoint. The check costs milliseconds and catches the research-licensed model before it reaches a staging cluster.
Start with a policy file that a lawyer can read and a script can parse.
# model-policy.yaml
allowed:
- Apache-2.0
- MIT
- OpenMDW-1.1
- BSD-3-Clause
review_required:
- llama3.3 # user threshold and naming clauses
- gemma # acceptable use policy incorporated by reference
- other # anything the hub reports as bespoke
denied:
- cc-by-nc-4.0
- cc-by-nc-sa-4.0
- research-only
unknown_policy: deny # a missing licence tag blocks the merge
Then a check that reads your model inventory, resolves the licence at the exact revision, and fails the build on anything denied or unrecognised.
#!/usr/bin/env python3
"""Fail CI when a model reference violates the licence policy."""
import sys
import yaml
from huggingface_hub import HfApi
policy = yaml.safe_load(open("model-policy.yaml"))
inventory = yaml.safe_load(open("models.yaml"))["models"]
api = HfApi()
failures, reviews = [], []
for entry in inventory:
info = api.model_info(entry["repo"], revision=entry["revision"])
licence = (info.card_data or {}).get("license") or "unknown"
if licence in policy["denied"]:
failures.append(f"{entry['repo']}@{entry['revision'][:12]}: denied licence {licence}")
elif licence in policy["review_required"]:
reviews.append(f"{entry['repo']}: {licence} needs a signed review record")
elif licence not in policy["allowed"]:
failures.append(f"{entry['repo']}: unrecognised licence {licence}")
for line in reviews:
print(f"REVIEW {line}")
for line in failures:
print(f"BLOCK {line}", file=sys.stderr)
sys.exit(1 if failures else 0)
The licence tag on a hub page is self-reported metadata, so treat a passing scan as a filter rather than a finding. It catches the obvious cases at speed. Anything landing in the review bucket gets a human reading the actual LICENSE file at that revision, once, with the outcome recorded in the inventory.
Wire it into the pipeline so it runs on every change to the inventory file.
# .github/workflows/model-policy.yml
name: model-policy
on:
pull_request:
paths: ['models.yaml', 'model-policy.yaml']
jobs:
licence-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- run: pip install huggingface_hub pyyaml
- run: python scripts/check_model_licences.py
- name: Store licence text alongside the pin
run: |
python scripts/fetch_licence_text.py --out licences/
git diff --exit-code licences/ || {
echo "Licence text changed upstream since the last pin."
exit 1
}
That last step is the one teams skip and the one that pays for the whole job. Fetching the licence text at the pinned revision and diffing it against a stored copy turns a silent upstream change into a failing build, which is the only way anyone finds out in time to act.
What a sanctions event would actually do to you
Working through the mechanics is more useful than worrying in the abstract, and the answer depends heavily on how you deployed.
If you self-host weights already on disk, a sanctions designation does not reach into your datacentre and delete them. Your immediate operational risk is low. What stops is everything downstream: updates, security patches to the model or its serving stack from that publisher, support, and in most interpretations your legal ability to continue commercial use. You are running frozen weights with no maintenance path, which is survivable for months and not indefinitely.
If you call a hosted API from the affected provider, service can stop with little notice, and your product stops with it. This is the acute case, and the mitigation is having a tested path to an alternative rather than a theoretical one.
If the model is embedded in a vendor’s product you buy, you may not even know you are exposed. Ask your vendors which models they serve and from where. A surprising number cannot answer quickly, which is itself informative.
The general lesson is that concentration risk in model choice looks exactly like concentration risk anywhere else, and the mitigations are the familiar ones. Keep prompts and tooling model-agnostic where the cost is low. Maintain an evaluation suite you can run against a replacement in a day rather than a quarter. Know, in writing, which model each production feature depends on.
Hosting modes and their risk profiles
Those three deployment shapes deserve a sharper treatment, because most production estates contain all of them at once and the mitigations differ by mode. Sorting your models by hosting mode is usually the fastest route to the exposure nobody knew about.
| Hosting mode | Who can stop you serving | Notice you get | Data leaves your boundary | Recovery after a designation |
|---|---|---|---|---|
| Publisher-hosted API | The publisher, their cloud, a regulator | Hours, sometimes none | Yes, every request | Rewrite the integration against another provider |
| Third-party inference provider serving open weights | The provider, their upstream | Days, usually | Yes, every request | Point at another provider or your own cluster, same weights |
| Cloud model catalogue (a hyperscaler’s managed endpoint) | The hyperscaler, the publisher | Days to weeks | Within your cloud tenancy | Catalogue removal still leaves you hunting for the weights |
| Self-hosted weights on your own accelerators | Nobody operationally, your lawyers legally | Legal effect immediate, technical effect none | No | Keep serving while you plan, if counsel agrees |
| Weights inside a vendor SaaS product | The vendor, silently | None; you may never be told | Depends on the contract | Contractual only, so read the exit clause now |
| On-device or edge deployment | Nobody, until the next app release | None | No | Shipped copies keep working, updates stop |
Read across that table and one row stands out. A third-party inference provider serving open weights is the only hosted mode where the artefact you depend on can move somewhere else without a rewrite, because the weights are the same weights everywhere. That portability is worth a great deal, and it evaporates if you never mirrored the files. Teams pick an open-weight model specifically for portability, consume it exclusively through one vendor’s API, and throw away the property they chose it for.
Two rows deserve separate attention from a security lead.
The cloud catalogue row looks safer than it behaves. A managed endpoint gives you tenancy guarantees and a familiar billing relationship, and none of that helps when the catalogue entry disappears because a publisher’s terms changed. Catalogue availability is a commercial decision made by two companies, neither of which is you.
The vendor SaaS row is where most organisations carry unmeasured exposure. Your support desk assistant, your code review bot, and your document summariser each run somebody’s model, and the answer is frequently a model your own intake process would have rejected. Send vendors a short questionnaire: which models serve our data, at what version, hosted where, under which licence, and what is your fallback. Log the answers in the same inventory as your own models. Vendors who reply within a week are fine. Vendors who cannot answer at all have told you something useful.
Naming the adversary in each scenario
Supply chain conversations drift into vagueness when nobody says who is doing the attacking. The controls in the rest of this article defend against six distinct actors with different goals, and knowing which actor a control addresses stops you from buying the wrong thing.
| Adversary | What they want | What they touch | Control that actually helps |
|---|---|---|---|
| A government acting on policy | Nothing from you; you are collateral | Legal availability of the dependency | Mirrored weights, a tested fallback, a named owner |
| A compromised or malicious publisher account | Code execution on your training and serving hosts | The checkpoint behind a repository name | Revision pinning, hash verification, signature checks |
| A typosquatter or lookalike repository | To be pulled by a hurried engineer | Your download step | A repository allowlist and mirror-only pulls |
| An insider in your own pipeline | Quiet modification of a deployed model | Your artefact store | Re-verification at deploy, internal signing at promotion |
| A data poisoner upstream of the release | Model behaviour under chosen triggers | The training set you cannot see | Evaluation suites, red-team probes, blast-radius limits |
| A litigant | Settlement pressure and damages | Your records, or your lack of them | Stored licence text, provenance notes, dated decisions |
The first row is unusual in security work because the adversary has no interest in you and cannot be deterred. Nothing you build makes a sanctions designation less likely. Everything you build changes how long you keep serving afterwards, which reframes the whole exercise as continuity engineering rather than defence.
Rows two through four are ordinary software supply chain attacks wearing new file extensions, and the countermeasures transfer directly from package management. Pin the version, verify the bytes, verify who signed them, refuse formats that execute on load.
Row five has no good answer today. A backdoor trained into weights cannot be found by reading the file, and the detection research remains too immature to run in CI. Assume you will miss it, then design so a compromised model output cannot achieve much alone: no unreviewed tool calls with side effects, no model output flowing straight into a shell or a SQL string, and a human in the loop wherever the action is irreversible. Containment is the same argument you would make for any component you cannot audit.
Row six shapes what you write down. A dated decision record explaining why a model was approved, by whom, under what licence, costs an hour now and is close to unreconstructable under discovery two years later.
Portability is cheaper before you need it
The engineering that makes a model swap survivable is unglamorous and mostly consists of not coupling to one provider’s specifics.
Keep the model call behind an interface of your own, so a change of provider touches one module. Avoid depending on provider-specific features in the critical path unless the value is large and you have accepted the lock-in explicitly. Record which model version produced which output, so that when behaviour changes after a swap you can tell what changed.
The evaluation suite is where the real work lives, and it pays for itself independently of any sanctions scenario. A set of a few hundred task-specific cases with a scoring function tells you within an hour whether a candidate model is acceptable. Without it, a forced migration becomes weeks of vibes-based comparison under deadline pressure, which is how teams end up shipping regressions.
# The interface that makes a swap a config change rather than a project.
class ModelClient(Protocol):
def complete(self, messages: list[dict], *, max_tokens: int) -> str: ...
PROVIDERS: dict[str, ModelClient] = {
"primary": AnthropicClient(model="claude-opus-4-8"),
"fallback": SelfHostedClient(endpoint="http://vllm.internal:8000"),
}
def generate(messages, provider=None):
name = provider or os.environ.get("MODEL_PROVIDER", "primary")
return PROVIDERS[name].complete(messages, max_tokens=2048)
Two things make this worth the small indirection. Switching provider becomes an environment variable, which means you can test the fallback path in staging regularly rather than discovering it is broken during an incident. And having a self-hosted option in the map at all forces you to keep one working, which is the actual insurance.
Pin the revision, not the name
Portability assumes you can get the same model back, and a repository name gives you no such guarantee. meta-llama/Llama-3-8B points at whatever sits on the default branch this morning. Model hubs are git repositories underneath: weights get re-uploaded, quantisations get replaced, and tokeniser configs change more often than anyone expects. A deployment that downloads by name runs whatever the publisher pushed last, which is a build reproducibility problem before it is a security one.
Pin to a commit.
from huggingface_hub import snapshot_download
LOCAL = snapshot_download(
repo_id="meta-llama/Llama-3-8B",
revision="e1945c40cd546c78e41f1151f4db032b271faeaa", # commit sha, never "main"
allow_patterns=["*.safetensors", "*.json", "tokenizer*", "LICENSE*"],
ignore_patterns=["*.bin", "*.pt", "*.pth", "*.msgpack"],
local_dir="/mnt/models/llama-3-8b",
)
The ignore_patterns line does real work. It refuses pickle-format artefacts at download time, so a repository shipping both formats hands you the safe one and nothing else. Set it once in a shared helper and every team inherits the policy without reading a wiki page.
Record the pin in a lockfile that lives in the repository, next to your other lockfiles, reviewed like any other dependency bump.
# models.lock.yaml
models:
- id: summariser-primary
repo: meta-llama/Llama-3-8B
revision: e1945c40cd546c78e41f1151f4db032b271faeaa
licence: llama3
licence_text: licences/llama-3-8b-e1945c40.txt
manifest_sha256: 9f2c1b0e7a4d5f83c6b1e0d29a7f4c8b3e5d6a1c9f0b2e4d7a3c5f8b1e6d0a92
mirror: s3://ml-artifacts/models/llama-3-8b/e1945c40/
format: safetensors
owner: platform-ml
Resolving a name to a commit takes one API call, so leaving it out of intake is laziness rather than friction.
python - <<'PY'
from huggingface_hub import HfApi
print(HfApi().model_info("meta-llama/Llama-3-8B").sha)
PY
A trap sits underneath all of this. A pinned revision protects you against upstream changes and does nothing about the files being deleted or the repository going private, which happens regularly for reasons ranging from licence disputes to a researcher having second thoughts. The pin tells you precisely what you lost. Getting it back is a separate control.
Mirroring weights you cannot afford to lose
A production model you can obtain only from a public hub is a dependency with a single point of failure, operated by somebody else, for free, with no contract behind it. Mirror it. The storage bill is small enough to be uninteresting, and the alternative is explaining why a customer-facing feature broke because a repository went private on a Saturday.
The mirror step belongs in intake, immediately after verification, and the mirror should be the only place production infrastructure ever pulls from.
#!/usr/bin/env bash
# mirror-model.sh REPO REVISION run in the intake sandbox, never on a serving host
set -euo pipefail
REPO="$1"; REV="$2"
DEST="/scratch/${REPO//\//_}-${REV:0:12}"
BUCKET="s3://ml-artifacts/models/${REPO}/${REV}"
python - "$REPO" "$REV" "$DEST" <<'PY'
import sys
from huggingface_hub import snapshot_download
snapshot_download(
repo_id=sys.argv[1], revision=sys.argv[2], local_dir=sys.argv[3],
allow_patterns=["*.safetensors", "*.json", "tokenizer*", "*.md", "LICENSE*"],
ignore_patterns=["*.bin", "*.pt", "*.pth"],
)
PY
# One manifest for the whole directory, sorted so the hash is stable across runs.
( cd "$DEST" && find . -type f ! -name 'manifest.sha256' -print0 | sort -z \
| xargs -0 sha256sum > manifest.sha256 )
sha256sum "$DEST/manifest.sha256" | awk '{print $1}' > "$DEST/MANIFEST_SHA256"
aws s3 sync "$DEST" "$BUCKET" --only-show-errors
echo "mirrored $REPO@${REV:0:12} -> $BUCKET"
echo "manifest: $(cat "$DEST/MANIFEST_SHA256")"
Hashing a directory rather than a single file matters more than it sounds. A serving stack loads weights, a tokeniser, a generation config, and increasingly a chat template, and changing any one of those changes model behaviour. Teams that hash only the .safetensors shards collect a green tick while the file that actually moved slips through unnoticed.
Mirroring counts as a control only when production cannot reach the original.
- Block egress to public model hubs from training and serving subnets at the network layer, not in application config.
- Set
HF_HUB_OFFLINE=1andTRANSFORMERS_OFFLINE=1in serving images so a stray call fails loudly instead of quietly downloading. - Point
HF_HOMEat a read-only volume populated from your mirror. - Fail the container build when the model directory is missing, rather than fetching at container start.
Storage arithmetic makes the decision trivial. A 118B mixture-of-experts model in bf16 occupies roughly 240 gigabytes; three of those plus a handful of fine-tunes fits inside a terabyte, which costs less per month at commodity object storage rates than an hour of the engineering time you would spend recovering from a deletion. Keep the last two approved revisions of each model rather than only the current one, so a rollback never needs an internet round trip.
The same discipline applies to your own fine-tunes, and teams skip it because the artefact feels internal. A fine-tune inherits the base model’s licence obligations, depends on a base checkpoint that can vanish, and is usually the least documented artefact in the estate. Mirror the base, store the training config, and keep both next to the adapter weights.
Choosing between self-hosting and a hosted endpoint
Pinning and mirroring presume you decided to run the weights yourself, and that decision deserves its own reasoning rather than being inherited from whoever set up the first prototype. The trade sits between operational cost you control and continuity risk you do not.
Four of the five leaves converge on the same box, which is the reason to draw it. Whatever you decide about who runs the GPUs, you still pin a revision, verify the artefact, and keep a copy. Hosting is an operations decision. Provenance is a security decision, and it applies in every branch.
The branch teams get wrong most often is the bottom right. An open-weight model behind somebody else’s endpoint feels like the low-effort option, and it is, right up to the moment that provider has an outage or a legal problem. Pre-integrating a second provider costs an afternoon: the same weights, a different base URL, a credential, and a scheduled smoke test that exercises the second path. Teams who do this find out their fallback broke during a Tuesday cron run rather than during an incident.
Self-hosting carries a cost that rarely makes it into the business case, and it deserves stating plainly. You become responsible for the serving stack, which means CVEs in vLLM, TensorRT-LLM, or whichever runtime you chose, kernel and driver updates on GPU hosts, and capacity planning for a workload with unpleasant tail latency. Real work for a platform team, every quarter, forever. The compensating benefit is that no external party can switch your product off, which for some workloads is worth several engineers.
For everyone else a middle position deserves naming: hosted for the steady state, self-hosted capacity that exists and gets exercised, sized for degraded service rather than full load. Serving a reduced feature set on your own hardware for a fortnight while you migrate is a completely different outcome from being down, and it costs a fraction of running full-capacity inference all year.
Weights are a binary you cannot inspect
The integrity question deserves more attention than it usually gets, because the normal tools do not apply.
With a library, a determined reviewer can read the source and form a judgement. With a set of weights, there is no source to read. A few gigabytes of floating-point numbers cannot be audited for backdoors by inspection, and the research on detecting behavioural implants in model weights is early and not something you can operationalise today.
That leaves provenance as the only practical control. Record the hash of exactly the artefact you downloaded, verify it against what the publisher states, and verify it again at deployment so that a tampered copy in your own artefact store is caught. This is basic and frequently skipped, because model downloads happen through convenience tooling that hides the artefact.
# Pin what you actually pulled, and check it again at deploy time.
sha256sum model-weights.safetensors | tee weights.sha256
# In CI, before serving:
sha256sum -c weights.sha256 || { echo "weights changed"; exit 1; }
Prefer safetensors over pickle-based formats where you have the choice. Python’s pickle format executes code during deserialisation, which makes loading an untrusted .bin or .pt checkpoint equivalent to running an untrusted program. safetensors exists specifically to remove that, and a checkpoint offered only in a pickle format on a model hub is worth treating with suspicion.
Mirror the weights you depend on. Public hubs remove models, publishers change licences, and repositories go private. A production dependency you cannot re-download is a dependency you should be storing yourself, and the storage cost is trivial next to the migration cost.
Signing and verifying weights end to end
A hash proves the bytes are identical to what you recorded earlier. Who produced them in the first place is a separate question, and signing is what closes it. The tooling here matured faster than most teams noticed, and it now amounts to a pip install rather than a research project.
Sigstore’s model transparency project ships a model-signing package that signs an entire model directory and records the signing event in a public transparency log. Keyless signing binds the signature to an OIDC identity, so verification asks a question with a human answer: was this signed by the release identity we expect, from the provider we expect.
pip install model-signing
# Sign a model directory using a workload or human OIDC identity.
model_signing sign /mnt/models/llama-3-8b
# Verify against the identity you expect, rather than "some valid signature".
model_signing verify /mnt/models/llama-3-8b \
--signature model.sig \
--identity "[email protected]" \
--identity-provider "https://accounts.google.com"
Inside your own estate an offline key is often the better fit, because the claim you care about is your own promotion decision.
# Intake host: sign the artefact once review has passed.
model_signing sign key /mnt/models/llama-3-8b --private-key /keys/model-intake.priv
# Serving host: refuse to start without a valid internal signature.
model_signing verify key /mnt/models/llama-3-8b \
--signature model.sig --public-key /keys/model-intake.pub \
|| { echo "unsigned model artefact, refusing to serve"; exit 1; }
Signing at two boundaries answers two different questions, and conflating them produces a weak control. A publisher signature, where one exists, tells you the artefact came from that publisher. Your own signature tells you the artefact cleared your intake process, which is the claim your serving hosts should enforce. Google has been signing its released models through Sigstore and the practice is spreading, so check upstream signatures where they are offered and design as though they are absent.
Make it a gate rather than a habit.
# deploy/model-gate.yml, runs before any pod pulls weights
steps:
- name: pull-from-mirror
run: aws s3 sync "$MIRROR_URI" /mnt/models/candidate --only-show-errors
- name: verify-manifest
run: |
cd /mnt/models/candidate && sha256sum -c manifest.sha256 --quiet
- name: verify-internal-signature
run: |
model_signing verify key /mnt/models/candidate \
--signature model.sig --public-key /keys/model-intake.pub
- name: verify-inventory-pin
run: python scripts/assert_pin_matches_inventory.py /mnt/models/candidate
- name: promote
run: mv /mnt/models/candidate "/mnt/models/$MODEL_ID"
The fourth step catches process failures rather than attacks. Asserting that the artefact on disk matches the revision recorded in the inventory closes the gap where an engineer mirrors a newer checkpoint by hand and forgets the record, which happens far more often than tampering and creates exactly the same confusion during an incident.
Scanning checkpoints before they reach a GPU
Format choice is a stronger control than scanning, so settle the format policy first and treat scanners as a backstop for cases the policy could not prevent.
| Format | Executes code on load | Where you meet it | Residual risk |
|---|---|---|---|
| safetensors | No | Most current releases | Header parsing bugs, and nothing at all about behaviour |
pickle (.bin, .pt, .pth, .ckpt) | Yes, by design | Older PyTorch checkpoints, community re-uploads | Arbitrary code during deserialisation |
| GGUF | No | llama.cpp and desktop runtimes | Metadata and parser bugs in the runtime |
| ONNX | No for the weights themselves | Cross-runtime exports | Custom operators and external data files widen the surface |
| Keras H5 and TF SavedModel | Yes, via lambda layers and custom ops | TensorFlow estates | Code paths hidden inside the graph |
The rule that falls out of the table is short. Production serves safetensors or GGUF. Anything else gets converted once, in an isolated environment, or gets refused.
Conversion concentrates the risk, because loading a pickle checkpoint in order to convert it is precisely the dangerous operation. Do it in a container with no network route, no cloud credentials, and no path to your artefact store, then carry the output out.
docker run --rm --network none \
--read-only --tmpfs /tmp \
-v "$PWD/in:/in:ro" -v "$PWD/out:/out" \
-e HF_HUB_OFFLINE=1 \
convert-sandbox:latest \
python /opt/convert_to_safetensors.py /in/model.bin /out/model.safetensors
Scanners still earn their place ahead of that step. modelscan from Protect AI reads pickle, H5, and SavedModel artefacts and reports unsafe operators, while picklescan does a similar job and sits inside the malware scanning stack Hugging Face runs across uploaded files.
pip install modelscan
modelscan -p ./candidate --reporting-format json > scan.json
python - <<'PY'
import json, sys
issues = json.load(open("scan.json")).get("issues", [])
critical = [i for i in issues if i.get("severity") in {"CRITICAL", "HIGH"}]
if critical:
print(f"{len(critical)} unsafe operator findings, quarantining artefact")
sys.exit(1)
print("no high severity findings")
PY
Treat a clean scan as weak evidence. Researchers at JFrog and at Sonatype have each published bypass techniques against picklescan, which is the expected outcome for any tool trying to decide safety by inspecting a format designed to run arbitrary code. A scanner reporting nothing has told you it found nothing. Format policy removes the class of problem; scanning tells you somebody tried.
Hardware provenance is the next layer
The same week brought a Huawei-led consortium publishing documentation of full-parameter post-training of DeepSeek’s V4 family on Ascend chips, reaching 34.22% model FLOPs utilisation. The number is respectable and the strategic point is larger: training pipelines are being built that do not depend on US hardware.
For a security team, this matters because it decouples model availability from chip export controls, which cuts both ways. A model ecosystem that can train without Western silicon is less disruptable by export policy, which is stability. It also means the set of capable models available to any actor, including hostile ones, keeps growing regardless of hardware restrictions, which removes an assumption some threat models still rest on.
The planning implication is to stop treating compute restrictions as a durable brake on adversary capability. Whatever a frontier model can do for an attacker today, assume an approximately equivalent open-weight model will be freely downloadable within a year, running on hardware nobody controls.
The intake pipeline as one flow
Individually the controls above are each half an hour of work. Assembled, they form a pipeline a platform team can stand up in an afternoon and that most engineers never have to think about again, which is the property that makes governance survive contact with delivery pressure.
Two things about this flow matter more than its exact shape.
The evaluation gate sits late on purpose. Running a task suite before the artefact has been verified means measuring something whose identity you have not established, and the number cannot be attached to a pin afterwards. Verify, then measure, then record the measurement against the revision so it still means something six months later.
The loop at the bottom separates a control from a launch checklist. Acceptable use policies change, publishers change licences, fallbacks rot, and a quarterly pass over the inventory catches all three for an hour of somebody’s time. Put it on a calendar with a named owner, because unowned recurring work does not recur.
The gates map onto the adversaries listed earlier. Licence and legal review cover the litigant and the policy actor. Pinning, hashing, and signature checks cover the compromised publisher and the typosquatter. Format policy and scanning cover the malicious artefact. Evaluation covers the drift and quality failures that happen with no adversary present at all, which is statistically the thing most likely to hurt you this year.
Teams sometimes ask which gates they can drop under deadline. The honest answer is that the licence check and the pin stay because they cost nothing and their failure modes are legal, the mirror stays because recovery without it may be impossible, and everything else can degrade to a manual step performed by a named person who writes down what they did.
An inventory schema your team can copy
Every control above produces facts, and facts without a home get lost. An inventory is the cheapest artefact in this article and the one that makes the others usable during an incident, because the first question anybody asks at 2am is which systems are affected.
# models/inventory.yaml, one entry per deployed model, changed only by pull request
- id: support-summariser
status: production
purpose: Summarises inbound support tickets for triage
owner_team: customer-platform
approver: k.tunca
approved_on: 2026-05-14
model:
repo: meta-llama/Llama-3-8B
revision: e1945c40cd546c78e41f1151f4db032b271faeaa
format: safetensors
quantisation: none
manifest_sha256: 9f2c1b0e7a4d5f83c6b1e0d29a7f4c8b3e5d6a1c9f0b2e4d7a3c5f8b1e6d0a92
signed_by: model-intake-key-2026
mirror: s3://ml-artifacts/models/llama-3-8b/e1945c40/
legal:
licence_id: llama3
licence_text: licences/llama-3-8b-e1945c40.txt
commercial_use: permitted
redistribution: permitted-with-attribution
use_restrictions: aup-2026-03.md
aup_last_reviewed: 2026-07-01
publisher_jurisdiction: US
training_data_notes: provenance/llama-3-8b.md
operations:
hosting_mode: self-hosted
serving_stack: vllm 0.9.2
endpoints: [ml-serve-prod-eu, ml-serve-prod-us]
data_sensitivity: customer-content
eval_suite: evals/support_summarisation/
eval_pass_threshold: 0.82
last_eval_score: 0.87
continuity:
fallback_model: mistralai/Mistral-Small-3
fallback_hosting: hosted-api
fallback_last_tested: 2026-06-30
rto_hours: 8
degraded_mode: 'Extractive summary from a classical model, no generation'
Every field in that schema exists because somebody needed it during an incident and could not find it. The fallback_last_tested date reveals more about an organisation than any policy document, because a date older than a quarter means the fallback is a document rather than a control.
Keep the inventory in git, in the same repository as the policy file, and require a pull request to change it. Reviews produce the audit trail for free, and a diff showing a revision bump is exactly the artefact a regulator or a customer security questionnaire asks for.
A standards direction here is worth tracking. CycloneDX has been extended with AI-specific component types, so an inventory like this can be emitted as a machine-readable AI bill of materials instead of living only in your repository. CISA and its G7 partners published guidance in 2026 applying supply chain transparency principles to AI systems, covering models and versions, training and fine-tuning data provenance, infrastructure dependencies, and external services. Customers have started asking for that material during procurement, so the inventory you build for your own incidents ends up doing double duty in sales. Generate the AI-BOM from the inventory rather than maintaining two sources of truth by hand.
The intake review form
Schemas record decisions. Somebody still has to make them, and a short form keeps that consistent across teams and reviewers. Paste this into the pull request template that adds a model to the inventory.
## Model intake review
**Model:** ****\*\*****\_\_****\*\***** **Revision (sha):** ****\*\*****\_\_****\*\*****
**Requested by:** **\*\***\_\_\_\_**\*\*** **Use case:** ****\*\*****\_\_\_\_****\*\*****
### Legal
- [ ] Licence identified from the LICENSE file at this exact revision, not the hub tag
- [ ] Licence text copied into `licences/` and committed
- [ ] Commercial use permitted for our deployment shape
- [ ] Redistribution position understood (do customers receive weights, or outputs?)
- [ ] Acceptable use policy read; no clause conflicts with the product feature
- [ ] Any user-count or revenue thresholds recorded, with current headroom
- [ ] Publisher jurisdiction recorded
### Integrity
- [ ] Pinned to a commit sha, not a branch or a tag
- [ ] Format is safetensors or GGUF (conversions done in the offline sandbox)
- [ ] Directory manifest hashed, including tokeniser, config, and chat template
- [ ] Upstream signature verified where the publisher provides one
- [ ] Scanner run, findings triaged, no unresolved high or critical items
- [ ] Signed with the internal intake key after review
### Operations
- [ ] Mirrored to internal storage; production egress to public hubs blocked
- [ ] Evaluation suite exists for this use case, with an agreed pass threshold
- [ ] Baseline eval score recorded against this revision
- [ ] Fallback model named and integrated behind the same interface
- [ ] Fallback exercised end to end in staging within the last quarter
- [ ] Owning team and on-call rota named
- [ ] Degraded mode defined for the case where no model is available
**Unknowns block approval.** Record any item you cannot answer, with the person
who owns getting the answer and a date by which they will have it.
Reviewer: ****\*\*****\_\_****\*\***** Date: \***\*\_\_\*\***
That last instruction carries the weight. A review form where “unknown” passes becomes a formality within two sprints, and the consequence surfaces later as a model nobody can characterise sitting in a revenue path.
Keep the form short enough that a reviewer finishes it in twenty minutes with the model directory open in another window. Every item maps to a step the pipeline already automates, so most boxes get ticked by reading CI output rather than by fresh investigation. Human judgement concentrates in three places: whether the use restrictions touch what your product does, whether the evaluation threshold is honest for the task, and whether the named fallback is genuinely good enough to put in front of customers.
Organisations adopting something like this find the first pass painful and every subsequent one routine, because the expensive work is characterising models already in production without records. Budget a week for that backlog, then treat each new model as a twenty minute task.
Runbook: a model dependency goes dark
Continuity plans that exist only as principles collapse under time pressure, so write the specific one down. This runbook covers any event removing your legal or technical ability to keep using a model: a sanctions designation, a licence revocation, a repository deletion, a provider shutting down, or a vendor dropping the model from a catalogue.
The first hour is pure lookup. Who does this touch, in which systems, serving which customers, under which contracts. An inventory answers that in minutes. Without one, the first hour becomes a Slack thread asking whether anyone remembers what the search ranking service runs, and the second hour becomes grep across forty repositories.
The first day runs legal and technical work in parallel, and the sequencing matters. Counsel decides whether continued use is permitted, and engineering prepares the cutover regardless of that answer, because preparing a fallback you turn out not to need costs an afternoon while the reverse costs a week of outage. Do not delete the weights while the question is open. A designation may arrive with a wind-down period, deletion is irreversible, and legal hold obligations can attach to both the artefact and its records.
The first week is migration and honesty. Cut over, watch the evaluation metrics alongside the user-facing ones, and tell customers what changed if quality moved. Teams consistently underestimate how much of the recovery is communication: support scripts, a status page entry, and account managers who need one sentence they can say without checking.
Three preparations decide whether this is a bad week or a bad quarter, and all three are cheap to build in advance.
- A query mapping publisher to affected systems, runnable against the inventory in under a minute.
- A fallback path that ran successfully in staging within the last ninety days, with the run recorded and dated.
- A named decision maker for the trade between quality regression and feature disablement, agreed with product beforehand rather than negotiated mid-incident.
Run the whole thing as a tabletop once a year. Pick a real publisher from your inventory, declare them designated at nine in the morning, and time the lookup. The exercise reliably surfaces the same two gaps: a vendor product nobody mapped, and a fallback that stopped working three releases ago.
Anti-patterns that keep showing up
These failures repeat across organisations with striking consistency, which makes them easy to check for in an afternoon. Each one has cost somebody real time.
Pinning to main. The download call passes revision="main" or omits the argument, and the deployment runs whatever the publisher pushed most recently. The fix is a commit sha in a lockfile. The tell is a model that behaves differently after a rebuild with no code change.
Trusting the licence tag. The hub page says Apache-2.0, the LICENSE file in the repository says something narrower, and the model card adds a use restriction in prose that no scanner reads. Self-reported metadata is a filter for automation; a human reads the file at the pinned revision before anything ships.
Assuming a fine-tune is yours. A fine-tune of a restricted base inherits that base licence, including naming and use clauses. Training on your own data changes nothing about the terms attached to the weights you started from. Teams routinely treat internal fine-tunes as unencumbered assets and skip intake entirely.
Mirroring the weights and forgetting everything else. The .safetensors shards sit in the bucket while the tokeniser, generation config, and chat template still come from the hub at container start. The model then fails to load, or worse, loads with a subtly different chat template and produces degraded output nobody traces back to the right cause.
Treating scanning as the control. A green modelscan result becomes the justification for allowing pickle checkpoints in production. Scanners match known-bad patterns in a format built to execute code, and published bypasses exist for the popular ones. Format policy removes the class; scanning reports attempts.
Community quantisations with no lineage. A convenient four-bit re-upload from an unknown account gets deployed because it fits the available GPUs. Nobody can say which base revision it came from, what calibration data was used, or whether anything else changed along the way. Quantise the base model you already verified, in your own pipeline, and record the recipe in the inventory.
Egress blocked in config only. HF_HUB_OFFLINE=1 lives in a values file that a debugging engineer overrode last month, and the serving pod has been fetching from the internet ever since. Network policy at the subnet level is the control; environment variables are a convenience.
The unowned inventory. A spreadsheet built during a compliance push, accurate for six weeks, then quietly wrong forever. Inventories survive when they live in git, gate deploys, and break the build when they drift from reality.
A fallback nobody has run. Every organisation has one. The configuration exists, the code path exists, and the last successful execution happened during the original implementation. Schedule it, alert when the schedule fails, and surface the date in the inventory where a reviewer will trip over it.
One pattern connects all nine: a control designed correctly and then never exercised. Model supply chain security contains very few hard technical problems and a great many maintenance problems, which is encouraging, because scheduling maintenance is something organisations already know how to do.
A short governance checklist
Turning this into practice does not require a large programme. Four artefacts cover most of the exposure.
Keep an inventory of every model in production, including those inside vendor products, with the version, the licence, the hosting location, and the owning team. Most organisations discover during this exercise that they have more models than they thought and that at least one is research-licensed.
Record a hash for every set of weights you self-host, verified at download and re-verified at deploy. Store licence text alongside the weights rather than trusting a link that may change.
Keep an evaluation suite per production use case, runnable on demand against any candidate model, with a documented pass threshold agreed by whoever owns the feature.
Write down the fallback for each production model and test it quarterly. A fallback that has never been exercised is a plan, not a control.
None of this is exotic, and all of it is the same discipline applied to third-party libraries for twenty years. The category is new enough that the discipline has not arrived yet, which is why a policy announcement about distillation caught so many teams without an answer to a question they should have been able to answer in minutes.