Air-gapped deployment
Cameras that will never reach the internet. A large share of the industrial market, and a first-class path rather than a workaround.
The shape of it#
── your side (online) ──────────┐ ┌── customer site (air-gapped) ──
│ │
read fingerprint ──────────────┼────────►│ camera, offline = 1
xcaicxctl mint --device … │ USB / │ reads unit.token from disk
unit.token ────────────────────┼─ image ►│ verifies with compiled-in pubkey
│ │ never opens a socket
reconcile usage-spool.jsonl ◄───┼─────────┤ usage accrues locally
│ │No activation call, no heartbeat, no revocation fetch. The camera verifies its licence with the public key compiled into the SDK and runs.
1. Get the fingerprint#
The token must be bound to the fingerprint the camera will actually compute. Read it on the unit itself, during factory test:
# On the target, with your chosen source wired up
export XCAICX_DEVICE_ID="$(cat /sys/fsl_otp/HW_OCOTP_CFG0)"
echo "$XCAICX_DEVICE_ID"Do not
If you do not set XCAICX_DEVICE_ID, the SDK falls back through devicetree serial, DMI UUID, /etc/machine-id, and finally the MAC address. /etc/machine-id is regenerated by some image build processes, and a MAC is trivially cloned. For air-gapped units — which cannot self-correct by re-activating — bind to fused silicon.
2. Mint the token offline#
xcaicxctl mint \
--sku edge-industrial \
--customer CUST-PLANT7 \
--device 3f2a9c4e8b1d5a6f0c2e7b9d4a1f8e3c \
--days 365 \
--grace-days 30 \
--key server/keys/root_ed25519.key \
-o unit.tokenmint signs locally with the key file and never contacts a server. Useful flags:
| Flag | Effect |
|---|---|
--device | The fingerprint to bind to. Required for air-gapped. |
--days | Term. Omit for the SKU default; 0 where the SKU is perpetual. |
--grace-days | Days past expiry during which everything still runs |
--api-quota | Hard on-device inference cap. 0 = unmetered. |
--max-streams | Override the SKU's stream cap downwards |
--modules | Restrict the entitlement set below the SKU's |
--require-activation | Do not use for air-gapped — forces the online exchange |
Verify before shipping it:
xcaicxctl inspect unit.token --pubkey server/keys/root_ed25519.pubCheck dev matches the unit, act is optional, and exp/grace_days are what the contract says.
3. Configure the camera#
cfg.license_path = "/etc/xcaicx/unit.token";
cfg.state_dir = "/var/lib/xcaicx";
cfg.cloud_endpoint = NULL;
cfg.offline = 1; /* never touch the network */eng = xcaicx.Engine(license_path="/etc/xcaicx/unit.token",
state_dir="/var/lib/xcaicx",
offline=True)With offline = 1 the SDK never opens a socket. xcaicx_engine_heartbeat() becomes a no-op returning XCAICX_OK, so you can leave the call in your scheduler.
Build with -DXCAICX_WITH_TLS=OFF to drop libssl from the image entirely.
4. Reconcile usage#
Usage still accrues to state_dir/usage-spool.jsonl, sealed by period. Collect it with whatever mechanism the site already permits — a maintenance USB stick, an existing one-way data diode, a quarterly service visit.
cat /var/lib/xcaicx/usage-spool.jsonl{"device_id":"3f2a9c…","license_id":"LIC-…","period_start":1786500000,
"period_end":1786586400,"inferences":2592000,"frames":2592000,
"stream_seconds":86400,"events":214,
"modules":{"detect":2592000,"defect":0,"ppe":0,"anpr":0}}Counts only — no frames, no crops, no imagery. When a security officer at an industrial site asks what the file contains, that is the complete answer, and being able to show them the file is usually the end of the conversation.
Feed collected spools back to the authority when you are next online:
xcaicxctl usage LIC-D3ACF708DB6048AB # what the server has recordedRecords dedupe on (device_id, period_start, period_end), so re-submitting a spool you already delivered is safe.
5. Renewal#
An air-gapped camera cannot renew itself. Plan for it:
- Mint the replacement token before the current one expires.
grace_daysis the buffer for a service visit that slips. Set it to match the site's realistic maintenance interval, not to a default.- Deliver the new token to
license_pathand restart the engine. There is no online step.
Careful
Set the term against the site's maintenance schedule, not the calendar. A 365-day term on a plant that is only accessible during an annual shutdown gives you a zero-day margin — the licence and the only opportunity to replace it expire in the same week.
6. What you give up#
Being explicit, because these are the questions that come back later:
| Capability | Online | Air-gapped |
|---|---|---|
| Seat accounting enforced server-side | Yes | No — you mint against a count you track yourself |
| Revocation | Propagates on heartbeat | Not possible until the next visit |
| Usage reaching the invoice automatically | Yes | Manual collection |
Fleet visibility (xcaicxctl fleet) | Live | Only what you have collected |
| Weak-binding detection | Recorded at activation | Not recorded |
Air-gapped licensing therefore trusts your provisioning process rather than the server. If a customer's site is air-gapped and you need hard seat enforcement, the answer is a network path to the licensing authority for provisioning only — not for inference — which most security teams will accept when they understand no imagery crosses it.
Next#
- Licensing model — token claims and the fingerprint sources
- xcaicxctl CLI — full
mintandinspectreference - Security & threat model — what the offline path does and does not buy