Here's a question that only shows up once you've actually tried to ship something that runs permanently disconnected: if your software never talks to the internet again after the day it's installed, what replaces your vulnerability feed?
Most of the dependency tooling I've used assumes a live connection. A registry you can query at 3am when a CVE drops, a scheduled dependency audit, an auto-patch pipeline that pushes a fix to production before most users have noticed anything happened. All of that is a safety net that catches problems after the fact. Take the network away properly (not 'usually connected but pretending otherwise') and that net disappears. Whatever ships in the build is what runs, often for months, with nothing watching it from the outside.
Front-loading trust instead of maintaining it
The practical effect is that disconnected software has to front-load almost all of its supply-chain trust decisions, rather than spreading them across the life of the deployment. In a connected system you can afford to be a bit loose about what gets pulled into a build, because continuous scanning and a patch pipeline sit behind you as a correction mechanism. Offline, there is no correction mechanism. So the question stops being 'how fast can I react' and becomes 'how sure am I before I ship'.
Allow-listing dependencies before they're a problem
The pattern I've settled on is to turn dependency management from a detection problem into an admission problem. Instead of pulling in whatever the package manager resolves and scanning the result afterwards, you maintain an explicit allow-list of exactly which packages, at which versions, with which hashes, are permitted to enter a build at all. A new dependency doesn't get scanned after the fact, it gets reviewed and added to the manifest before CI will even fetch it. It's a small shift in wording, deny unless approved rather than allow unless flagged, but it changes what a dependency bump actually looks like: a reviewed, auditable diff to a manifest file, not a background process nobody reads.
allowlist = load_yaml("deps.allowlist.yaml")
lockfile = parse_lockfile("package-lock.json")
for pkg in lockfile.packages:
entry = allowlist.get(pkg.name)
if entry is None:
fail(f"{pkg.name} not in allow-list, needs review before it can ship")
if entry.version != pkg.version:
fail(f"{pkg.name} version drift: allowed {entry.version}, got {pkg.version}")
if sha256(pkg.tarball) != entry.hash:
fail(f"{pkg.name} hash mismatch: possible tampering or a silent republish upstream")
build()
That check is deliberately boring. It doesn't need a threat feed or anything clever, just a manifest, a lockfile and a hash comparison. The interesting work happens earlier, when a human decides whether a new dependency goes into that manifest in the first place, and on what schedule anyone revisits the ones already there.
The SBOM is the record, not the paperwork
Generating a software bill of materials, CycloneDX or SPDX, either works, gets talked about as a compliance exercise you produce because a customer's procurement team asked for one. For disconnected software it's closer to the only audit trail you will ever have. There's no live inventory service to query later. When a new CVE surfaces for a library you bundled eight months ago, the SBOM from that specific release is the only quick way to know which of your shipped versions actually contain it. I generate it directly from the same lockfile and allow-list manifest that CI already checks, so there's no separate process that can quietly drift from what actually got built. Diffing SBOMs between two releases is also just genuinely useful on its own: it tells you exactly what changed, dependency by dependency, which is a far more honest release note than 'various fixes and improvements'.
Signed installers instead of a live update check
The other half of a connected system's trust model is usually 'only accept updates signed and pushed from my own servers'. You can keep the signing, you just lose the pushing. What I ended up with for SIOS, the platform I build at Mickai, is a release signed with a key kept well away from any build machine, with the public key distributed out of band so it can be checked independently of the installer itself. The verification step is simple and happens once, by whoever is doing the install, often onto a machine with no outbound network path at all.
installer = read_file("release-4.2.bin")
signature = read_file("release-4.2.bin.sig")
pubkey = load_pubkey("release-pubkey.pem")
if not verify_signature(installer, signature, pubkey):
abort("signature verification failed: do not install this binary")
It's a manual step rather than an automatic one, and that's the point of it. In a disconnected environment, the moment of installation is the only point where the operator and the software are both present and able to check each other.
The honest tradeoff
None of this is free, and it's worth being straightforward about the cost rather than only talking up the upside. The whole model above is a deliberate trade: you give up continuous, hours-scale patching in exchange for a system that never calls home. A critical CVE in a dependency on your allow-list can, on a connected system, be patched and auto-pushed in under a day. Here, the same fix has to go through review, a rebuild, a fresh SBOM and re-signing, then physical distribution to machines that might be air-gapped or simply hard to reach. That can easily stretch to weeks, not hours. That's a real security cost, not a rounding error, and anyone running or recommending this kind of architecture should say so plainly rather than only advertising the parts that sound good.
It also doesn't remove the need for a human to keep watching upstream security advisories for whatever's on the allow-list. The list isn't a one-time decision, it needs revisiting on a schedule by someone whose job includes checking whether anything on it has picked up a disclosed vulnerability since the last review. Skip that and the allow-list quietly stops meaning anything, it just becomes a static file nobody interrogates. The honest version of this setup needs a fast lane too: a shorter, security-only rebuild and re-sign path that skips whatever isn't essential, so when something genuinely urgent turns up, the gap between disclosure and a distributable fix is measured in days rather than whatever your normal release cadence happens to be.
I'd genuinely like to know how other people running disconnected or air-gapped systems handle that gap between a CVE landing and a signed fix actually reaching a box. Is there a sane middle ground between fully offline and continuously patched, something that doesn't just quietly reintroduce the live connection you were trying to remove in the first place? Or is a slower, more deliberate update cadence simply the price of the offline guarantee, full stop?
Top comments (0)