Supply Chain Security (v0.60.0)
"Tensors you can ship through procurement." v0.60 layers 6 supply-chain controls on top of LoRA adapters so a security team can clear shipped weights for production.
soup adapters scan — spectral backdoor detector
Pure-numpy spectral analysis of LoRA weight matrices. Flags four patterns associated with weight-space trojans:
| Pattern | Warn | Fail |
|---|---|---|
| Rank-1 dominance | 50× | 200× |
| Top singular value / energy | > 75% | > 95% |
| Frobenius outlier (σ) | > 4σ | > 8σ |
| NaN / Inf | — | any |
soup adapters scan ./my_adapter --format json | jq '.overall'
# "OK" | "WARN" | "FAIL"Exit codes: 0 = OK, 1 = WARN (advisory), 3 = FAIL (CI gate).
soup adapters sign / verify — Merkle-root tamper detection
soup adapters sign ./my_adapter --backend unsigned
# → ./my_adapter/.soup-signature.json
soup adapters verify ./my_adapter # exit 1 on mismatch (lenient)
soup adapters verify ./my_adapter --strict # exit 3 on mismatch (CI-strict)The unsigned backend (default) computes a Merkle root over all adapter files for offline SHA-256 verification. ed25519 is live as of v0.71.2 — soup adapters sign --backend ed25519 --key <priv.pem> (or --generate-key, or SOUP_SIGNING_KEY) writes a real detached signature; soup adapters verify --public-key <trusted.pem> does a cryptographic verify that fails closed on any tamper or wrong key. sigstore (keyless OIDC, Fulcio and Rekor) is live as of v0.76.0; see Sigstore keyless signing below. Through v0.75.2 soup adapters sign --backend sigstore refused with exit 2 and soup attest emit --sign sigstore fell back to an unsigned statement and exited 0; neither is true any more (Governance has the attestation half).
soup adapters sign exits 2 for a refused argument (an unknown backend, --interactive-oidc without --backend sigstore, the [sigstore] extra missing, no ambient credential when the Sigstore trust root can be loaded) and 1 for a missing adapter directory or a signing service that failed at run time, which includes a trust root that cannot be loaded (for example offline), even when no credential is present either.
From v0.71.2 to v0.75.0 the
--public-keycheck could be walked straight past, and this page said otherwise. The trusted key was consulted only inside the ed25519 branch, so with--public-key trusted.pema record carrying any other backend was never compared against it and verified with exit 0, even under--strict. An adapter re-signed with the defaultunsignedbackend passed a check that was supposed to authenticate the signer, which is the failure the command exists to prevent. It is fixed in v0.75.1: with--public-key, a record whose backend is not ed25519 fails verification with a stated reason. Without--public-keynothing changed, and anunsignedrecord still verifies by Merkle root alone, as described above. If you gated a pipeline on--public-key, the gate was open; re-verify anything you accepted on an affected version.
Sigstore keyless signing (v0.76.0)
Install the extra: pip install "soup-cli[sigstore]" (sigstore>=4.4.0,<5.0.0; it is not part of [all]). Signing is keyless. It uses an ambient OIDC identity (for example GitHub Actions) to obtain a Fulcio certificate and submits to the Sigstore public-good Rekor log, so the signer's identity ends up in a public transparency-log entry. A browser sign-in is never opened implicitly on a headless run; pass --interactive-oidc to allow it.
soup adapters sign ./my_adapter --backend sigstore
soup adapters verify ./my_adapter \
--cert-identity 'https://github.com/acme/repo/.github/workflows/release.yml@refs/heads/main' \
--cert-oidc-issuer 'https://token.actions.githubusercontent.com'sign signs the Merkle root and stores the whole Sigstore bundle in .soup-signature.json: the signature field stays empty and sigstore_bundle holds the bundle. The same flag exists on soup attest emit --sign sigstore, which also needs --output; that command, its exit codes and the statement format are on the Governance page.
Verification is fail-closed and needs both values, supplied by you. --cert-identity and --cert-oidc-issuer are the policy you trust; the identity embedded in the bundle is not trusted on its own, because that would reduce authentication to self-consistency. A Sigstore record verified without both flags is reported invalid (exit 1, or 3 with --strict), either flag alone is a finding too, and --public-key with a Sigstore record fails because it requires an ed25519 signature. As with ed25519, a pass shows that the signer asserted this Merkle root; it does not show that the adapter is safe.
One difference between the two verify commands, so a CI gate does not misread it. soup attest verify gives a Sigstore verifier that is unavailable (for example the trust root cannot be fetched, or the extra is missing) its own exit 1 and a bad signature exit 3. soup adapters verify folds both into the same list of findings: a verifier outage and a forged signature both print a reason and exit 1 (3 with --strict). If you gate on that exit code, you cannot tell an outage from a bad signature without reading the printed reason. The argument and extra refusals on this page (exit 2 on sign, the missing-extra finding on verify, the attest verify exit codes) were run on the v0.76.0 CLI without the extra installed; the signing and the Fulcio and Rekor round trip were not, because that needs the extra and the network. Upstream's tests mock the signer and benchmarks/ has no end-to-end record, so read this as shipped in v0.76.0, not as validated.
soup adapters check-safetensors — refuse pickle at the boundary
soup adapters check-safetensors ./my_adapter --strict && echo "SHIP IT"Rejects any .bin, .pt, or pickle-format weights. The single biggest LoRA attack vector is pickle.load on hostile weights; --strict makes the CI gate hard.
Is the adapter alive: soup adapters check and audit
A different question from the backdoor scan: does the adapter do anything at all, and did the run that wrote it do what its config asked? soup adapters check reads the weights without PyTorch and ends in one line, alive or inactive: <reason>; it refuses a weights file over 2 GiB before opening it, because it loads every tensor into memory. soup adapters audit --config soup.yaml compares what a finished run recorded in adapter_config.json with the config. Both are on Adapter check and audit, with their exit codes and limits.
License-conflict gate in soup adapters merge
soup adapters merge a/ b/ c/ --output merged/ \
--license mit --license apache-2.0 --license mit \
--license-override "Approved by legal team 2026-05-20"33-entry SPDX-license compatibility matrix. Conflicting licenses require --license-override "<reason, ≥ 8 chars>" for audit.
Namespace-pin TOFU (anti-AI-Jacking)
Soup keeps a local trust-on-first-use pin of a Hugging Face repo's author, and only four paths consult it: soup spectrum scan <model-id>, soup probe sae-diff --auto-download, the base model of a stream_layers: true training run, and, since v0.76.0, a Hub id given as training.prm_reward (downloaded before training starts). All four download through Soup's own hub wrapper, which records (repo_id, author, created_at) in a SQLite store (~/.soup/namespace_pin.db; SOUP_NAMESPACE_PIN_DB moves it) the first time it sees a repo. A later download through that wrapper is refused if the author changed or if created_at changed in either direction (a pinned repo's creation time never legitimately changes, so any drift looks like a deleted-and-recreated repo).
This does not protect an ordinary training run. The base model that soup train (without stream_layers), chat, serve, merge, export and infer load is fetched by transformers directly and is never checked, so a hijacked organization could still serve you altered weights through any of them. The check also fails open: if the Hub metadata cannot be fetched (offline, rate-limited) or the SQLite store cannot be read, the download goes ahead. And there is no CLI flag to approve an author change; the override exists only as the allow_namespace_shift keyword argument of the Python wrapper. If a hijacked base model is in your threat model, download the base once, check it, and point base at that local directory.
soup airgap-bundle — physical-media transfer
soup airgap-bundle \
--output secure-bundle.tar \
--model ./llama3-8b-merged \
--dataset ./prod-logs \
--wheel ./wheelhouse \
--bundle-size-cap 50Packs model + datasets + wheels + kernels + manifest into a single tarball suitable for transfer through a data diode (physical-media / sneaker-net). Embeds a SHA-256 manifest per file. Refuses to build if total size > --bundle-size-cap (default 100 GiB).
Three details are easy to get wrong. --output is written as a plain, uncompressed tar whatever its extension, so name it .tar (a .tar.gz name produces a file that tar -xzf rejects as "not in gzip format"). --model, --dataset, --wheel and --kernel each take a directory (the last three are repeatable), and every path must sit under the working directory. And a single file passed to --dataset is accepted without an error and packs nothing: the summary panel still counts it under Datasets and the command exits 0, while the tarball lacks it. Compare the Files count in the panel, or run tar -tf secure-bundle.tar, against what you expect before you carry it across. --repro-receipt <json> embeds a receipt from soup train --repro-receipt, and is auto-detected from <model>/repro-receipt.json.
Reporting a vulnerability
This restates upstream's SECURITY.md at v0.76.0; that file is the authority if the two ever differ.
Supported versions. Security fixes are shipped for the latest published release. The policy marks 0.76.x as supported and everything below 0.76 as not, so a v0.75 install has no patched release to wait for: pip install -U soup-cli. No new advisory was published with v0.76.0. The two below are public and both fixes are carried in 0.76.0, so a user still on 0.75.0 or 0.75.1 should upgrade.
How to report. Report privately, and do not open a public GitHub issue for anything security-sensitive. The preferred route is a private report through GitHub Security Advisories. If you cannot use GitHub, email team@trysoup.dev (the project address), or makazanalpamys@gmail.com (the maintainer's personal address) if that bounces. Do not report in Discord: it is a public channel. Include the affected version or versions and your platform, a minimal reproduction or proof of concept, and the impact you observed.
What to expect. Upstream aims to acknowledge a report within 5 business days and to ship a fix or mitigation for a confirmed, in-scope issue "as promptly as is practical". That is a target, not a service-level agreement, and no other deadline is promised. Disclosure is coordinated: once a fix is released the reporter is credited by name in the release notes, unless they ask for anonymity. There is no bug bounty and no monetary reward; the credit is the whole of what is offered.
Scope. The threat model assumes the operator runs Soup on their own machine with their own data. In scope, as representative examples:
- path traversal or arbitrary file read or write from a user-supplied config, dataset or artifact path;
- SSRF in the synthetic-data providers, the inference server, or the hub and endpoint validators;
- command, Modelfile, Jinja chat-template, or systemd or launchd unit injection;
- secret leakage in logs, crash bundles or generated artifacts;
- sandbox escape in the RLVR code-execution reward path.
Out of scope: vulnerabilities in third-party model weights or datasets you choose to load; issues that require an already-compromised host; and the DNS and email configuration of trysoup.dev (a missing or permissive DMARC, SPF or DKIM record, and anything else a public DNS query establishes), which the project fixes but does not treat as a finding in Soup.
Advisories published so far, each with the page that says what changed and what to re-check:
- GHSA-63h4-gvp4-r26g, fixed in v0.75.1: an adapter with nothing in it, and eight hardening fixes.
- GHSA-9f49-hqpf-c849, fixed in v0.75.2: endpoint URLs can no longer name a private address.
Numbers
+152 new tests in v0.60.0. Security-fix coverage: 12 HIGH, 11 MEDIUM, 6 LOW.
See also
- Compliance pack — v0.71.35 puts signing, scanning and attestation on a HIPAA / SOC 2 / EU AI Act / SR 11-7 path.
- Governance — v0.59 BOM + SLSA-3 + audit log.
- Adapters — v0.57 diff / merge / blame / branch (the rest of the LoRA toolkit).
- Adapter lifecycle (v0.67) —
soup lockSHA256 closure makes signed + scanned adapter artifacts reproducible across the team;soup adapters prputs review + signing on every adapter change.
Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.