Skip to main content
Early accessElevarq Analyzer is not yet generally available — this manual documents the current build.Request an evaluation →
Elevarq Analyzer docs · Data-boundary & safety model

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

PropertyValue
Base imagegcr.io/distroless/nodejs22-debian12:nonroot
Runtime usernonroot (UID 65532, GID 65532)
Shell in the runtime imageNone
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 1tini (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/imports only. 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/licenses and GET /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, or
  • WORKBENCH_MASTER_KEY_FILE — a path to a mounted secret file (the file's trailing newline is trimmed). WORKBENCH_MASTER_KEY takes 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 Secret and reference it via masterKey.existingSecret + masterKey.existingSecretKey (the chart wires WORKBENCH_MASTER_KEY from it via secretKeyRef). Prefer this over the inline masterKey.value. See Kubernetes deployment.
  • Docker Compose: set WORKBENCH_MASTER_KEY from your environment, or mount a file and set WORKBENCH_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 Required and 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: https on 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 | base64

    The final base64 line 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, mode 0644 — the file holds public keys only and is not sensitive):

    {
      "version": 1,
      "keys": {
        "producer-2026-q2": "MCowBQYDK2VwAyEA...base64-32-bytes..."
      }
    }

    version is the file-format version (current: 1); keys maps the signing_key_id the 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.json in 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) where canonicalBody is the package JSON re-marshalled with content_hash, signature, and signing_key_id zeroed (importer.CanonicaliseForHash). The base64 of the resulting 64-byte signature goes in the signature field; the matching id goes in signing_key_id.

What happens when a signed envelope arrives

  • Both fields present + signature verifies: import accepted; audit log records import_attempt.signature_verified with the signing_key_id (no bytes).
  • signing_key_id not in the registry: rejected with closed reason signing_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_verified lines 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.

Next

Run Workbench

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

Pin a digest in production — verify the image.