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 byoffline-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
| Setting | Default | Purpose |
|---|---|---|
ELEVARQ_IDENTITY_DIR | /var/lib/elevarq/identity | Persistent 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.json | Where you install the signed, instance-bound license. |
ELEVARQ_EMIT_IDENTITY_REQUEST | unset | Set 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:
| Reason | Meaning |
|---|---|
LIC-IB-INVALID_SIGNATURE | Signature did not verify against the pinned release key. |
LIC-IB-INSTANCE_MISMATCH | The 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_MISMATCH | install_secret_hash matches neither the current nor the rollover (.prev) value. |
LIC-IB-EXPIRED | Past expires_at; obtain a renewed license (Step 4). |
LIC-IB-UNBOUND | The 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:
| File | Created when | Contents |
|---|---|---|
/var/lib/elevarq/identity/instance_id | First container start | UUIDv4. The persistent identity of this install. |
/var/lib/elevarq/identity/install_secret | First container start | 32 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_idmatches the license you uploaded at bootstrap.instance_idmatches the value at/var/lib/elevarq/identity/instance_id.expires_atis 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_secretis generated and written atomically to/var/lib/elevarq/identity/install_secret. - The previous bytes are saved at
/var/lib/elevarq/identity/install_secret.prevfor 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.
| Code | Stage | Status | Operator action |
|---|---|---|---|
LIC-OA-FILE_TOO_LARGE | import | 413 | The activation file exceeds 16 KiB. Verify the file came from Elevarq support and was not corrupted in transit. |
LIC-OA-PARSE_ERROR | import | 400 | The JSON is malformed. Re-download from support; do not edit the file by hand. |
LIC-OA-INVALID_SIGNATURE | import | 400 | The 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_KEY | import / startup | 400 | The 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-EXPIRED | import / startup | 409 | expires_at has passed. Run Step 5 (re-attestation) to get a fresh activation. |
LIC-OA-INSTANCE_MISMATCH | import / startup | 409 | The 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_MISMATCH | import / startup | 409 | The 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_MISMATCH | import / startup | 409 | The 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_LICENSE | request export | 409 | No 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_ERROR | import | 500 | Workbench 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-REVOKED | import / startup / verifier | 409 | A 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_LARGE | revocation import | 413 | The 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_ERROR | revocation import | 400 | The revocation list JSON is malformed. Re-download from support; do not edit by hand. |
LIC-OA-REVOCATION_INVALID_SIGNATURE | revocation import | 400 | The 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_KEY | revocation import | 400 | The 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.