Skip to content
Wharfbase

What Wharfbase reads

Wharfbase needs to look at your server to find its databases. This page lists everything it reads, what it changes, and what it never does. The same limits are enforced in code, not only described here.

The Wharfbase container never mounts the Docker socket. A separate container, socket-guard (about 3 MB, source), mounts it and forwards only the calls below. It sits on an internal network that only Wharfbase shares, so nothing else can reach it. Every other call is refused with a 403 and logged. Paths that try tricks (%-escapes, .., //) are refused before any rule is checked, and request bodies are rebuilt from only the fields that are allowed.

Call Why
GET /_ping, /version, /info, /system/df Docker version, host facts, volume sizes
GET /containers/json, /containers/{id}/json the containers, and each one’s image, ports, networks, mounts, labels, command and environment
GET /images/{id}/json image metadata (exposed ports, default env, version labels)
GET /networks, /networks/{id}, /volumes, /volumes/{id} who can reach what; where data lives
GET /events rescan when containers start, stop or change
GET /containers/{id}/archive reading files inside containers: SQLite database headers and, when you open one, a snapshot (turn off with GUARD_ALLOW_ARCHIVE=false)
POST /networks/{id}/connect, /disconnect joining a database’s network so it’s reachable without publishing ports. Only for the Wharfbase container itself, never host or none, never a network labeled wharfbase.attach=false. Turn off with --no-network-attach.
POST /containers/create, start, wait, attach, resize, DELETE off by default (GUARD_ALLOW_EPHEMERAL=false); for wb shell and backups (Pro), limited to short-lived, labeled containers from database images, without privileges, host mounts, published ports or host networking. Create may set a TTY and stdin (an interactive client), nothing more. A backup reads the dump from the container’s output; nothing is written into a volume

Nothing that runs commands in your containers (exec), changes, stops, deletes or pulls them is on the list.

A container’s environment comes with its inspect data, and Wharfbase uses it to find ports, users, database names and which services point at which database. Values of variables whose names look secret (*PASSWORD*, *SECRET*, *TOKEN*, *KEY*, *AUTH*, *PASS*, *PWD*), passwords in connection strings, and secret command-line flags are replaced with *** before anything is stored, logged or shown. Wharfbase only records that a password variable exists and its name.

When you connect to a database (Phase 6), an admin can choose “use the detected password”. Wharfbase then reads that one variable’s current value, uses it once, and writes an audit entry. WB_SECRET_HINTS=off removes that option.

During a scan Wharfbase checks whether each database answers and which version it is. That’s a protocol handshake, like a client saying hello, without logging in. The one exception, with active checks on (the default): it asks a MongoDB for its database list without credentials, which only succeeds when authentication is off. That is the “MongoDB without authentication” finding. Turn it off under Settings → Discovery.

Wharfbase logs into a database only when a user connects to it, with credentials that user enters or picks. After that it reads and writes only what the user does in the data browser (opening a table, editing a cell, running a query). It changes nothing on its own.

  • Read-only connections (a viewer, a production database, a SQLite snapshot, or the connection’s own setting) refuse writes in Wharfbase, and where the engine allows it the database refuses them too: PostgreSQL runs editor scripts inside a READ ONLY transaction, MySQL sessions are READ ONLY, and SQLite files open read-only.
  • The SQL editor never touches Wharfbase’s own files. It refuses SQLite ATTACH, DETACH and VACUUM INTO, and MySQL LOAD DATA LOCAL. MySQL’s local-file support is also switched off at the protocol level.
  • Any transaction a script leaves open is rolled back when the script ends.

wb shell (only with GUARD_ALLOW_EPHEMERAL=true, only for editors on a connection that is writable for them) starts the database’s own client (psql, mysql, mongosh, redis-cli) in a short-lived container: the database’s image (or the official image of the same version) on the database’s network, with the connection’s password in that container’s environment, removed when you exit the client. It’s audited as shell.opened (which connection and image, never the password).

Backups (Pro, admins, only with GUARD_ALLOW_EPHEMERAL=true) run the database’s own dump tool (pg_dump, mysqldump/mariadb-dump, mongodump, redis-cli --rdb) in a short-lived container, the same way, with the job’s saved credentials in that container’s environment. The dump streams back into /data/backups (on the main server, also for databases on other servers); the container is removed when it ends. SQLite is copied with SQLite’s own backup API, without a container.

Live consumers (Pro) read the database’s list of client connections: pg_stat_activity, the MySQL process list, MongoDB’s currentOp, Redis CLIENT LIST, CockroachDB sessions or SQL Server’s sys.dm_exec_sessions. That’s each client’s address, user, database and application name, never what it runs. It happens when someone presses Check now, and every 5 minutes for activated databases whose connection remembers its password.

Approved writes (Team): when an admin approves an editor’s request, Wharfbase runs exactly the requested SQL or row changes once, with the request’s saved connection.

Masking (Team) replaces masked values on the server, before anything reaches the browser, an export file or the CLI.

Wharfbase in a cluster has no Docker socket. Its service account may get, list and watch:

  • pods, Services, StatefulSets, Deployments, PersistentVolumeClaims, NetworkPolicies and namespaces;
  • Secrets, with get only (no list or watch), unless you turn that off.

It reads a Secret only when someone connects with “Use the password from the pod”, and only the one key the database’s pod references. Each read is audited.

Environment values set from Secrets or ConfigMaps aren’t read during scans; their names are. The same redaction applies to values written in the pod spec.

Nothing is created, changed or deleted in the cluster, and nothing runs in your pods (no exec). See Kubernetes.

An agent on another server reads that server’s Docker the same way, through its own socket-guard, and only connects out: a WebSocket to the main Wharfbase (WB_HUB_URL), authenticated with an Ed25519 key it made at enrollment, the main server’s key pinned. Through it the main server receives the agent’s discovery results (redacted as above) and sends what users do: credentials for a session (kept in the agent’s memory only, gone when the link drops), browse calls, queries and dumps. Use an https:// hub URL so this is encrypted.

Off by default. With the host-scan option:

  • / is mounted read-only at /host.
  • Processes are read from the host’s process table (pid: host): names, command lines, cgroups and listening TCP sockets, to find databases installed with apt or systemd. Processes inside containers are skipped because the Docker source already covers them.
  • Config files of those databases are read, e.g. postgresql.conf, my.cnf, mongod.conf, redis.conf.
  • SQLite files are searched under /srv, /opt, /home and /var/lib, 6 levels deep (WB_SQLITE_SCAN_PATHS, WB_SQLITE_SCAN_MAX_DEPTH). Only the first 100 bytes of a matching file are read. Container storage (/var/lib/docker, containerd, containers, kubelet, snap) is never walked.
  • The server process runs as uid 10001 with one capability, CAP_DAC_READ_SEARCH, so it can read root-only files such as data directories and pg_hba.conf. It can’t write anywhere on the host.

Everything is in its own volume (/data): a SQLite database with users, sessions, settings, the audit log, and what discovery found (redacted as above). With WB_STORE_URL the same tables are in your PostgreSQL instead (High availability). Passwords you save for connections are encrypted with the master key (AES-256-GCM). The key lives in secrets/master.key, outside the volume.

From the data browser it also keeps:

  • Query history: the statements each user ran, with timing, row count and error code, but not their results. That’s the last 500 per user, for as long as the plan keeps history (7 days on Community).
  • Saved queries, if users save them.
  • Audit entries for every change: which object and row, but not the values unless WB_AUDIT_ROW_VALUES=true. For SQL, the first 200 characters of a script are kept (WB_AUDIT_SQL_TEXT).
  • Export files in /data/exports. Each is deleted after its single download, or after an hour.
  • SQLite snapshots in /data/tmp/sqlite, while the session that copied them is open.

With a paid plan, also:

  • Backups in /data/backups/<database>/, as many as each job keeps, with their SHA-256.
  • Saved views, permissions, alert rules, schedules and notification channels (channel settings, which may hold webhook URLs or bot tokens, are encrypted with the master key).
  • API tokens as SHA-256 hashes only; the token itself is shown once.
  • Write requests with their SQL or row changes, until deleted with their connection or user.
  • Masking rules, and a random key for the hash masks (encrypted).
  • The SSO client secret and the audit stream’s signing key, encrypted. wb keys rotate re-encrypts them with the rest.
  • Live consumers: per database, the containers or addresses seen connected in the last day, with connection counts and user, database and application names.
  • wb login stores a CLI session token (not your password) in ~/.config/wharfbase/config.json, readable only by you (0600). wb logout ends the session on the server and removes the token.
  • Inside the container, wb talks to the server through a unix socket only the server’s user can open (/data/run/admin.sock), acting as the owner. Its audit entries say cli-local.
  • wb doctor --bundle writes a zip on your side; nothing is uploaded. It holds the diagnostics, the WB_*/GUARD_* environment with secrets replaced by ***, the server’s last 1,000 log lines (already redacted, with passwords in connection strings masked again) and version numbers. No database contents, passwords, keys or tokens. Read it before you send it to anyone.

Nothing about your server, by default. The details:

  • Setup, sign-in and the UI stay on your server.

  • Update check (on by default; Settings → General, or WB_UPDATE_CHECK=false): once a day, a GET to the license server’s /v1/releases/latest, which answers with the latest version number and a link to its notes. The request carries nothing about your server, not even its version: Wharfbase compares the versions itself. The license server sees your IP address, as with any request; its own logs don’t record it. Builds from source (version 0.0.0) don’t check.

  • Telemetry is off unless you turn it on, at setup or under Settings → General, where Show what’s sent displays the next report exactly. Once a day it sends to the license server’s /v1/telemetry:

    • a random ID, made when you turn telemetry on and forgotten when you turn it off. It isn’t the instance ID, so reports can’t be matched to a license;
    • the Wharfbase version, the plan (and whether it’s a trial), and whether this is a hub with agents elsewhere;
    • counts: servers, users, databases found, databases connected;
    • databases found per engine, as numbers (postgres: 3);
    • days since setup, hours from setup to the first connection, and whether a server uses every database its plan allows.

    No names, hosts, addresses, container or database names, or anything from your databases. The license server refuses a report with any other field, keeps only the latest report per ID, doesn’t store the IP address, and deletes an ID’s report 180 days after the last one.

  • Diagnostics (Settings → Diagnostics, wb doctor): a HEAD request to the license server, to compare its clock with yours. Nothing is sent with it.

  • License server (WB_LICENSE_SERVER, by default https://license.fleeq.net), only when you enter an online license key or start a trial:

    • activation: the license key, the instance ID (random, made at setup), the instance’s name and the Wharfbase version;
    • the daily check: the license and activation IDs, the instance ID, the version and three counts: servers, activated databases and users. No names, hosts, addresses or anything from your databases;
    • a trial: the email you type and the instance ID;
    • removing a license: the license ID and the instance ID.

    Offline licenses never contact it, and neither does Community. The key itself is checked on your server with Wharfbase’s built-in public key; when the license server can’t be reached, a paid plan keeps working for 14 days.

Things you configure on a paid plan send data where you point them:

  • Alerts: the alert (event, database name, engine, server, a link) to your email server, webhook, Slack, Discord or Telegram.
  • The audit stream (Team): every new audit entry to your webhook or syslog server.
  • Single sign-on (Team): the server fetches your identity provider’s public configuration and keys and exchanges the sign-in code for an ID token; your browser goes to the provider to sign in. Wharfbase keeps the account’s email, name and subject from it.
  • Agents connect to the main server you enrolled them with, and nowhere else.
  • AI-assisted SQL (Pro, off until an admin sets it up under Settings → AI): when someone uses Ask, Explain or Fix in the SQL editor, the server sends your AI provider (Anthropic, or the OpenAI-compatible endpoint you set, which can be a model on your own machines) the question or the SQL, the engine and its version, and the names and types of the tables and columns in that schema (up to 60 tables). With Fix, also the failed statement and its error message. Never a row, a default value, an enum’s values or a comment, and never a column masked from the person asking. Settings → AI shows the exact text for an example. Your API key is stored encrypted and sent only to the provider.
  • wharfbase: runs as uid 10001, with a read-only root filesystem, all capabilities dropped and no-new-privileges. With the host-scan override or a single-container template it starts as root only to take over its data directory, then drops to 10001 (entrypoint).
  • socket-guard: a static Go binary on a distroless image, read-only, all capabilities dropped.