Security

What is on by default, what is off, and the three switches that matter before it faces anybody.

DataGripe holds credentials for other people's databases. That is the threat model, and most of what follows comes from taking it seriously rather than from a checklist.

This page is what an operator needs. The full design is in the authentication and hardening spec, and the production checklist is in docs/operations.md.

Before it faces anyone

Three settings, in order of how often getting them wrong causes a real problem.

WEB_ORIGIN — the exact origin browsers use. Every HTTP route and the WebSocket upgrade validate Origin against it. This is a security control, not only a convenience: it is what stops another origin driving the socket.

NODE_ENV=production — adds Secure to the session cookie. Correct behind TLS and wrong in front of plain HTTP, where the browser will not send the cookie back at all and sign-in silently fails.

ALLOW_SIGNUP=false — the default. The first account may always sign up; this governs every one after it. Leaving signup open on a deployment reachable from anywhere means anyone can create an account on it.

Accounts and sign-in

Cookie sessions, stored in the database. Sessions survive a restart and a restore; rotating SESSION_SECRET signs everyone out and nothing else.

A FIDO2 security key is an alternative to the password for both signup and sign-in, and sign-in is usernameless — insert the key and touch it. There is no email to type, which means there is no email to leak by asking whether one exists. Nothing needs switching on; WEBAUTHN_RP_ID only matters when the app answers on several subdomains and one key should work across all of them.

Google sign-in is the third way in, off until a deployment configures an OAuth client. Identities are matched on Google's sub rather than the email address, an unverified address is refused, and GOOGLE_ALLOWED_DOMAINS restricts it to your Workspace domains — set it if ALLOW_SIGNUP is true, because empty means any Google account on the internet. The only claim stored is the address.

Each of the three is independently switchable, and none is a second factor on top of another: a key and a Google identity are alternatives to a password, not additions to one. Turning off the last way in is refused at startup. All of it is on accounts and sign-in.

State-changing HTTP routes require a CSRF token matching the session's.

Authorization is per action

Socket authentication is not object authorization. The upgrade binds the socket to a user, a session, a workspace and the capabilities their role holds — and the dispatcher checks the capability an action needs on every single message, not once at the door.

Roles are a project's own: a name and a set of capabilities, edited as a matrix in project settings (roles). Owner, editor and viewer exist in every project with the capabilities those three ranks always had, so nothing about an upgraded deployment changes.

viewer editor owner
Browse schema, read history, watch executions yes yes yes
Run queries, edit data and structure, manage datasources – yes yes
Push, sync, repo commands, MCP, members, restart – – yes

Reading is not a capability: being a member is being able to read. A project where somebody must not see the data wants a second datasource, not a checkbox.

Somebody must always be able to manage members — the modern form of "owners cannot remove the last owner" — and taking a capability away reaches open sessions immediately rather than on their next reconnect.

Datasource credentials

Encrypted at rest with CONNECTION_ENCRYPTION_KEY (AES-GCM, versioned keys) and never returned to the browser — not masked, not write-only-in-the-form, absent. The server holds them.

Consequences worth planning for:

  • Losing the key does not sign anyone out. It orphans every stored password, permanently. Back it up separately from the database.
  • Rotating it is additive: add the new version to the keyring, restart, re-save connections opportunistically. Old versions keep decrypting, so there is no flag day.
  • Predefined connections (connections.json) resolve their secrets from the environment at boot and are held only in memory. They never touch the application database, which is the right shape when the secret belongs to your secret manager rather than to DataGripe.

Reaching other machines

Before opening any target connection, the hostname is resolved and every returned address is classified. Blocked by default: loopback, RFC1918, link-local — including 169.254.169.254, the cloud metadata endpoint — unique local, multicast, unspecified and reserved ranges.

TARGET_HOST_ALLOWLIST overrides per hostname, exact or *.suffix.

SSRF_DISABLED=true turns the policy off entirely. On a hosted or multi-tenant deployment this is the difference between a database client and an open proxy into your network, because the server will then connect to any host the user names, including loopback and metadata endpoints.

