Explanation
Data-boundary & safety model
Workbench is the commercial control surface you install in your own environment, so its safety story matters before you point it at anything. The design goal is a narrow, firm boundary: minimal attack surface, nothing read from your data that doesn't need to be, no calls home, and every secret encrypted at rest.
This page states what Elevarq Workbench does and does not do regarding security and what assumptions it makes about the host running it.
Deployment model
Elevarq Workbench is single-tenant, single-host, customer-controlled. The product is designed to be installed inside the customer's own infrastructure and operated by the customer. There is no Elevarq- hosted SaaS, no multi-tenant MSP path, and no shared-cluster deployment story.
Each customer installation:
- Holds one organization's data.
- Is operated by that organization's admin(s).
- Activates one license at a time.
- Runs in one container on one host (or a single VM, single Kubernetes pod, single bare-metal install).
If you intend to operate Workbench for multiple end customers, each customer needs their own Workbench install and their own license.
Container posture
| Property | Value |
|---|---|
| Base image | gcr.io/distroless/nodejs22-debian12:nonroot |
| Runtime user | nonroot (UID 65532, GID 65532) |
| Shell in the runtime image | None |
| Root filesystem (with example Compose) | read_only: true |
| Writable paths | /var/lib/workbench/ and /tmp/ only |
| Linux capabilities (with example Compose) | All dropped (cap_drop: [ALL]) |
no-new-privileges (with example Compose) | true |
| PID 1 | tini (signal-forwarding init, statically linked, SHA-pinned from upstream) |
The image does not include curl, wget, apt, bash, or any
other shell. The Docker HEALTHCHECK invokes
workbench -healthcheck, which reuses the binary already in the
image to call /healthz over loopback. There is no shell to exploit.
Signing posture
Every release-tagged image is signed by the Workbench release workflow using Cosign keyless OIDC.
- No long-lived signing keys live in this repository, in any release workflow, or in the container image. The signing identity is the GitHub Actions OIDC token for the workflow itself, recorded in the public Rekor transparency log.
- Operators verify signatures using public inputs only (cosign's built-in trust roots + the workflow's certificate identity). See the verification guide.
- A failed signature verification means the image is not the one Elevarq released. Do not run such an image.
Authentication
- Operators authenticate via email + password against
Workbench's session surface. Passwords are hashed with
argon2id; sessions are server-side, with sliding plus hard expiry. - The Analyzer authenticates via a bearer token against
POST /api/v1/importsonly. Analyzer tokens never grant access to operator surfaces — they are a separate authentication channel. - Mutating routes (admin bootstrap, license activation, database registration, analyzer-token creation, finding status changes) require a double-submit CSRF token on top of the session cookie. GET routes never require CSRF.
- License read (
/api/licensesandGET /api/license) is public before bootstrap (the payload is empty until a license is activated, and the first-run wizard reads it before the operator has a session) and requires a valid operator session after bootstrap. Post-bootstrap the payload carries the active plan, status, expiry, configured SSO providers, and ticket- integration allowed types — commercial / security posture metadata that is not appropriate to expose unauthenticated.
Secrets handling
- Database passwords are encrypted at rest with AES-256-GCM using a key derived from the workspace seed (HKDF). Passwords are never returned in responses, never logged, never written to audit events.
- The activated license artefact is stored at the path
WORKBENCH_LICENSE_ARTEFACT(default/var/lib/workbench/license.json). Verification happens against an embedded public key ring; the artefact itself is not a secret in the cryptographic sense, but Workbench does not echo it back in any response. - Analyzer tokens are stored only as bcrypt-style hashes. The raw token is returned exactly once at creation and is unrecoverable thereafter.
- Workspace seed is generated on first run and stored inside
the SQLite database. It is the root of trust for the
per-install signing key (sessions) and the per-install
encryption keys (database + integration credentials). Since #415
the seed is not stored in the clear: it is wrapped with
AES-256-GCM under a key-encryption-key (KEK) that is HKDF-derived
from the operator-supplied master key (see below). Backing up
the
/var/lib/workbench/volume is therefore necessary but not sufficient — you must independently preserve the master key, or the backed-up seed cannot be unwrapped.
Master key (WORKBENCH_MASTER_KEY)
The master key is the root of at-rest encryption. It HKDF-derives the KEK that unwraps the workspace seed, which in turn derives the AES-256-GCM keys protecting every credential Workbench stores at rest (integration tokens and the like). Workbench reads it from the environment only — never from the YAML/CLI config surface, so it cannot be committed or logged:
WORKBENCH_MASTER_KEY— the secret value inline, orWORKBENCH_MASTER_KEY_FILE— a path to a mounted secret file (the file's trailing newline is trimmed).WORKBENCH_MASTER_KEYtakes precedence.
Workbench fails closed if neither is set or the file is empty: it refuses to start rather than run with an unprotected seed.
⚠️ The master key must outlive the container, the volume, and the person who set it up. An intact data volume with the wrong (or missing) master key is unrecoverable — every credential Workbench stores at rest becomes permanently undecryptable. The key is the single thing that turns a routine patch/redeploy into data loss if it is not preserved.
Generate a long, high-entropy value (any length — HKDF stretches it):
openssl rand -base64 48
It is a passphrase, not a raw 32-byte key. WORKBENCH_MASTER_KEY
accepts a secret of any length; it is HKDF-stretched into the 32-byte KEK
internally. Do not trim, pad, or reshape a value to make it exactly 32
characters/bytes — that changes the secret, and against an already-initialised
store it is a different key that fails closed. Once a store is bootstrapped,
the exact byte sequence you first used is the only value that unwraps it.
Example strings that read like ...-master-key-32-bytes... are descriptive,
not a length requirement — use a value verbatim or not at all.
Store + wire it in your platform's secret manager — never in git, never baked into an image:
- Kubernetes / Helm: create a
Secretand reference it viamasterKey.existingSecret+masterKey.existingSecretKey(the chart wiresWORKBENCH_MASTER_KEYfrom it viasecretKeyRef). Prefer this over the inlinemasterKey.value. See Kubernetes deployment. - Docker Compose: set
WORKBENCH_MASTER_KEYfrom your environment, or mount a file and setWORKBENCH_MASTER_KEY_FILE. See Docker Compose deployment.
Back it up OUTSIDE the data volume. The volume and the key must live in different places so a single backup (or a single loss) can never contain — or strand — both. Record who can retrieve the key and where, so a redeploy months later still has it.
Recover: a deployment started with the same master key against the same volumes decrypts all stored credentials transparently. A wrong or missing key fails closed at startup — restore the correct key (and the volumes); do not re-bootstrap, which would discard the data.
Rotation (re-wrapping the seed under a new key) is not yet supported. Treat the initial key as long-lived; protect it accordingly.
Transport security — HTTPS is required
Workbench's session and CSRF cookies are Secure, so a browser will
silently discard them over plain HTTP and login cannot work. To make
that requirement explicit rather than a confusing silent failure,
Workbench enforces HTTPS (default on; WORKBENCH_REQUIRE_HTTPS=true):
- A non-loopback request that is not HTTPS is refused with
426 Upgrade Requiredand a message explaining the fix — the page is never served with cookies the browser would drop. - A response served over HTTPS carries
Strict-Transport-Security: max-age=63072000; includeSubDomains(HSTS). - Loopback (
localhost,127.0.0.1,[::1]) is always exempt, so local development and the in-container health check keep working.
Workbench itself listens on plain HTTP and expects TLS to be terminated in front of it (reverse proxy, ingress, or load balancer). Configure that terminator to:
- Terminate TLS and forward requests to Workbench.
- Set the header
X-Forwarded-Proto: httpson forwarded requests. - Set Workbench's
WORKBENCH_TRUST_FORWARDED_FOR=true(the trusted-proxy switch) so the forwarded scheme is honoured. Without it the forwarded header is ignored — a deliberate anti-spoofing default for a directly-exposed listener — and Workbench will (correctly) refuse the request as insecure.
Only disable enforcement (WORKBENCH_REQUIRE_HTTPS=false) for a
deliberately isolated, non-browser, or legacy transport where you accept
that browser login will not work.
Data flow
By default, Workbench makes no outbound network calls. The exceptions:
- License activation (
POST /api/license) — operator-initiated. Workbench verifies the uploaded artefact locally; no network call is required to activate. - License refresh — periodic, configurable cadence
(
WORKBENCH_REFRESHER_INTERVAL, default 5 minutes). The refresh path re-reads the local artefact and re-verifies signatures. It does NOT phone home to Elevarq. - Analyzer imports are inbound (Analyzer → Workbench). Workbench never initiates a call to the Analyzer.
If your environment must enforce egress restrictions, allow
outbound HTTPS to ghcr.io only for image pulls.
Audit log
Every operator-mutating action emits a structured audit event on
the same log stream. The event set is closed (defined in
auth-and-session-v1, license-activation-v1,
database-registration-v1, etc.). Audit events include the
operator id, the database id (where applicable), and a closed
event: name — they do not include passwords, license bytes, or
raw analyzer tokens.
Threat-model boundary
This documentation makes no claims about:
- Host-OS hardening.
- Docker daemon configuration.
- The operator's network perimeter.
- The correctness of the operator's TLS termination itself (cipher suites, certificate issuance/rotation). Workbench requires HTTPS and enforces it at the edge (see Transport security above), but the TLS terminator is the operator's to configure and secure.
- The PostgreSQL targets Workbench connects to — securing those databases is your responsibility.
Those are the operator's responsibility. Workbench's posture is narrow but firm: a minimal-attack-surface container, no long-lived keys, no outbound calls by default, secrets-at-rest encryption, auditable mutations, and signature-verifiable artefacts.
Analyzer-import signed envelopes
The analyzer import endpoint (POST /api/v1/imports) accepts an
OPTIONAL signature + signing_key_id pair on each push
envelope per the analyzer-workbench-import-v1 specification
(AWI-R043). When the operator configures the trusted-keys
registry, Workbench verifies the signature on every import; the
signed-envelope path is the non-repudiation evidence chain for
SOC2 / ISO27001 reviewers asking "WHO produced this analyzer
data".
Configure the registry
-
Generate an Ed25519 keypair on the producer side. Tooling choice is the analyzer operator's; common options:
# Using openssl 3.x: openssl genpkey -algorithm Ed25519 -out producer-2026-q2.priv.pem openssl pkey -in producer-2026-q2.priv.pem -pubout -outform DER \ | tail -c 32 | base64The final
base64line is the 32-byte raw public key the Workbench-side registry needs. The private key NEVER leaves the producer. -
Write the registry file at a path Workbench reads (e.g.
/etc/workbench/trusted-keys.json, mode0644— the file holds public keys only and is not sensitive):{ "version": 1, "keys": { "producer-2026-q2": "MCowBQYDK2VwAyEA...base64-32-bytes..." } }versionis the file-format version (current:1);keysmaps thesigning_key_idthe producer puts on the envelope to the base64-encoded 32-byte Ed25519 public key. -
Point Workbench at it by setting
WORKBENCH_IMPORT_TRUSTED_KEYS=/etc/workbench/trusted-keys.jsonin the container environment. Workbench loads + validates the file at startup; a malformed file is fatal — the binary refuses to boot rather than silently ignore a bad security-critical config. -
Producer side: the analyzer computes
ed25519.Sign(privKey, canonicalBody)wherecanonicalBodyis the package JSON re-marshalled withcontent_hash,signature, andsigning_key_idzeroed (importer.CanonicaliseForHash). The base64 of the resulting 64-byte signature goes in thesignaturefield; the matching id goes insigning_key_id.
What happens when a signed envelope arrives
- Both fields present + signature verifies: import accepted;
audit log records
import_attempt.signature_verifiedwith thesigning_key_id(no bytes). signing_key_idnot in the registry: rejected with closed reasonsigning_key_unknown.- Signature bytes malformed (wrong length, bad base64) OR
one field present without the other: rejected with closed
reason
signature_malformed. - Signature decodes correctly but doesn't verify (tampered
body): rejected with closed reason
signature_invalid. - Registry not configured (env var unset): ANY signature
presence rejects with closed reason
signature_verification_not_yet_supported— the fail-closed posture is preserved so an operator who hasn't opted into verification can't accidentally accept an unverified signed package.
Key rotation
Rotation today is edit + restart:
- Append the new key id + pubkey to the registry JSON.
- Restart Workbench (the registry loads on startup).
- Cut producers over to the new key id; verify
import_attempt.signature_verifiedlines in the audit log carry the new id. - Remove the old key id from the registry + restart again.
Any subsequent envelope signed with the rotated-out key
rejects with
signing_key_unknown.
Online rotation via SIGHUP or an admin-API trigger is a follow-up. Until it ships, plan rotations during a maintenance window.
What this does NOT cover
- HSM / KMS-backed verification: v1 reads raw Ed25519 pubkeys from a local file. KMS-backed pubkey resolution is an enterprise follow-up.
- Signing-key generation tooling: lives on the producer side; Workbench is verification-only.
Reporting a security issue
If you believe you have found a security issue in Workbench:
- Do not post reproduction steps or proof-of-concept details anywhere public.
- Report it privately by email to support@elevarq.com. Include the product version, a description of the issue, and reproduction steps; we will acknowledge the report and keep you informed through remediation.
- Allow a reasonable disclosure window before any publication.