Skip to main content
Version 1.0Elevarq Analyzer 1.0 is generally available — this manual documents the current release.Contact us to get started →
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 covers how a BYOL deployment obtains and installs its license without a live connection from the deployment host — for air-gapped or outbound-restricted installs. Online-licensed customers stop at the bootstrap guide § Step 2.

Two models, by product:

  • Unified image (the shipping product) — instance-bound license. The license is bound to the deployment's identity at mint time, and the license file itself is the credential — there is no separate activation file. This is the flow in the next section.
  • Workbench standalone — legacy activation exchange. A generic license plus a separately counter-signed activation.json, governed by offline-activation-v1. That request → sign → install exchange is documented from § Workbench standalone onward; it has not yet migrated to the instance-bound model.

It does not cover platform-specific deployment wiring — see Docker Compose deployment or Kubernetes deployment — nor the internal support runbook. AWS Marketplace is unaffected by either model: its entitlement is resolved centrally.

Unified image — instance-bound license

On the unified image a BYOL license is bound to one deployment instance at mint time: the deployment's identity (instance_id + install_secret_hash) is baked into the signed license. The license file itself is the credential — there is no separate activation file and no instance-count ledger. A license copied to any other instance is inert (it falls back to the community baseline), so binding is enforced by construction. This applies to every unified BYOL deployment, not only air-gapped ones. (AWS Marketplace is resolved centrally and is unaffected.)

One license = one instance. A deployment fleet of N instances obtains N licenses, one per identity request.

Environment

SettingDefaultPurpose
ELEVARQ_IDENTITY_DIR/var/lib/elevarq/identityPersistent deployment identity (instance_id + install_secret). Must be a persistent volume — deleting it mints a new identity and orphans the license.
ELEVARQ_LICENSE_PATH/var/lib/elevarq/identity/arq-license.jsonWhere you install the signed, instance-bound license.
ELEVARQ_EMIT_IDENTITY_REQUESTunsetSet to 1 to print the identity request JSON and exit. No license key required — the license does not exist yet; this request is the input to minting it.

Step 1 — Emit the identity request

Run the image once in request-emit mode. This mints the deployment identity (on first run) and prints the identity request — the input Elevarq mints the license against. It needs no license key and no network:

docker run --rm \
  -v elevarq_identity:/var/lib/elevarq/identity \
  -e ELEVARQ_EMIT_IDENTITY_REQUEST=1 \
  ghcr.io/elevarq/elevarq:<tag> > identity-request.json

identity-request.json carries instance_id, install_secret_hash (a SHA-256 digest — the raw 32-byte install_secret never leaves the host), product, and version.

Step 2 — Get your instance-bound license from elevarq.com

Bring the identity request to the Elevarq portal:

  • Log in at elevarq.com (create an account if you don't have one).
  • Upload identity-request.json.
  • Select your tier (Starter / Professional / Business) and any extras (e.g. additional databases).
  • Pay. Elevarq mints a license bound to that exact instance and returns arq-license.json.

For fully air-gapped sites, carry identity-request.json out and the returned arq-license.json back over whatever operator-controlled channel your agreement allows (email / USB / DVD). The request carries no secret material.

Step 3 — Install the license and start

Place the returned license at ELEVARQ_LICENSE_PATH inside the identity volume and start the container normally:

docker run -d \
  -v elevarq_identity:/var/lib/elevarq/identity \
  # ...your normal run flags; the license now sits in the identity volume...
  ghcr.io/elevarq/elevarq:<tag>

At import and on a re-check cadence the supervisor verifies the license: signature → expiry → instance_id match → install_secret_hash match (against the local identity). On success the deployment is entitled to its tier. On any failure it fails closed to the community baseline (max_databases=0, paid surfaces off) and logs the reason:

ReasonMeaning
LIC-IB-INVALID_SIGNATURESignature did not verify against the pinned release key.
LIC-IB-INSTANCE_MISMATCHThe license is bound to a different instance_id — the core anti-reuse case (a copied license, or the identity volume was reset).
LIC-IB-INSTALL_SECRET_MISMATCHinstall_secret_hash matches neither the current nor the rollover (.prev) value.
LIC-IB-EXPIREDPast expires_at; obtain a renewed license (Step 4).
LIC-IB-UNBOUNDThe license carries no instance binding — a pre-instance-bound generic license is not honored.

Step 4 — Renewal (term boundary)

Instance-bound licenses carry a bounded term (decoupled from billing). Before expires_at, obtain a renewed license for the same identity: emit a fresh identity request (Step 1 — same instance_id, current install_secret_hash) and repeat Steps 2–3. While a subscription is active Elevarq re-mints on a cadence; if it is cancelled, re-minting stops and entitlement ends at the next term boundary. There is no separate re-attestation exchange and no runtime revocation list — a copied license is already inert (LIC-IB-INSTANCE_MISMATCH).

Identity persistence

ELEVARQ_IDENTITY_DIR must be a persistent volume. Deleting or recreating the container without persisting it mints a new instance_id, which orphans the installed license (fail-closed to baseline) and requires a new license against the new identity. Back this volume up separately from your data volume.


Workbench standalone (legacy activation)

The sections below document the Workbench standalone activation model (offline-activation-v1): a generic license plus a separately counter-signed activation.json, exchanged over the Workbench HTTP endpoints, with an issuer-side max_instances ledger and a revocation list. The unified image does not use this flow — it uses the instance-bound model above. Standalone has not yet migrated.

What persistent state matters

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

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

The directory /var/lib/elevarq/identity/ (ELEVARQ_IDENTITY_DIR) must be a persistent volume — the example Compose file mounts it as the named volume elevarq_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/elevarq/identity/ 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/elevarq/identity/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/elevarq/identity/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/elevarq/identity/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/elevarq/identity/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/elevarq/identity/install_secret.
  • The previous bytes are saved at /var/lib/elevarq/identity/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/elevarq/identity/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 elevarq_identity named volume (Compose: docker volume rm elevarq_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/elevarq/identity/ 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/elevarq/identity/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 Elevarq

docker pull ghcr.io/elevarq/elevarq:v<version>-small-cpu

Pin a digest in production — verify the image.