Threat model
What QueryProxy protects, from whom, where the trust boundaries lie and which risks stay with you. The first published threat model for operators.
Updated:
This is the first version of QueryProxy’s threat model. It describes what the current release (v0.3.0) is built to withstand and, as plainly, what it is not. Use it to review a deployment; it is not a certification and does not replace a review of your own environment.
QueryProxy sits between people who need data and databases that hold it. The product’s job is to make sure that every statement a person runs on a production database was seen by the guards, approved by someone else, run with limited privileges, masked on the way out and written to a record that is hard to rewrite.
Assets
| Asset | Where it lives | Protection | What remains |
|---|---|---|---|
| Target database credentials | The application database, encrypted | AES-256 with APP_KEY; never shown again after saving; redacted in stored error messages, audit metadata, notifications and the UI |
Whoever holds APP_KEY and the database can decrypt them. The application log receives the raw driver error message, which can include user names, hosts and row values; protect that log like the database. php artisan config:cache writes secrets taken from *_FILE into bootstrap/cache/config.php in plain text |
APP_KEY |
Environment, APP_KEY_FILE, or a key file on the data volume |
You choose where it lives; queryproxy:doctor warns when it shares a volume with the SQLite database |
A backup of that volume holds the key next to the data |
| Result files | The result disk | Masked while written (fail closed), private disk only, downloads pass the request policy, pruned after the retention period | Masking is rule-based; data no rule matches is stored as is |
| Audit records | The application database | Hash chain, triggers against UPDATE and DELETE, optional export to a SIEM |
The database owner can rewrite history unless you keep an external copy |
| Chat secrets, masking keys, TLS certificates | The application database, encrypted | Same key as the credentials; the signing secret can be set only by a system administrator | Same as the credentials |
| Submitted SQL | The application database | Shown redacted to Auditors and in chat by default; raw SQL is not in the audit screen, the CSV or the SIEM export | Requesters, the team’s DBAs and administrators see the SQL as submitted |
Actors
| Actor | Starting point | Goal |
|---|---|---|
| Malicious developer | A QueryProxy account with the Developer role | Read data they should not, change data without approval, run something dangerous on the server |
| Compromised DBA account | A stolen or misused account with the DBA role | Approve harmful requests, weaken protections, read unmasked data |
| Attacker on the network | Network position between browser, QueryProxy, chat and target databases | Impersonate a user or a chat approver, read or alter traffic, reach internal services |
An administrator is trusted: administrators can override approvals, set signing
secrets and manage users. A person with root on the host, or with both the
database and APP_KEY, is outside the model.
Trust boundaries
Browser ──HTTPS──▶ reverse proxy ──HTTP──▶ QueryProxy (web, worker, scheduler)
Slack / Teams ◀──HTTPS webhook── QueryProxy ◀──signed callbacks── Slack / Teams
QueryProxy ──TLS──▶ target databases
QueryProxy ──▶ application database, result disk
QueryProxy ──▶ SIEM (stdout, syslog, signed webhook), Prometheus (scrape)
- Browser to QueryProxy. The container serves plain HTTP on
7432; the TLS terminator is yours.TRUSTED_PROXIESandTRUSTED_HOSTSdecide whoseX-Forwarded-*headers and which host names count. - Chat to QueryProxy. Callbacks are signed (Slack signature, Teams HMAC), expire after five minutes and carry a single-use action token bound to one request. The approver is mapped to a QueryProxy user in the integration’s team.
- QueryProxy to target databases. Network connections created from v0.3.0 require TLS. Reads run in a read-only session with timeouts. The privileges of the connection’s database user are the last line.
- QueryProxy to the outside. Webhooks go only to allowlisted hosts and are
never redirected. Audit sinks and
/metricsare off until configured.
What each actor can try
The malicious developer
- Run a write without approval. Writes enter the queue as
pendingand wait for a DBA; the requester cannot approve their own request. Reads run in a read-only session on the database. - Dodge the guards with syntax. The inspector normalises comments and judges statements by their dangerous part, and unclassifiable input is treated as a write. It is a denylist, though: a spelling it does not name yet can pass. The approval, not the denylist, authorises a statement.
- Read unmasked data. Masking is applied while the result is written, so
unmasked rows never reach the result store. A statement that filters by a
masked column can still leak the value by inference (a
WHEREclause that tests candidate values). The sensitive-filter badge warns the approver when a request filters by a column that has a masking rule; it does not block the request. It does not follow column aliases, CTEs or derived tables, and ignores regex and validator rules. - Exhaust resources. A hard
LIMITceiling, a statement timeout, a lock timeout and a per-connection slot limit apply. On MySQL a long write is not cut by the database timeout. - Reach internal hosts through connections or webhooks. Connection hosts can be
denied (
QUERYPROXY_CONNECTION_HOST_DENYLIST), SQLite files are pinned (QUERYPROXY_SQLITE_ALLOWED_DIR) and outbound webhooks are SSRF-guarded.
The compromised DBA account
- Approve their own request. Refused. Another DBA, or an administrator who overrides the rule with a written reason that lands in the audit log, has to decide.
- Forge a chat approval. The HMAC alone proves only that the caller knows the secret, so the secret can be set only by an administrator, the callback needs a live single-use token, and the approver must be a mapped user of the team.
- Weaken the protections. A DBA manages masking rules and connections and can turn a rule off or grant access. Those changes are audited. Review them in the audit log, and keep Auditors who can read it; a DBA cannot.
- Recover pseudonymised values.
hmacmasking replaces a value with a keyed token that is stable inside a team. Low-entropy identifiers such as national ids or phone numbers can be tested against the token by anyone who can get a candidate through the same rule (the masking playground, or an approved query that returns candidates). Treathmacas pseudonymisation inside the team, not anonymisation. Rotating the key makes old and new tokens stop matching. - Delete evidence. Audit records cannot be updated or deleted through the
application, and the database refuses it through triggers (on MySQL and MariaDB
the triggers cannot block
TRUNCATE). Whoever can alter the database outside the application is the next actor up.
The attacker on the network
- Read or change traffic to QueryProxy. Put TLS in front, set
SESSION_SECURE_COOKIE=true, and setTRUSTED_PROXIESto your proxy only. - Forge client addresses. Not possible as long as
TRUSTED_PROXIESnames only your proxies. A broad range such as10.0.0.0/8lets every host in it setX-Forwarded-For;queryproxy:doctorfails on*and on catch-all ranges only. - Replay a chat callback. Refused after five minutes and after the first decision.
- Sit between QueryProxy and a target database. New connections require TLS,
so a downgrade to plain text fails. Encryption alone does not prove who is on
the other end: for PostgreSQL choose
verify-caorverify-full, for MySQL and MariaDB tick “Verify the server certificate”, and for SQL Server leaveTrustServerCertificateoff. Existing connections from before v0.3.0 keep their old behaviour until you edit them. For SQL Server, refusing a server that offers no encryption is the ODBC driver’s documented behaviour, not something QueryProxy tests. - Scrape
/metrics. Off by default; needs a token or an allowed address.
What the guard does not cover
The guard is a defence-in-depth layer, not a security boundary. The full list is in SQL guards. The ones that matter most when you assess risk:
- Code that already lives on the server (functions, procedures, triggers) is not inspected when a request calls it.
- MySQL and MariaDB stored-code bodies and
SET PASSWORDlists are not inspected asSETstatements. - Writes to the catalog tables of SQLite and SQL Server, and
TRUNCATE,DROPandALTERon catalog tables, are outside the catalog rule. - SQL Server has no engine-level read-only session. Without a separate read credential, a read relies on the guard alone.
- Redaction for chat and audit views leaves a few forms visible, such as a
$12.50money literal outside SQL Server and a value typed in double quotes on PostgreSQL or SQL Server (it reads as an identifier and fails when run).
Give each connection a database user with the least privileges that fit the team, and a separate read-only credential where the database offers one.
Risks that stay with you
- Keeping
APP_KEYsafe, and apart from the backups. - TLS in front of QueryProxy and the correct
TRUSTED_PROXIES. - The privileges of the database users QueryProxy connects as.
- An external copy of the audit log (a SIEM sink) if you need to prove the log was not rewritten by someone with database access.
- Who is an administrator, and who is a DBA of which team.
- The host, the container runtime and the image you run. Release images are signed; see Upgrading and rolling back.