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:
| File | Created when | Contents |
|---|---|---|
/var/lib/arq/instance_id | First container start | UUIDv4. The persistent identity of this install. |
/var/lib/arq/install_secret | First container start | 32 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_idmatches the license you uploaded at bootstrap.instance_idmatches the value at/var/lib/arq/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/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_secretis generated and written atomically to/var/lib/arq/install_secret. - The previous bytes are saved at
/var/lib/arq/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/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.
| 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/arq/ 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/arq/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.