QueryProxy

PostgreSQL access control and query approval

PostgreSQL decides who may read or change which objects. It does not decide whether this particular statement, typed by a person at 3 a.m., should run. QueryProxy adds that decision: developers submit SQL, the statement is parsed and checked before it can enter the queue, a DBA approves it, and the result is masked before it is stored. The PostgreSQL password stays in QueryProxy’s vault and is never handed to a developer.

What do GRANT and row-level security cover?

GRANT gives a role privileges on objects: tables, columns, schemas, functions. It is the right tool for scoping what a role may reach, and any production setup should use it.

Row-level security goes further. The PostgreSQL 18.6 documentation says row security policies restrict “which rows can be returned by normal queries or inserted, updated, or deleted by data modification commands”. The same page notes that superusers and roles with the BYPASSRLS attribute always bypass it, and table owners normally do too. (Read on 2026-10-13.)

What neither does is look at the statement. A role that may UPDATE a table can run an UPDATE with no WHERE clause. A role that may SELECT the users table receives every email address in it, unmasked, on a laptop. Neither involves a second person. These gaps are the argument of what database access control is: roles say who may act, and cannot review the statement. The post on stopping UPDATE and DELETE without WHERE looks at that gap in detail.

What does QueryProxy block on PostgreSQL?

Every submission is parsed before it reaches the approval queue, and a request that breaks a guard is rejected at submission with an explicit message. The SQL guards reference lists the rules per dialect. The ones that matter most on PostgreSQL:

  • UPDATE and DELETE without a WHERE, or with one that is always true (WHERE 1 = 1), are rejected. A SELECT with no LIMIT gets LIMIT 1000 injected, and a limit above 10000 is clamped.
  • COPY, including COPY ... TO PROGRAM, is blocked.
  • File functions are blocked: pg_read_file(), pg_read_binary_file(), pg_ls_dir(), lo_import() and lo_export().
  • dblink*, and the administration functions pg_terminate_backend, pg_cancel_backend, pg_reload_conf, pg_rotate_logfile, pg_promote and pg_switch_wal, are blocked.
  • Functions that run SQL handed to them as a string, such as query_to_xml, xpath_table, ts_stat and crosstab*, are blocked, because the guard cannot read the statement inside.
  • CREATE EXTENSION is judged by name. Untrusted procedural languages (plpythonu, plperlu, and anything matching the pl...u convention) are refused; pgcrypto, uuid-ossp or postgis install normally.
  • SET of a guarded setting such as session_preload_libraries, dynamic_library_path or session_replication_role is refused in every scope.
DELETE FROM customers;                      -- rejected at submission
DELETE FROM customers WHERE id = 42;        -- allowed, still needs a DBA
SELECT pg_read_file('/etc/passwd');         -- rejected at submission

The table names are examples; the verdicts follow the documented rules.

How are DO blocks handled?

A DO block, a CREATE FUNCTION or a CREATE PROCEDURE carries a body in another language. QueryProxy reads that body with PostgreSQL’s own lexical rules, applies the blocked-function and guarded-setting rules to it, and accepts only the plpgsql and sql languages.

A DO block is classified as a write and always goes through human approval, where the reviewer sees the full body. A body that uses dynamic SQL (EXECUTE) is refused outright, since the statement it runs is built at run time. Everything else a plpgsql block can do stays decided at approval, not by the parser.

How do you connect a PostgreSQL database?

A DBA registers the database under Connections with the pgsql driver. The default port is 5432 (DbDriver.php, v0.2.5). The default Docker image ships pdo_pgsql, so no image change is needed; the configuration guide covers the rest. Credentials are encrypted at rest with AES-256. A developer sees the connection only after a DBA grants it.

For read-only teams, register a separate connection with a database user that has SELECT privileges only, and point it at a replica where you can. The production hardening guide recommends this.

What happens to the result?

Rows are masked while they stream from the cursor to the result file, so an unmasked copy never exists on disk. Masking rules match a column name (such as *email*) or the content of a value, and a value that no rule matches comes back unchanged. Dynamic data masking explains the idea, and the data masking documentation lists the rule types and strategies.

Masking also covers failures. A PostgreSQL error such as Failing row contains (...) can echo the row that broke a constraint, so when the connection has any masking rule the whole row is masked in the stored error message. Every step, from submission to download, is written to an immutable audit log.

What are the limits on PostgreSQL?

  • The denylist is incomplete. It names dangerous syntax known today, and a spelling it does not name passes. The SQL guards reference says so itself: what authorizes a statement is the DBA approval, not the guard.
  • No read-only transaction or statement timeout on the database side yet. Both are listed as planned for v0.3.0. Until then, use a SELECT-only database user for read connections.
  • TLS is not configurable yet. The connection uses sslmode=prefer today, and a TLS setting is planned for v0.3.0. Check the transport yourself.
  • Masking is rule-based. A value that no rule matches is returned as it is, so QueryProxy does not claim to mask all personal data.
  • The DO body scan reads plain text only. A DO block in plpgsql is still arbitrary server-side code, and a plainly written DELETE without a WHERE in its body passes, because blocks are decided at approval.

For PostgreSQL, the guard’s verdicts are also checked against a live server (the live-pgsql test group). The reference states that MySQL, MariaDB and SQL Server have not been run against live servers.

When do you not need QueryProxy?

If one person owns the database and is the only one who connects, there is no second person to approve anything, and GRANT plus a read-only role is enough. The same holds for a development database that holds no real data. If your problem is only that applications keep passwords in config files, a secrets manager or IAM database authentication solves that without an approval queue. QueryProxy earns its place when people need ad-hoc access to data that matters, and you want a second pair of eyes and a record of what ran.

Read the approval workflow for how a request moves, or compare QueryProxy with a bastion host. MySQL and MariaDB teams should start from MySQL and MariaDB access control.

Frequently asked

How do I give developers access to a production PostgreSQL database without sharing the password?
Register the database once in QueryProxy, where the credential is stored AES-256 encrypted, and grant developers the connection instead of the password. They submit SQL, a DBA approves it, and a worker runs it with the stored credential. Developers never see the password.
Is GRANT SELECT enough to protect personal data in PostgreSQL?
No. GRANT SELECT lets the role read every column it was granted, including email addresses and phone numbers, and returns them unmasked to the client. Row-level security can hide rows, but not mask a column in the rows it allows. Masking is a separate layer, which QueryProxy applies while the result is written.
Does QueryProxy block COPY ... TO PROGRAM?
Yes. The SQL guard refuses PostgreSQL COPY, including COPY ... TO PROGRAM, at submission, so the request never reaches a DBA. The same applies to pg_read_file(), pg_read_binary_file(), pg_ls_dir(), lo_import() and lo_export().
How does QueryProxy handle DO blocks in PostgreSQL?
A DO block is classified as a write and always goes to a DBA, who sees the full body. The body is scanned for blocked functions and guarded settings, and a body that uses dynamic SQL (EXECUTE) is refused because the statement it runs cannot be read in advance.
Does QueryProxy replace PostgreSQL roles and row-level security?
No. It adds a layer in front of them. Give the QueryProxy connection a database user with only the privileges the work needs, ideally SELECT only for read connections, because the database-side read-only transaction and statement timeout are not available yet.

Arrow keys to move, Enter to open.