Skip to main content
Early accessElevarq Analyzer is not yet generally available — this manual documents the current build.Request an evaluation →
Elevarq Analyzer docs · Activate offline (air-gapped)

How-to guide

Activate offline (air-gapped)

When Workbench cannot reach the internet, activation is a three-leg exchange: export a request from the instance, upload it to the Elevarq portal and download the counter-signed activation file, then import that file. This guide covers the exchange and the periodic re-attestation that keeps it valid.

This page documents the offline-activation flow for air-gapped or outbound-restricted Workbench deployments. Online-licensed customers stop at the bootstrap guide § Step 2; this page is for operators who cannot reach Elevarq from the deployment host.

It does not cover platform-specific deployment wiring — for that see Docker Compose deployment or Kubernetes deployment — nor the support-team runbook (internal).

The umbrella specification for the offline-activation system is offline-activation-v1. The customer-facing surfaces on this page are operator-facing projections of that spec.

What persistent state matters

Two on-disk values bind your Workbench install to a signed activation file:

FileCreated whenContents
/var/lib/arq/instance_idFirst container startUUIDv4. The persistent identity of this install.
/var/lib/arq/install_secretFirst container start32 random bytes. Only the SHA-256 hash of this value leaves the host.

The directory /var/lib/arq/ must be a persistent volume — the example Compose file mounts it as the named volume arq_identity and the Helm chart provisions a volumeClaimTemplate named identity. See Configuration for the mount and the upgrade guide § 1 for the backup-first procedure.

Deleting /var/lib/arq/ mints a new instance_id on next start. Every previously-signed activation file then fails verification with LIC-OA-INSTANCE_MISMATCH. Back this volume up separately from /var/lib/workbench/ (data).

Step 1 — Export the activation request

Workbench builds a signed JSON request the operator hands to Elevarq support. The endpoint is authenticated; log in first as per the bootstrap guide § Step 1.

HTTP form:

curl -fsS "$WORKBENCH/api/license/activation-request?license_key=ELV-AB7Q-MZ2K-9PYV-43XR" \
  -b /tmp/wb.cookies \
  -o request.json

CLI form (useful when the container is reachable only on a private network and the operator runs the binary directly):

workbench license-key request-export \
  --key ELV-AB7Q-MZ2K-9PYV-43XR \
  --out request.json

Both forms emit byte-identical JSON for the same identity and key (elevarq.activation_request.v1 schema):

{
  "schema":              "elevarq.activation_request.v1",
  "license_key":         "ELV-AB7Q-MZ2K-9PYV-43XR",
  "license_id":          "<empty on first activation; populated on re-attestation>",
  "customer_id":         "<copied from the loaded license, optional>",
  "instance_id":         "<from /var/lib/arq/instance_id>",
  "install_secret_hash": "<SHA-256(install_secret), base64url-no-pad>",
  "product":             "workbench",
  "product_version":     "<binary semver>",
  "deployment_type":     "docker | compose | k8s | bare | unknown",
  "hostname":            "<informational; never trusted>",
  "created_at":          "<RFC3339>"
}

request.json contains no private secrets — install_secret_hash is a SHA-256 digest, not the raw 32 bytes. The runtime emits an audit event license.activation_request.exported on every export so the action is visible in retro-analysis.

Step 2 — Transfer the request to Elevarq support

Send request.json to Elevarq through whichever channel your support agreement names. Common options:

  • Email to support@elevarq.com, or the address named in your support agreement.
  • Sneakernet for fully air-gapped sites (USB / DVD).

The request file is safe to email or attach unencrypted — it is intended to be transported over operator-controlled channels.

Elevarq support verifies the request against the issuance ledger (license validity, instance count vs max_instances, re-attestation cadence) and counter-signs it.

Step 3 — Receive the signed activation file

Support returns activation.json — a signed file matching the elevarq.activation.v1 schema:

{
  "schema":              "elevarq.activation.v1",
  "license_id":          "<must match your license_id>",
  "customer_id":         "<copied from request, optional>",
  "instance_id":         "<must match your /var/lib/arq/instance_id>",
  "install_secret_hash": "<must match your current hash>",
  "issued_at":           "<RFC3339>",
  "expires_at":          "<RFC3339>",
  "signing_key_id":      "<embedded-keyring key id>",
  "grace":               false,
  "signature":           "<Ed25519, base64url-no-pad>"
}

Before importing, you can eyeball:

  • license_id matches the license you uploaded at bootstrap.
  • instance_id matches the value at /var/lib/arq/instance_id.
  • expires_at is a sensible future date. Short-expiry activations are intentional — see Step 5 (re-attestation).

Step 4 — Import the activation file

csrf=$(grep workbench_csrf /tmp/wb.cookies | awk '{print $7}')
curl -fsS -X POST "$WORKBENCH/api/license/activation" \
  -b /tmp/wb.cookies \
  -H 'Content-Type: application/json' \
  -H "X-Workbench-CSRF: $csrf" \
  --data-binary @activation.json

A successful import returns 200 OK. The activation file is persisted atomically at /var/lib/arq/activation.json (mode 0600) and the runtime emits license.activation.verified.

The licenses cache transitions from unactivated to active within one refresh cycle. Confirm via:

curl -fsS "$WORKBENCH/api/licenses" -b /tmp/wb.cookies
# {"plan": "...", "cache_state": "active", "activation": { ... }}

/healthz should also report cache_state: "active".

Step 5 — Re-attestation

Activations are short-expiry by design. Before expires_at, run the renewal flow to rotate install_secret and emit a fresh request:

