# Known limitations A single register of proxy-monster's known limitations, accepted caveats, and deferred gaps โ€” what the system does not guarantee, where it deliberately over-denies, and what's tracked for later. This is the one place to look; detailed design/tracking lives in the linked docs. Severity legend: ๐ŸŸข fail-safe over-deny (a legitimate query may DENY, never a wrong-ALLOW) ยท ๐ŸŸก fail-closed deny of a legitimate case (feature intentionally unavailable) ยท ๐Ÿ”ด known disclosure/leak (not fail-safe โ€” a real gap). The overriding invariant is fail-closed: a cleartext PII leak is the worst outcome, over-denying is acceptable. Everything below is consistent with that except the items explicitly marked ๐Ÿ”ด. Every engine, MySQL, and PostgreSQL mention below is about a **target database** โ€” what the proxy protects and enforces against. None of it concerns the control-plane store, which is PostgreSQL only and carries no portability caveat ([`docs/migrations.md`](./docs/migrations.md)). ## Web session lifecycle - A web session without a stored refresh token cannot be revalidated against the IdP mid-session. This occurs when `offline_access` is absent or `PM_RESULT_KEY` is unset. The timer sweep leaves that session alone; identity staleness is bounded by the configured absolute cap (`PM_WEB_SESSION_ABSOLUTE`, default 2h), not the 5-minute IdP recheck interval. - proxy-monster is a single-instance system. Timer-sweep serialization, refresh-token use, migrations, and runtime guards assume one control-plane process. Multi-replica coordination and leader election are out of scope by design; do not deploy multiple active replicas. ## Daemon session renewal and revocation - ๐ŸŸก Silent `pmon` renewal is bounded by `PM_SESSION_WINDOW` (default 2h): the daemon's renew loop re-mints ~30 min before token expiry, but with the default 12h TTL that first attempt lands long after the window closed โ€” so a default-config login still lasts one TTL, then needs `pmon login` again. Raise the window (or shorten `--ttl`) to get continuous renewal. - ๐Ÿ”ด Closing a daemon renewal window does not revoke the wire token issued under it (token TTL, 12h default, outlives the 2h window). The exposure is a revocation the IdP signals _only_ as `invalid_grant`: group-removal revocations empty the role set and every statement DENYs, and SCIM `active=false` / local deactivate revoke the tokens outright (`revokeActiveCredentialsTx`). Detail: [`docs/auth-model.md`](./docs/auth-model.md#security-invariants). ## Audit trail - ๐ŸŸก OIDC group reconciliation (`provisionFromOidc`) commits before, and separately from, the login-audit transaction: a session mint that then fails leaves the reconciled membership in place; the next login re-reconciles. OIDC-reconciled membership changes are recorded in no audit trail (only manual-admin and SCIM membership changes are); the `auth` event covers the login itself. ## Identifier handling (schema-aware enforcement) - ๐ŸŸก Identifiers containing `.` or `/` cannot be authorized normally. Both are legal inside a _quoted_ identifier but are the key/EUID delimiters (analyzer keys join on `.`, Cedar EUIDs on `/`), so a component containing one either fails identity rendering or is denied during Cedar resource binding. Production denies; an explicit `exception.unanalyzable` grant may relay a statement whose dotted identity failed before resource facts were emitted. ## Live namespace tracking Decisions resolve against each connection's live probed namespace (PostgreSQL probe-always; MySQL per-statement re-probe). The session settings the masker depends on are pinned fail-closed: - ๐ŸŸก MySQL `character_set_results` must stay UTF-8 โ€” anything else fails the session closed. Exception: the Connector/J (DBeaver) session-init `SET character_set_results = NULL` is rewritten to `utf8mb4` so those clients work. - ๐ŸŸก The injected namespace probe consumes a transaction's first-statement slot, so `SET TRANSACTION` right after `BEGIN` fails with SQLSTATE 25001. Suppressing the probe for a "bare opener" would be a wire-signature guess a stored `CALL` can mimic while moving `search_path` โ€” fail-open. Use `BEGIN ISOLATION LEVEL โ€ฆ` instead. - ๐ŸŸก `client_encoding` must remain UTF8 โ€” under another encoding the same bytes bind different objects on each side, so a non-ASCII identifier could dodge its mask. It is `GUC_REPORT`: a switch is observed before the next statement and the relay fails closed. - ๐ŸŸก `standard_conforming_strings` must remain on โ€” with it off, a backslash-escape literal can hide a statement boundary the admission lexer cannot see. `GUC_REPORT`: a session turning it off fails closed (SQLSTATE `0A000`). ## Catalog freshness Connection-scoped enforcement resolves columns and functions from the fragments captured on the connection's own held target-DB connection (design: [`docs/per-connection-catalog.md`](./docs/per-connection-catalog.md)). - Enforcement-path residuals (detail + severities in [`docs/per-connection-catalog.md`](./docs/per-connection-catalog.md)): ๐Ÿ”ด rename/redefinition launders name-keyed classifications (re-tag is the admin's job); an approval grant not bound to the resolved resource; DDL inside a DML-fired trigger unflagged; a bounded external `Aโ†’Bโ†’A` revert window; ๐ŸŸก MySQL temp-table shadowing; PostgreSQL transactional-DDL probe edges. External out-of-band DDL is corrected on the next re-check past the staleness bound, not immediately. - ๐ŸŸก A permitted UDF whose body reads a masked column leaks it: `SELECT my_udf(id) FROM t` where `my_udf` internally reads `ssn` returns ssn in the clear. The UDF requires a Cedar Function permit, but the proxy cannot see reads inside its body on the service-account connection. Passing the column as an argument is still caught (`SELECT custom_udf(ssn)` DENYs, [`docs/derived-masking.md`](./docs/derived-masking.md)). A permitted UDF on a masking datasource must not read PII. Output vouching is backlogged ([`docs/backlog.md`](./docs/backlog.md)). ## Query coverage - ๐ŸŸข sqlglot-go parser gaps route through `exception.unanalyzable` (production denies; a development exception may relay). Bare `SELECT *` over a table-function / `LATERAL` / `VALUES` source follows the same gate. - ๐ŸŸก `RENAME TABLE a TO b` is denied โ€” sqlglot-go leaves it an unmodeled `Command`, and matching the verb would also match `RENAME USER` (privilege management). Use `ALTER TABLE a RENAME TO b`. ## Authz / policy - ๐ŸŸก A stored FAILED query's diagnostic re-decides against the CURRENT catalog, not an execution-time snapshot (unlike rows, #229). Example: `users.ssn` is masked, a write fails with `DETAIL: Failing row contains (1, 010-โ€ฆ)`, then ssn is dropped โ€” a later view re-decides all-unmasked and releases the raw text. Needs DDL within the 24h retention window ([`docs/diagnostic-redaction.md`](./docs/diagnostic-redaction.md)). - ๐ŸŸก The analyzer's diagnostic leak set covers only the statement's own tables. `DELETE FROM orders โ€ฆ` whose FK `ON DELETE CASCADE` hits a constraint on `order_items` dumps THAT table's row in the `DETAIL` โ€” a column set the check never considered ([`docs/diagnostic-redaction.md`](./docs/diagnostic-redaction.md)). - ๐ŸŸก A PostgreSQL DEFERRABLE constraint fails at `COMMIT`, and the sanitize flag is per statement: the `UPDATE` that violated it was marked sanitizing, but the `COMMIT` (empty leak set) is not, so its `DETAIL: Key (ssn)=(โ€ฆ) is still referenced` relays raw. The known transactional-deferral class ([`docs/diagnostic-redaction.md`](./docs/diagnostic-redaction.md)). - ๐ŸŸก Run-as-approver checks the approver's active status at `/execute`, not the requester's: a requester deprovisioned after approval still has the task execute (they can't view the result โ€” `/result` checks `isDeactivated`). Whether the task should run at all is unresolved, flagged rather than silently decided. Detail: [`docs/task-execution.md`](./docs/task-execution.md). - ๐ŸŸก Open presets โ€” curation is load-bearing. Under `system:development` (and the permit-by-default system posture), a data-reading system object that the shipped `system:` classification _forgets to tag_ is exposed. The curated classification must be thorough + version-maintained. Detail: [`docs/access-model.md`](./docs/access-model.md). - ๐ŸŸกโ†’๐Ÿ”ด No fail-closed manifest-completeness guard: an un-manifested system table gets no `system:` tag, so a datasource-wide read grant or a `system:development` permit could read it (config-dependent). Deferred โ€” [`docs/backlog.md`](./docs/backlog.md). ## Data plane - ๐ŸŸก Binary/prepared results (every JDBC/ORM client) cannot be masked: they deny by default under a MASK verdict, unless `exception.unmaskable` grants the unmasked relay. PostgreSQL `COPY` is always denied. Detail: [`docs/access-model.md`](./docs/access-model.md). - ๐ŸŸก A shutdown drain landing between a pipelined PostgreSQL `Execute`'s relayed `CommandComplete` and its buffered `Sync` rolls the statement back โ€” a client that treated `CommandComplete` as commit believes a rolled-back write succeeded. Narrow window; MySQL's analogue is benign (skipped and retried on reconnect). Fix (bounded reads through the pending `Sync`) is a follow-up. - ๐ŸŸก A PostgreSQL multi-statement query may control the transaction only by beginning with `BEGIN` and ending with `COMMIT`; any other `BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`, or `SET TRANSACTION` in it is refused. Send those statements as their own query. - A PostgreSQL refusal on the extended protocol outside `BEGIN` does not undo the statements already executed since the last `Sync`; PostgreSQL would roll that implicit transaction back. Inside `BEGIN` a refusal fails the transaction as an error does. - ๐ŸŸก A MySQL multi-statement query with a syntax error or an empty statement (`;;`) runs nothing, where MySQL runs the statements before it. A MySQL stored procedure that returns result sets fails (1312): the proxy relays one result per statement. ## Athena - ๐ŸŸก One datasource reflects and classifies one catalog (`PM_ATHENA_DEFAULT_CATALOG`). Athena resolves `catalog.database.table` across catalogs in one query, and the analyzer accepts the qualifier, but a table in any other catalog has no columns in the snapshot, so the statement is unanalyzable and denies. Cover a second catalog with a second datasource. - ๐ŸŸก A forwarded `GetQueryResults` page reports no row or byte volume; the audit completion covers the accepted submission only. ## Wire-cert distribution (direct clients and `pmon`) A proxy advertises its wire-TLS certificate chain at `Register`; `GET /api/datasources/{id}/wire-cert` serves it for `psql`/`mysql`/DataGrip (`verify-full`), and `pmon` uses the same bytes as its upstream root pool. - ๐Ÿ”ด The advertised chain's root of trust is the shared `PM_SECRET_TOKEN`, not a proxy identity: whoever holds it can re-register any datasource's address and chain and capture its wire tokens. Fix is a datasource-bound registrar identity (per-datasource credential / proxy mTLS). Production requires the secret at startup; its admin-equivalent scope is the remaining gap ([`docs/datasource-registration.md`](./docs/datasource-registration.md)). - ๐ŸŸก Every certificate in the advertised file becomes a `pmon` trust anchor โ€” a bundle contaminated with an unrelated CA widens trust instead of failing closed (refusing at registration would mean no datasource, no catalog, total outage). Publish only the certificates the proxy actually presents; narrowing to the verified path is tracked in [`docs/backlog.md`](./docs/backlog.md). - ๐ŸŸก Certificate expiry is never checked before advertising โ€” the client's error is authoritative โ€” so the console can offer a certificate no client will accept. ## SQL editor The web SQL editor executes over the proxy-dialed `EditorExec` channel through the proxy's normal `Decide`; a persistent per-session stream holds one target-DB connection so `SET`/`USE`/temp/`BEGIN` persist across queries. - ๐ŸŸก No concurrent-editor-session cap (auth'd DoS surface): each session pins one unpooled target-DB connection until explicit close or the idle sweep (30-min cutoff on a 15-min tick โ†’ up to ~45 min), so an authenticated user opening many sessions can exhaust the target's `max_connections`. A per-principal cap is a follow-up. ## Result caps and rates See [`docs/result-caps.md`](./docs/result-caps.md). - ๐ŸŸข An editor-session result (the editor and the approval workflow) is bounded by the channel's own 5,000-row ceiling even under an uncapped verdict. Reading a larger result unbounded goes over the wire. - ๐ŸŸก Parallel connections can overshoot a rate by up to one statement cap each. A rate is summed from completion rows, which land after a statement finishes, so connections that all pass `Decide` before any of them completes see the same stale total. Charging the statement cap at `Decide` and settling it on completion would close this. --- _Add an entry here whenever a fix ships with an accepted caveat or a deferred gap, rather than burying it in a design doc. Keep detail/tracking in the linked docs; this file is the index._