"No outbound connections" arrives as a line in a security questionnaire and reads like a firewall detail. It is not. Commercial software licensing rests on one assumption, that the installed copy can reach the vendor, and that sentence removes it. Everything the callback was doing has to be rebuilt to work one-way, offline, on a schedule the customer controls. Most of it is a few weeks of work. One piece is not, and knowing which piece is the whole design.

The requirement comes from more places every year. Banks and clearing houses. Hospital systems whose clinical network is separated by policy. Industrial operators with an OT segment that has never had a route to the internet and is not getting one for your product. And increasingly, ordinary enterprises whose security team has decided that a vendor binary opening a session to a vendor endpoint is an unreviewed data flow they now have to describe.
Their objection is procedural rather than paranoid. Under SOC 2 or ISO 27001 your outbound connection lands inside their control narrative and someone has to describe it every year. Under GDPR, a callback carrying user identifiers is a transfer that needs a named lawful basis. The cheapest answer available to a reviewer is no, and if your product does not run without the callback you are out of the evaluation before anyone reads your architecture.
You are probably here because
- A security questionnaire came back saying the install cannot make outbound connections, and your licence check is an HTTPS call to your own API.
- A deal is parked in security review because nobody there will sign off on your binary opening a session to your endpoint.
- Your last patch reached an offline customer as a zip file and a page of emailed instructions, and you cannot prove what they actually installed.
- Renewal is an argument instead of a number, because you do not know how many seats or nodes those customers are running.
All four are the same root cause — six separate jobs were bundled into one outbound request, so removing the request removes all six at once. The next section itemises them, and the rest of the article rebuilds each one offline.
What phoning home is actually doing
Before replacing it, itemise it. A vendor callback is usually doing six separate jobs that were bundled into one HTTPS request because they could be.
Entitlement. Deciding which features this customer paid for and whether the subscription is current.
Metering. Counting the thing you price on, so renewal and overage rest on a number rather than an argument.
Diagnostics. Crash reports and traces that let support answer a ticket without a screen share.
Update delivery. Getting new bytes onto the machine, including security patches you are obliged to ship.
Revocation. Turning something off quickly, because a subscription lapsed, a key leaked, or a build shipped with a defect.
Remote configuration. Flipping a flag or a limit without a release.
Rebuilt separately, they are not equally recoverable. Entitlement and update delivery come back almost whole. Metering comes back accurate enough to bill on. Diagnostics come back as a manual step, provided you make it one command rather than a page of instructions. Product analytics mostly do not come back. Revocation inside a day does not come back at all, and your licence terms should be designed around that.
How much of each job survives a no-egress requirement
Our planning estimates for a first offline release. Revocation is the row that changes your commercial terms, not your code.
The licence file: sign it, do not encrypt it
An offline licence is a signed statement of fact the software verifies locally. It is not a secret and will not stay private: the customer's security team is going to open it.
Sign it with an asymmetric key. Ed25519 gives a 32-byte public key and a 64-byte signature, fits anywhere, and verifies in microseconds; ECDSA over P-256 is the fallback where a validated cryptographic module has not caught up. The private key never ships, never sits in the repository and never lives on a laptop. It lives in a KMS or an HSM, and the issuance service calls a sign operation. The anti-pattern is encrypting a licence with a symmetric key compiled into the product: anyone with strings and an afternoon extracts it, and every licence you ever issued becomes forgeable with no path back except a mandatory upgrade for the whole installed base.
JWS in compact serialisation is a fine container and every language already has a verifier. Two rules make it safe. Pin the algorithm in your verifier rather than reading it from the header, because trusting the header is the family of bugs that produced alg: none acceptance and HMAC-versus-RSA key confusion across a decade of libraries. And put a key identifier in the header so you can rotate without a flag day. Keep the payload legible: base64 that decodes to readable JSON answers a security question by itself, and a proprietary binary format generates a meeting.
| Claim | What it carries | What breaks if you leave it out |
|---|---|---|
| licence_id | A unique identifier for this specific issued licence | You cannot tie a support ticket, a usage export or a renewal back to what was actually issued |
| entitlements | The feature set this customer bought, as flags | Every enable and disable becomes a code change and a release the customer has to accept |
| capacity | Seats, nodes, cores, documents, calls, whatever you price on | Metering has nothing to compare against, and overage is unprovable at renewal |
| nbf / exp / grace_until | Activation, expiry, and the separately stated end of grace | No term, so your only remaining lever is shipping a new build |
| issued_at / kid | Issue time and the identifier of the key that signed it | Key rotation turns into a coordinated upgrade across every deployment |
| binding | The anchor value and its mode: strict, warn, or none | You cannot loosen binding for one customer without cutting a release |
| support_window | The newest version this licence entitles | Perpetual-plus-maintenance contracts have no technical expression at all |
What to bind to, and how tightly
Node-locking made sense when software ran on a server somebody screwed into a rack. It makes much less sense against an installed base that autoscales, snapshots, restores into a DR region and rebuilds every pod on a Tuesday. The anchors people reach for first are the ones that change most often.
Binding anchors, ranked by how often they survive normal operations
Stability under ordinary events: image rebuild, autoscaling, snapshot restore, DR failover, node replacement.
Two of those deserve a note. /etc/machine-id is derived at first boot, so a golden image cloned without truncating the file gives every instance the same value, which is worse than one that changes. And a MAC address inside a container is assigned by the network plugin, so it is effectively random per pod.
The default we start from: bind to an organisation and a capacity, not a machine. Count what you price on, enforce the count, and let the customer run it wherever they run it. Where per-node pricing genuinely requires machine binding, pick the softest anchor that still counts, put the mode in the licence so you can relax it for one account without a build, and make rebinding self-service with an audit record instead of an email to support at 2am during a failover.
The clock belongs to them
Expiry is checked against a clock the customer administers, and there is no authority to appeal to. Design for that instead of against it.
Keep a monotonic high-water mark: the latest timestamp the software has ever observed, written to more than one place, typically a row in the product's own database plus a file under the state directory. If the current time is meaningfully behind it, you have a rollback signal. Log it, surface it to the administrator, and keep running. The common cause is a VM resumed from a snapshot, a dead RTC battery, or NTP correcting a drift nobody noticed.
Include a skew check in preflight, warn past a few minutes, and never let enforcement trigger on a small negative delta. A product that stops working because a hypervisor paused a VM over a maintenance window has a churn problem, not a piracy problem.
Enforcement is a ladder, not a switch
The worst design in this space is a boolean: valid licence, or the process exits. It converts every administrative delay into an outage, and administrative delays are the normal case. Build a ladder with five rungs and publish it.
Rung one, record. The licence is inside its term. Nothing visible happens. The usage ledger records.
Rung two, notify. Inside the renewal window, or past expiry but inside grace. A banner visible to administrators only, plus a structured log event their monitoring can alert on. End users cannot fix it, and telling them only generates tickets.
Rung three, restrict growth. Existing work continues, new provisioning stops: no new seats, no new projects, no new instances of the metered object. This rung creates urgency without creating an incident.
Rung four, read-only. Reads and exports keep working, writes stop. Export keeps working at every rung. Holding a customer's own data hostage over a lapsed licence is how you lose the renewal.
Rung five, stop. The service refuses to start, with an error naming the licence ID, the expiry date and the exact command to install a new one.
Attach durations, put them in the documentation and the agreement, and make grace longer than the customer's purchasing cycle. Thirty days is short for an enterprise; sixty to ninety is realistic for anything routed through a committee, and it costs you close to nothing because the renewal was going to happen anyway.
Metering without a callback
You still need a number at renewal, so build a local usage ledger and design it for one question, the billing question. The shape that works: an append-only record where each entry carries a timestamp, an event type, a stable opaque subject identifier, a count, the licence ID, the software version, a monotonic sequence number, and the SHA-256 of the previous entry. The hash chain does not make forgery impossible; it makes deletion visible, because a missing block breaks the chain and a skipped sequence number falls out of a two-line script. Keep personal data out entirely: identifiers and counts, no free text, no document names. Your invoice should never be what drags you into a data-protection review.
Export is a single command producing a file the customer can read and diff before sending. That reciprocity is the point: a security team that can read your usage export approves it in one pass, and a binary blob goes to a review board. You cannot make a ledger unforgeable on hardware you do not control, so stop where the effort curve goes vertical. Make it accurate for the customers who are not trying to cheat you, and make tampering visible enough that the rare exception is a commercial conversation with evidence attached.
Updates: the bundle is the product
With no update channel, the artifact you hand over is the entire delivery mechanism and has to carry everything the customer's change process asks for: the images or binaries, a manifest listing every component with its SHA-256 digest, a detached signature over the manifest, an SBOM in CycloneDX or SPDX, the migration set, a changelog, an upgrade matrix, and a verification command that runs with no network.
Sign the manifest rather than each file, verify the manifest first, and check every digest before anything is unpacked or executed. The customer runs that verification on their side of the boundary with a public key you distributed through a different channel than the bundle. Document the command in the release notes, because a security team that can independently verify what you shipped stops treating each release as a review event.
One trap worth naming: Sigstore keyless signing does not work here, because it needs an OIDC round trip to a certificate authority and a transparency-log lookup. Cosign with a key pair works offline and is the version to promise. And if you ship to many customers over many years, look at The Update Framework's role separation before inventing your own; it exists for exactly this case, untrusted transport and a client that cannot ask a server anything.
Ship the SBOM and the vulnerability statement with the bundle, not on request
The customer's scanner will produce findings against your image, most of them in transitive dependencies you never call. If the first time you address that is an email thread three weeks after delivery, the change window is gone. An SBOM at build time is a CI step, and a machine-readable statement of which findings are not exploitable in your configuration turns a recurring argument into a file. Both belong inside the signed manifest.
Send it over and we will tell you what we would change.
Email a decoded sample of your licence payload, the code that verifies it, and the runbook a customer follows to install an update with no network to contact@precisionfederal.com. You get back a short written note naming the three things we would change and why. One business day. No charge, no meeting, no deck.
contact@precisionfederal.comGetting the bytes across the boundary
"Air-gapped" covers a wide range. Ask which of these five the customer means before building for the strictest one.
| Channel | How it moves | Fits when | What it costs you |
|---|---|---|---|
| Pull-through registry mirror | Their Harbor, Artifactory or ECR proxies your registry | Egress is restricted and allowlisted, not absent | Immutable tags forever, and old digests must stay alive for years |
| Signed tarball, installed by hand | docker save or oras pull on a connected host, then copied in | Most on-premises installs | Every release is a customer task; the runbook is part of the product |
| Registry-to-registry copy | skopeo copy into their private registry, no daemon needed | Kubernetes customers with an internal registry | Every image reference in your manifests must be rewritable at install |
| Removable media or one-way transfer | The bundle lands via a data diode or a courier's drive | A genuine air gap | Bundle size is a hard constraint and a bad build has no do-over |
| Customer-hosted mirror | You publish, they mirror on their own release schedule | Large customers with a change advisory board | A second credential set and a mirror contract you now maintain |
Bundle size decides more than people expect. Put model weights in their own layer and stop rebuilding it for application changes: OCI layer reuse by digest is free deduplication that careless Dockerfile ordering throws away. Test the bundle at the size it will be, on the media it will cross.
Version skew, migrations and the rollback you will actually use
Connected products upgrade continuously and can assume the previous version was yesterday. Offline products get upgraded when a change window opens, which for some customers is twice a year, so the migration path has to survive a customer arriving eighteen months late.
Publish a support window and hold to it: any release upgrades directly from any release inside that window, and for anything older you publish a stepping-stone path naming the exact versions to pass through. Every migration is forward-only and idempotent, so a resumed upgrade corrupts nothing. Expand-and-contract keeps the previous binary running against the new schema for one release, which is what makes rollback possible at all.
Install into two slots so rollback is a switch of the active slot rather than a restore from backup, because the restore path is the one nobody has rehearsed. Then test the upgrade you will actually receive: oldest supported version, oldest supported operating system, production-sized data. Migration duration is the number the change board cares about, and "four minutes on my laptop" is not an answer when their table has 400 million rows.
Preflight and support bundles
You gave up telemetry, so the replacement has to be a first-class command rather than a page of documentation. Two of them cover most of it.
preflight runs before install and fails on what you already know breaks. CPU instruction sets, because quantised inference paths assuming AVX2 or AVX-512 crash badly on older hardware. GPU driver and runtime versions against what the images were built for. Kernel and glibc. The cgroup memory limit versus what the model needs resident, which is the most common cause of a container killed twenty seconds into its first request. Staging-volume free space, sized for the bundle plus the extracted copy. Clock skew, filesystem case sensitivity, inode limits, SELinux and AppArmor policy. And a positive check that egress is blocked, so you learn now rather than in production that a code path quietly assumed a network.
support-bundle collects logs, configuration, versions, the licence, preflight output and recent metrics into one archive, with redaction on by default and the redaction list documented. A dry-run flag prints the file tree it would collect without collecting it. That flag is what gets the command approved rather than banned, and a banned support command turns every ticket into a screen share.
Keys, rotation, and what happens when one leaks
Use separate keys for separate jobs. One signs licences, another signs artifacts, and a leak of one should not compromise the other. Embed at least two licence public keys in every release, with the licence naming which to use, because that is what makes rotation a scheduled task rather than a coordinated upgrade across an installed base you cannot reach. Put the next rotation date in a calendar now and rehearse it on a test deployment.
Then the honest part. There is no offline revocation. If the licence signing key leaks, every unexpired licence is forgeable and your only lever is the expiry date you already chose. That is the real argument for terms of twelve months or shorter with an automated reissue pipeline, and it is separate from the commercial argument. Short terms are only reasonable if renewal is effortless: a licence that arrives by email, installs with one command, and can be staged ahead of expiry so the customer never sits in a gap between the old one lapsing and the new one landing.
What enforcement actually buys you
Worth saying plainly, because it decides how much you should spend here. Anything running on someone else's machine can be patched by someone else. A determined engineer with a debugger will find the verification call and make it return true, and obfuscation only changes how long that takes.
Licensing in enterprise software is an accounting system. The controls make honest customers accurate, give both sides a number to renew against, and make a deliberate breach evidenced rather than a shrug. That is a sufficient goal and it is a few weeks of work. Building for the adversary costs several times as much, degrades reliability for every legitimate customer, and still fails against the one person motivated enough to try. The protections that hold are contractual: an audit clause, an annual usage attestation your ledger export satisfies in one command, and a renewal that starts from a shared number. Put the engineering into making that number trustworthy and easy to produce.
Mistakes we see repeatedly
- The private signing key in the repository, in a CI secret, or on a laptop instead of a KMS or HSM that only exposes a sign operation
- A verifier that reads the algorithm out of the token header instead of pinning it, reopening a decade-old class of signature-bypass bugs
- One public key compiled in, with no key identifier and no second slot, so rotation requires an upgrade every customer has to accept
- Hard expiry with no grace ladder, converting a renewal stuck in procurement into a production outage you own
- Binding to a MAC address or disk serial in an environment that autoscales, snapshots and fails over as a matter of routine
- An install step that pulls a base image, a wheel or a model file from the internet, found at the customer site rather than in CI
- Verification instructions that require a network round trip, which is what keyless signing needs and what an air-gapped customer cannot make
- Telemetry that fails closed, taking the product down when an endpoint it should never have depended on is unreachable
What to have in place before the first offline release
- A licence verifier with a pinned algorithm, a key identifier, and two embedded public keys
- An issuance service that signs through a KMS or HSM and records every licence it has ever issued
- A written grace ladder with durations, in the documentation and in the agreement
- A hash-chained local usage ledger the customer can read, exported by one command
- An update bundle with a signed manifest, per-file digests, an SBOM, and an offline verify command
- A preflight command that fails on the environment problems you already know about
- A support-bundle command with redaction on by default and a dry-run that prints what it would collect
- A published upgrade matrix and a tested path from the oldest version you still support
- Two-slot installation, so rollback is a slot switch rather than a restore
- A rotation calendar for both signing keys, with the next date already scheduled
A four-week path to shipping it
The proportions below are what we plan against on a first offline release. Almost none of the work is cryptography. Verifying a signature is a few lines; everything expensive sits around it.
Where the engineering hours go
Share of a first-release build, excluding the product changes the entitlement model itself forces.
Offline Licensing And Update Path
Step six is the one teams skip and the one that finds the defects. Expiring a licence on purpose, watching each rung fire on schedule, installing the renewal and confirming a clean recovery takes an afternoon. Skip it and the first run happens at a customer, at scale, with an account manager on the thread.
Bottom line
A no-egress requirement is not a reason to lose the deal, and it is not a six-month project. Sign a legible licence file with a key that lives in an HSM. Bind to an organisation and a capacity rather than to hardware. Build a grace ladder that outlasts a purchasing committee. Keep a readable usage ledger. Ship an update bundle with a signed manifest and an SBOM the customer can verify without a network. Then accept that revocation is contractual rather than technical, set your terms accordingly, and put the engineering hours into making renewal effortless instead of making enforcement clever.
Frequently asked questions
Issue a signed licence file the product verifies locally with an embedded public key. Ed25519 or ECDSA P-256, private key in a KMS or HSM, algorithm pinned in the verifier, key identifier in the header so you can rotate. The licence carries entitlements, capacity, an activation date, an expiry and a separately stated end of grace. The customer installs it as a file, and nothing has to reach you.
As a self-contained bundle with a manifest listing every component and its SHA-256, a detached signature over the manifest, an SBOM, the migration set and an upgrade matrix. The customer verifies the signature offline with a public key delivered through a separate channel, then loads the images with a registry-to-registry copy or a saved archive. No install step may reach the internet, which is the requirement that most often fails on a first attempt.
Yes, with a local append-only ledger where each record links to the hash of the previous one, exported by a single command into a file the customer can read before sending. Keep it to counts, opaque identifiers and timestamps. It is not tamper-proof on hardware you do not control, but it is accurate for customers acting in good faith and it makes gaps visible.
Offline deployments cannot check a revocation list, so every unexpired licence signed by that key stays valid until it expires. The containment is a term of twelve months or less, at least two public keys embedded in every release so rotation needs no coordinated upgrade, and separate keys for licence signing and artifact signing so one incident does not become two.
