connect a database

Read-only credentials, per engine

Why Evalyst refuses credentials that can write, what each driver checks, and how bastion host keys are handled.

Evalyst refuses to activate a source whose credentials can write. That is not distrust of the SQL guard — the guard is good and it stays — it is about what a bypass costs. On a read-only account, the worst case of a guard bug is a failed query. On a writable account, it is your data.

So verifying a source runs two checks:

  1. connectivity — can we reach the database and authenticate;
  2. read-only — can these credentials modify anything.

The second is belt and braces on purpose, because grant text lies. Each driver reads the engine’s own authority on effective privilege and runs a probe that writes nothing but makes the server reveal what it would allow. If either says “can write”, the source stays inactive and you get the snippet for creating a proper reader.

EngineGuideWhat the driver checks
PostgreSQLpostgresrolsuper / rolbypassrls, has_table_privilege(… INSERT/UPDATE/DELETE/TRUNCATE), temp-table probe
MySQL / MariaDBmysqlSHOW GRANTS, an UPDATE-against-a-missing-table privilege oracle, @@global.read_only
SnowflakesnowflakeSHOW GRANTS TO ROLE, a CREATE TABLE privilege oracle
BigQuerybigquerytestIamPermissions for tables.updateData / delete / create

When you cannot provision a reader yet

A hard refusal has its own failure mode: an organisation whose DBA has not yet created the read-only user cannot onboard at all, and the pressure that creates is to switch the check off globally — which is how a safety property dies quietly.

So the gate is acknowledgeable instead. An admin can accept write-capable credentials for one source, recorded with who accepted it, when, and why. There is deliberately no global switch and no environment variable.

Crucially, it changes what we allow and never what we claim. A source in that state keeps reporting read-only unverified — on its card, in the API, and in what the agent is told about it. Supply real reader credentials later and re-verify, and the badge flips with no code change.

SSH bastions and host keys

If your database is only reachable through a bastion, Evalyst forwards over SSH rather than asking you to open the port.

We generate the keypair. You get the public half and a ready-to-install authorized_keys line restricted to forwarding to that one database and nothing else — no shell, no other destination. There is no endpoint that returns the private half. Pasting your own key is still supported when your platform team issues it, but a pasted key has usually already been through a clipboard, a browser and a form, and usually opens other things too.

Host keys are pinned per source, not read from a shared known_hosts. This matters more than it sounds: a bastion behind round-robin DNS — a connection-proxy pool can front several nodes with different host keys — can never validate against a store that holds one key per host, and verification then succeeds or fails depending on which node DNS hands you. So the trusted key is configured on the source and accepts a list. Leave it unset and the first connection accepts and records what it sees; a refusal reports the key it refused, so an unpinned pool node can be pinned without guesswork.

Cost caps travel with the connection

We are spending your compute money, so the limits are part of attaching a source rather than something to remember later: a row cap and a statement timeout on every engine, a bytes-billed ceiling on BigQuery that fails an over-budget query before it scans, and a pinned warehouse on Snowflake.