csrf=$(grep workbench_csrf /tmp/wb.cookies | awk '{print $7}')
curl -fsS -X POST "$WORKBENCH/api/license/activation/renew" \
  -b /tmp/wb.cookies \
  -H "X-Workbench-CSRF: $csrf" \
  -o request-renewed.json

The response body is the same elevarq.activation_request.v1 shape as Step 1. Transport, support-side counter-signing, and import (Step 4) repeat exactly as for the first activation.

Behind the scenes:

  • A fresh 32-byte install_secret is generated and written atomically to /var/lib/arq/install_secret.
  • The previous bytes are saved at /var/lib/arq/install_secret.prev for one rollback window.
  • An in-flight activation file signed against the OLD hash still verifies during the rollback window, so a late-arriving Step 4 succeeds. The first successful import against the NEW hash clears .prev.

Re-attestation runs the same audit emission as initial activation. The licenses cache exposes the new install_secret_hash_prefix after rotation.

Step 6 — Decommission

To clear the activation without deleting the persistent identity:

csrf=$(grep workbench_csrf /tmp/wb.cookies | awk '{print $7}')
curl -fsS -X DELETE "$WORKBENCH/api/license/activation" \
  -b /tmp/wb.cookies \
  -H "X-Workbench-CSRF: $csrf"

The licenses cache transitions to unactivated immediately; baseline entitlements (no paid features, max_databases=0) apply on the next read of /api/licenses.

DELETE does NOT clear /var/lib/arq/instance_id or install_secret. The deployment's identity persists across decommission-and-rebind cycles — the same install can take a new activation against the same instance_id.

To destroy identity entirely, stop the container and delete the arq_identity named volume (Compose: docker volume rm arq_identity; Helm: delete the identity PVC). The next start mints a fresh identity, which then needs its own activation through Steps 1–4.

Rejection-reason table

Every error response on the activation surface returns {"error_code": "<CODE>"} with the closed code below. The runtime emits license.activation.rejected carrying the same code; no underlying error string is exposed to the operator.

CodeStageStatusOperator action
LIC-OA-FILE_TOO_LARGEimport413The activation file exceeds 16 KiB. Verify the file came from Elevarq support and was not corrupted in transit.
LIC-OA-PARSE_ERRORimport400The JSON is malformed. Re-download from support; do not edit the file by hand.
LIC-OA-INVALID_SIGNATUREimport400The signature did not verify against the embedded public-key ring. Confirm the file is from Elevarq support; the file was not modified after signing.
LIC-OA-UNKNOWN_KEYimport / startup400The signing_key_id is not in this Workbench version's embedded ring. Upgrade Workbench, or ask support to re-sign with a current key.
LIC-OA-EXPIREDimport / startup409expires_at has passed. Run Step 5 (re-attestation) to get a fresh activation.
LIC-OA-INSTANCE_MISMATCHimport / startup409The activation's instance_id doesn't match this install. Either the activation was issued for a different deployment, or /var/lib/arq/ was reset since you exported the request. Re-export and re-attest.
LIC-OA-INSTALL_SECRET_MISMATCHimport / startup409The activation's install_secret_hash matches neither the current nor the rollback (.prev) hash. Most often caused by re-attesting after the rollback window closed; re-export Step 1 with the current hash.
LIC-OA-LICENSE_MISMATCHimport / startup409The activation's license_id doesn't match the loaded license. Confirm you're uploading the activation that matches the license you've bootstrapped, not an activation from a different license.
LIC-OA-NO_LICENSErequest export409No license loaded yet. Complete the license-activation step in the bootstrap guide § Step 2 first; re-export only works on a loaded license for re-attestation.
LIC-OA-PERSIST_ERRORimport500Workbench could not write /var/lib/arq/activation.json. Check that the identity volume is mounted, writable by UID 65532, and not full.
LIC-OA-REVOKEDimport / startup / verifier409A revocation list entry covers this activation. Elevarq support has marked this (license_id, instance_id) pair as revoked — typically after confirming a volume clone or an environment decommission. Contact support to either lift the revocation or issue a fresh activation against a new identity.
LIC-OA-REVOCATION_TOO_LARGErevocation import413The revocation list exceeds 256 KiB. Verify the file came from Elevarq support; do not concatenate multiple lists by hand — Workbench merges them server-side per LIC-OA-R183.
LIC-OA-REVOCATION_PARSE_ERRORrevocation import400The revocation list JSON is malformed. Re-download from support; do not edit by hand.
LIC-OA-REVOCATION_INVALID_SIGNATURErevocation import400The revocation list signature did not verify against the embedded public-key ring. Confirm the file is from Elevarq support and was not modified after signing.
LIC-OA-REVOCATION_UNKNOWN_KEYrevocation import400The revocation list's signing_key_id is not in this Workbench version's embedded ring. Upgrade Workbench, or ask support to re-sign with a current key.

Instance limits

The licensing layer carries a max_instances field per customer. At the runtime layer this is informational: the binary reads max_instances and surfaces it via GET /api/licenses, but the runtime does NOT refuse to start when an instance count is exceeded.

Enforcement happens at activation-signing time on Elevarq's side. If your customer agreement covers three instances and you already have three active activations, the signer rejects a fourth activation request and support contacts you to negotiate.

Operators can see the current value via:

curl -fsS "$WORKBENCH/api/licenses" -b /tmp/wb.cookies \
  | jq '.entitlements.instances'

Next

If activation didn't transition cache_state to active, the rejection code names the cause — see the table above. For non-activation failures (image won't start, /healthz 503), see Troubleshooting.

Run Workbench

docker pull ghcr.io/elevarq/workbench:v0.1.0

Pin a digest in production — verify the image.