Give the store's reader only the columns a module declares readable, never a secret, so a query cannot read the controller's signing key (hq issue 337)
mesh_store_reader held pg_read_all_data, so every caller of mesh-store.query and postgres_query could read every column the store holds: the signing key nodes trust, sealed secrets, password hashes. The reader now holds no role; each minute, on every database, its memberships are revoked and its grants brought to what the consumer's contribution lists under readable, minus any column or table whose name says it holds a secret. Before a statement runs, the reader's own session is asked which secret columns it can read and the statement is refused if any; a statement naming a known secret table is refused before it is sent.
This commit is contained in:
@@ -47,9 +47,26 @@ for, so one removed by hand is installed again.
|
||||
- **`postgres_retire_database` renames, it does not drop**: the database becomes
|
||||
`<name>_deleted_<yyyymmdd>` and its owner is locked. Removing the data is a person's act, by hand.
|
||||
- **A caller's statement never runs as the superuser.** `postgres_query` and the seat's `query` verb
|
||||
run as `mesh_store_reader` — `pg_read_all_data` and nothing else, every session read-only by the
|
||||
server's own setting, a 60 s statement timeout — with the statement sent as given (issue 193).
|
||||
Without the reader's password (`own-secrets.reader`) the call is refused.
|
||||
run as `mesh_store_reader` — every session read-only by the server's own setting, a 60 s statement
|
||||
timeout — with the statement sent as given (issue 193). Without the reader's password
|
||||
(`own-secrets.reader`) the call is refused.
|
||||
- **The reader reads only what a module declares readable, and never a secret** (issue 337). It holds
|
||||
no role — until issue 337 it held `pg_read_all_data`, which let any caller of the seat's `query` read
|
||||
the controller's signing key — and is granted per column, in each consumer's database, what that
|
||||
consumer's contribution lists:
|
||||
|
||||
`"contributes": {"postgres-database": {"name": "letta", "readable": ["agents", "steps(id, created_at)"]}}`
|
||||
|
||||
An entry is `[schema.]table` (every column it has, at each pass) or `[schema.]table(column, …)`. A
|
||||
database no contribution names — the controller's own among them — gives the reader nothing. A column
|
||||
or table whose name says it holds a secret (private, secret, sealed, token, password, credential, salt,
|
||||
a key) is never granted, declared or not, and the refusal is reported. Every minute, on every
|
||||
database, the reader's role memberships are revoked and its grants brought to exactly that, so an
|
||||
existing install is narrowed without anybody acting and a narrowed declaration takes effect at the
|
||||
next pass. Before a caller's statement runs, the reader's own session is asked which secret-named
|
||||
columns it can read (a grant to `PUBLIC` or by hand would show there); if any, the statement is
|
||||
refused and the columns named. A statement that names a known secret table of the controller's, or a
|
||||
secret-sounding name, is refused before it is sent — a second layer, not the guard.
|
||||
|
||||
## Tools
|
||||
|
||||
@@ -59,6 +76,7 @@ for, so one removed by hand is installed again.
|
||||
| `postgres_query` `{database, sql}` | one read-only statement, as the reader; rows keyed by column, `NULL` as null |
|
||||
| `postgres_retire_database` `{database, confirm}` | renames a database aside and locks its owner; `confirm` repeats the name |
|
||||
| `mesh-store.databases`, `mesh-store.query` | the store seat's verbs: the same listing and read-only query |
|
||||
| `postgres_reader_grants` | what the reader may read in each database as the last pass left it: readable columns, refused secrets, declared but missing, revoked, and any secret it can still read; names only |
|
||||
| `provisioner_retirement` | what is held, what waits for a person, what was rejected, every retired consumer and set-aside database with when, why and size |
|
||||
| `provisioner_retire_approve`, `provisioner_retire_reject` `{consumers, why, by}` | a person's answer to a set waiting; through the controller's `retire approve|reject` |
|
||||
| `provisioner_delete` `{consumer, confirm, why, by}` | drops one retired consumer's database and role, or one set-aside database; through the controller's `cleanup delete` |
|
||||
@@ -76,4 +94,6 @@ has one, byte for byte the same as keycloak's.
|
||||
also runs `live_test.go` against a real, throwaway one (see the file for a `pgvector/pgvector`
|
||||
container): the extension installed and a second pass a no-op, the reader unable to write, a
|
||||
retired login locked out with its data kept and listed retired, enabled again as it was, and a deletion
|
||||
dropping only its own database and role.
|
||||
dropping only its own database and role. `live_reader_test.go` and `readable_live_test.go` need no extension: an install's
|
||||
reader as issue 337 found it reading no secret after its first query, and the reconcile granting exactly
|
||||
what is declared, refusing secrets, taking back what is no longer declared, and finding a grant to `PUBLIC`.
|
||||
|
||||
Reference in New Issue
Block a user