Skip to content
Wharfbase

Security model

Wharfbase holds the keys to your databases and talks to Docker, so it is built to give away as little as possible if something goes wrong.

Wharfbase never touches the Docker socket. Socket-guard holds it and forwards an allowlist of read calls and a few narrow actions:

  • joining networks, for Wharfbase itself only;
  • reading files from containers;
  • optionally, helper containers from official database images, without host access;
  • optionally, relays: with network attach set to Relays, Wharfbase joins no database network, and a relay container of its own image forwards one port per database. See Relays.

Even a compromised Wharfbase can’t start a privileged container or touch another container. On Unraid, which runs single containers, this layer is missing, and the template says so.

In a cluster, Wharfbase runs without Docker, as uid 10001 with a read-only root filesystem and no capabilities. Its roles only read; the one exception is get on Secrets, by name, for “Use the password from the pod”, audited and optional. See Kubernetes.

  • Wharfbase:
    • non-root (uid 10001), with a read-only root filesystem;
    • every capability dropped, no-new-privileges;
    • one writable volume, /data.
  • The host scan, when you turn it on:
    • mounts / read-only;
    • adds one capability, to read root-only files.
  • Socket-guard:
    • a distroless image with a read-only filesystem and no capabilities;
    • on an internal network shared with Wharfbase alone.
  • Images:
    • multi-arch, on Docker Hub, signed with cosign (verify with cosign verify --key https://fleeq.net/wharfbase/cosign.pub docker.io/wharfbase/wharfbase:<version>);
    • shipped with an SBOM, and scanned in the release pipeline.
  • Database passwords and other stored secrets (the SSO client secret, the audit stream’s secret) are encrypted with AES-256-GCM.
    • Each value is bound to its row, so copying a value to another row fails.
    • The master key is 32 random bytes in secrets/master.key (mode 0400, outside the data volume), or WB_MASTER_KEY_FILE, or WB_MASTER_KEY.
    • wb keys rotate re-encrypts everything with a new key in one transaction.
  • On a PostgreSQL store (WB_STORE_URL) the secrets reach PostgreSQL already encrypted: a dump of it without the master key holds no readable password. Give the URL, with its password, as a secret file (WB_STORE_URL_FILE); Wharfbase never logs it.
  • A wrong or damaged master key stops Wharfbase from starting. It never creates a new key over an existing data folder, and it touches nothing. Back up the key separately from the data.
  • Passwords of Wharfbase accounts are hashed with argon2id.
  • Sessions and API tokens are random 256-bit values; only their SHA-256 is stored.
  • Secret values in containers’ environments (for example POSTGRES_PASSWORD) are only read when you ask for a credential hint, and that is audited. WB_SECRET_HINTS=off turns hints off.
  • Roles: viewer, editor, admin, owner. Viewers only read.
  • Per-database permissions (Pro): none, read, write or admin, for each person or role.
  • Production databases (by label or tag) open read-only. Writing needs an unlock, which is audited and time-limited, or an approval (Team).
  • API tokens act as you, capped by their scope.
  • Two-factor sign-in (TOTP) with recovery codes. Admins can require it of every admin.
  • SSO (Team): any OpenID Connect provider, with PKCE.
  • The audit log is hash-chained, so changes to past entries show up (wb audit verify). It records sign-ins, connections, scripts (their first 200 characters), exports, unlocks, permission changes and settings.

A read-only connection is enforced by the database where the engine allows it:

  • a READ ONLY transaction on PostgreSQL and CockroachDB;
  • a read-only session on MySQL and MariaDB;
  • a read-only file on SQLite;
  • readonly requests on ClickHouse;
  • READ transactions on Neo4j.

On top of that, Wharfbase checks every statement, reading it the way the server will, including comments that a server runs or nests, and other ways to switch the mode off. On PostgreSQL and MySQL it also checks after each statement that the session is still read-only, and stops the script if not.

On SQL Server and Oracle, scripts run in a transaction that is rolled back, and statements that write or end the transaction are refused. A function that already exists and writes in its own transaction still looks like a read there. For a hard guarantee, connect with a user that only has read grants: each engine’s page shows one.

  • Cookies: HttpOnly and SameSite=Strict, plus Secure behind HTTPS.
  • Requests: a CSRF token on every change.
  • Headers:
    • a strict Content-Security-Policy, no framing, nosniff, no-referrer;
    • Cache-Control: no-store on API responses;
    • HSTS when the public URL is HTTPS.
  • Sign-in:
    • a rate limit on attempts;
    • a 15-minute lockout after 10 failures;
    • the same answer, and the same time, for an unknown account as for a wrong password.
  • The review: an OWASP ASVS Level 2 review, with its findings and fixes.
  • Fuzz tests: they run in CI and nightly, a million iterations per property:
    • the filter compilers of every engine;
    • the SQL splitter and the read-only checks;
    • the MongoDB shell parser;
    • the REST console parser;
    • the socket-guard.
  • Dependency audits: pnpm audit and govulncheck in CI, and Dependabot.
  • Reporting a vulnerability: see SECURITY.md. Reports are kept private for 90 days.