Security model¶
The threat is an untrusted agent: it may be prompt-injected, may hallucinate queries, or may try to misuse a broad capability. The gateway assumes that and places every guarantee in code and in the database — never in the agent.
Defense in depth¶
Several independent layers each have to fail before anything unsafe happens.
- No raw-SQL tool. There is no tool that executes caller-supplied SQL. The only surface is a fixed set of read-shaped tools.
- Fixed tool surface. Exactly six tools are registered. Table and column names are string literals in code, never taken from caller input.
- Argument validation. Each tool validates its arguments against explicit
allow-lists (e.g.
pclass ∈ {1,2,3},group_by ∈ {pclass, sex, embarked}) before any SQL is built. Values are always bound as parameters. - Read-only SQL guard.
security/readonly_sql.pyis a deny-by-default filter: it accepts only a singleSELECT, over allow-listed tables, with no comments, no statement chaining, and no dangerous functions. It is a heuristic first filter, not a full SQL parser. - Read-only database session. Every connection sets
default_transaction_read_only = onand a statement timeout, so a write that somehow reached the driver is still refused. SELECT-only role. The server connects asgateway_reader, which holdsSELECT-only grants onpassengersand no write privileges. This is the authoritative control: even if the layers above were bypassed, the database refuses the write.
The database role is the layer of record. The guard and session are defense in depth in front of it, not a substitute for it.
Secrets¶
- No secrets are baked into the container image;
.envand.venvare excluded from the build context, and all configuration is read from the environment at run time. DATABASE_URLis required at run time only — it is never needed to build the image and is never written to logs.
Audit logging¶
Every tool call emits one structured, greppable log line: tool name, sanitized
arguments, outcome (success/error), and a result count or error type. Raw SQL,
result rows, DATABASE_URL, and any credential-shaped argument are never
logged; a sensitive-key denylist drops the latter as defense in depth.
What this does not defend against¶
- Exfiltration of data the read-only role is allowed to read — column- and row-level access control is a deployment concern (views, RLS, or a restricted schema), out of scope for this demo.
- Denial of service via expensive queries beyond the statement timeout and row cap.