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:
- connectivity — can we reach the database and authenticate;
- 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.
| Engine | Guide | What the driver checks |
|---|---|---|
| PostgreSQL | postgres | rolsuper / rolbypassrls, has_table_privilege(… INSERT/UPDATE/DELETE/TRUNCATE), temp-table probe |
| MySQL / MariaDB | mysql | SHOW GRANTS, an UPDATE-against-a-missing-table privilege oracle, @@global.read_only |
| Snowflake | snowflake | SHOW GRANTS TO ROLE, a CREATE TABLE privilege oracle |
| BigQuery | bigquery | testIamPermissions 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.