Skip to content
Zingaro AI
Engineering blog · 30 May 2026 · 5 min read

Updating models inside an air-gapped network

No outbound connection means no pulls, no telemetry and no "just download it". How we package, verify and roll a model update through a network that cannot see the internet.

By Zingaro AI Engineering

  • Practical
  • deployment
  • sovereign
  • mlops

An air-gapped deployment is one where the machines running the models have no route to the internet. Not a firewall rule. No route. It is the strictest shape of sovereign deployment, and it is more common than people expect: defence, critical infrastructure, some ministries, some banks.

Running models in that setting is easy. Models do not need the internet to run. Updating them is where the work is, because every habit the AI ecosystem has, from pip install to from_pretrained to "the container pulls the weights on start", assumes a connection. This is how we do it without one.

What the inside cannot do

Write these down, because each one is a place a well-meaning engineer will try to punch a hole.

  • No package index. No PyPI, no npm, no apt mirror unless you bring one.
  • No container registry. No docker pull.
  • No model hub. No from_pretrained("org/model"), and no tokeniser or config fetched on first use.
  • No telemetry, no licence checks, no "call home to see if there is a newer version".
  • No time sync from the outside, which matters more than it sounds for certificate checks.

The bundle

Everything a release needs crosses the gap once, as one artefact, with a manifest. Ours looks like this.

ItemFormatWhy it is there
Runtime imageOCI image, saved as a tarballThe serving stack, pinned, with every dependency baked in
WeightsSafetensors, sharded, with checksumsThe model itself; never inside the image, so it can be updated alone
Tokeniser and configFiles alongside the weightsSo nothing is fetched at load time
Eval reportJSON plus a human summaryProof the candidate passed the gate before it left our side
SBOMSPDX or CycloneDXSo the receiving security team can review what is inside
SignaturesSigstore bundle, offline keySo the inside can verify the bundle without us or the internet
RunbookMarkdownInstall, verify, roll out, roll back, in that order

The image never contains the weights. Images change rarely; weights change with every model version. Keeping them separate makes the common update small and the review focused.

Build and sign, on the outside

The build script produces the bundle and signs it with a key whose public half was installed inside the network when the deployment was first set up. That first exchange happens in person. After that, every update can be verified inside without any further trust in the transport.

#!/usr/bin/env bash
set -euo pipefail
VERSION="$1"                       # e.g. 2026.05.30-tooluse-v7
OUT="bundle-${VERSION}"
mkdir -p "$OUT"
 
# 1. Runtime image, pinned by digest, saved as a tarball.
docker pull "registry.example.com/serve@${IMAGE_DIGEST}"
docker save "registry.example.com/serve@${IMAGE_DIGEST}" -o "$OUT/serve.tar"
 
# 2. Weights, tokeniser, config: everything the loader touches.
cp -r "/models/${VERSION}/" "$OUT/model/"
 
# 3. Evidence: the eval report this exact version passed.
cp "evals/reports/${VERSION}.json" "$OUT/eval-report.json"
syft "registry.example.com/serve@${IMAGE_DIGEST}" -o spdx-json > "$OUT/sbom.spdx.json"
 
# 4. Checksums over every file, then a signature over the checksum file.
(cd "$OUT" && find . -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum > SHA256SUMS)
cosign sign-blob --key "$SIGNING_KEY" --bundle "$OUT/SHA256SUMS.sigstore" "$OUT/SHA256SUMS"
 
tar -cf "${OUT}.tar" "$OUT"
sha256sum "${OUT}.tar" > "${OUT}.tar.sha256"
echo "bundle ready: ${OUT}.tar"

The bundle then moves across the gap by whatever the site allows: approved removable media, a data diode, a one-way transfer appliance. That step belongs to the client's security team, and the runbook says so.

Verify, on the inside

Nothing is loaded until the bundle verifies. The verification is a script, not a checklist, so it is run the same way every time.

#!/usr/bin/env bash
set -euo pipefail
BUNDLE="$1"
sha256sum -c "${BUNDLE}.sha256"                                     # transport integrity
tar -xf "$BUNDLE" && cd "${BUNDLE%.tar}"
cosign verify-blob --key /etc/zingaro/release.pub \
  --bundle SHA256SUMS.sigstore SHA256SUMS                             # authenticity, offline
sha256sum -c SHA256SUMS                                               # every file, unchanged
jq -e '.gate.passed == true' eval-report.json > /dev/null             # it passed the gate
docker load -i serve.tar
echo "verified: $(basename "$PWD")"

If any line fails, the update stops and the previous version keeps running. There is no "verify later".

Roll out in stages

Air-gapped does not mean brave. The new version goes through the same stages as anywhere else, just with the bundle as the source of truth.

  1. Shadow. The new model receives a copy of live traffic and its outputs are recorded, not used. Compare against the running version for a day.
  2. Canary. A small share of real traffic, with the review queue watching the exceptions closely.
  3. Full. The rest, and the previous version stays installed.

Two versions are always present on disk. Rollback is a symlink change and a restart, and it is rehearsed during the install, not discovered during an incident.

Logs stay inside

Nothing in the runtime image is allowed to open an outbound connection, and the network confirms it, but the discipline matters even so: no crash reporters, no usage pings, no "anonymous statistics". Logs go to the client's own collector. When we need to debug something, the client exports what they choose to export, reviewed, across the gap in the other direction.

Things that go wrong

  • Lazy fetches. A library that quietly downloads a tokeniser, a config or a small auxiliary model on first use. Every load path is exercised on a disconnected test machine before the bundle is built, and the error you want is a loud one.
  • Cached credentials and hub tokens left in an image. The SBOM review catches some of this; a grep of the image layers catches the rest.
  • Clock drift. Certificate and signature checks depend on time. An air-gapped network needs its own time source, and the runbook checks it first.
  • Bundle size. Weights are large and removable media is slow. Shipping a weight delta helps when only an adapter changed; a full bundle is the default because it is simpler to verify.
  • Version confusion. The running version must be printed on every dashboard and in every log line. When there is no internet, there is also no "check the registry".

None of this is exotic. It is the ordinary discipline of releasing software, applied with no shortcuts available. That constraint turns out to be clarifying: the process that works with no connection is the process that should have been running all along.

Built with
OCIDockercosignBash
Author

Zingaro AI Engineering

The engineers who train, serve and run the work: models, inference, speech and deployment.

About the teamRSS

Share
LinkedInX
This is the work we do

Bring us the hard part. We build, train and run it, on your hardware if the rules require.

Book a call
Read next
All pieces
Book a call

Bring us the hard part.

Inference, speech, post-training, deployment. Twenty minutes is enough to say whether we can take it.

A pilot starts within 5 working days of agreed scope · Nothing upfront · No seat licences