One accepted limitation, stated plainly. The driver resolves hostnames itself at connect time, so a DNS answer can change between the check and the connection — classic rebinding. Closing it fully needs a connect-time address hook the driver does not expose. A deployment that needs that guarantee must enforce egress at the network layer.

The host filesystem, and git

All off or restricted by default, and each is a separate decision.

Default
HOST_FS_DISABLED false Set it to true for anything hosted or multi-tenant. It turns off every host-filesystem feature — domain export and import, datasource paths, git datasources — because there the person pressing the button does not own the disk.
HOST_FS_ROOTS — Optional allowlist of absolute directories, re-resolved with realpath on every access, so a symlink swapped in later is caught.
GIT_ENABLED false Off means absent, not disabled-with-a-tooltip. Git runs with argv and never a shell, with GIT_TERMINAL_PROMPT=0 so a missing credential errors instead of hanging.
REPO_COMMANDS_ENABLED false See below.

Repo commands deserve their own paragraph. They let a repository's .datagripe/run.yaml declare commands DataGripe may run. It has a separate switch from the rest of git on purpose: every other git feature reads and writes files, and this one executes a program somebody else wrote, arriving over the network on git pull.

Even switched on, nothing runs until a person approves the list, and any change to that list needs a fresh approval. The approval is the security boundary. There is no sandbox, and the documentation says so rather than implying one.

MCP

MCP_ENABLED defaults to true and is the deployment's kill switch — off here means the route and the panel are both absent. It is on by default because the opt-in that matters is per project and defaults to off: an owner has to turn it on, and it is read-only until they say otherwise. A read-only project cannot change a row no matter what it is asked to run.

An MCP call is an execution like any other. It appears in the query history with the token's name beside it.

MCP_MAX_ROWS defaults to 200, much lower than the grid's, because the consumer is a context window.

Limits

Server-enforced, so a client cannot ask past them: query timeout, max rows, max bytes, concurrent queries per user, access-report cell count, domain-export row count. All on limits.

Rate limits are in-memory token buckets — 5 logins per minute per email, 30 per minute per IP, 30 executions per minute per user, and others. Exhaustion returns RATE_LIMITED, not a slow response.

What reaches the internet

Three things, and only three. Datasource connections you configured. The MCP endpoint, when a project's owner turns it on. And two about the app itself: the update check, which asks the release feed when a project is opened and at most once every ten minutes per server — no interval, no background thread — and the avatar, which is a hash of your address sent to Gravatar the first time the account menu is opened. Both are described on updates, and the first is removed outright by UPDATE_CHECK_DISABLED.

Restarting from inside the app

In Kubernetes, where a Deployment is guaranteed to start it again, a workspace owner can restart the server from the account menu — with imagePullPolicy: Always it can pull a new image, but does not rerun migration init containers or Helm hooks. The app itself applies pending migrations before serving requests, in both database modes. It is off by default in every other shape and refused by the server there, not just hidden, because a process nothing will restart must not be able to stop itself. RESTART_TO_UPDATE decides it either way, and every press is audited.

Audit log

Structured JSON on stdout, tagged "msg":"audit": signup, login success and failure, logout, security-key registration and removal, datasource create/update/delete, execution start and cancel, workspace membership changes, domain operations, access role changes, app.restart, and ssrf.blocked.

Never passwords, never secrets, never result values.

Pipe it to your log stack and alert on two things: bursts of auth.login.failure, and any ssrf.blocked at all — the second means somebody pointed the server at an address the policy refused.

Reporting something

Open an issue. For anything you would rather not file in public, say so in the issue without the details and a private channel will be arranged.

MCP functionality

The MCP Server tab has separate, default-off switches for domain management, datasource sync and Git. Enabling the server or changing its query mode does not enable these switches. Domain mutations, sync writes, commits and pushes also require read/write mode and an editor-level token account, with the corresponding domain.manage, sync.run, git.commit or git.push capability. Custom role restrictions are checked on each authenticated call. Existing host filesystem restrictions and the deployment Git switch still apply. See MCP for tool behavior and commit scope.

This page as Markdown, for an agent or a diff.