Database Proxy
agentsh provides database-aware enforcement for Postgres-family traffic. Declare protected upstreams with db_services, route governed processes through the per-session proxy, and apply connection and statement policy before SQL reaches the database.
Current Scope#
The shipped database runtime is Postgres-family only. PostgreSQL and Aurora Postgres use the primary supported path; Redshift and CockroachDB use the same protocol and classifier path with beta coverage. MySQL, MongoDB, Snowflake, BigQuery, Databricks, ClickHouse, MSSQL, Cassandra, Redis, and Oracle remain roadmap items.
| Area | Behavior |
|---|---|
| Protocol | PostgreSQL wire protocol v3, including Simple Query, Extended Query, SQL prepared statements, COPY handling, CancelRequest mapping, and transaction state tracking. |
| Dialects | postgres, aurora_postgres, beta redshift, beta cockroachdb. |
| Platforms | Linux runtime only today. Use native Linux, WSL2, or a Linux VM for enforcement. |
| TLS modes | terminate_reissue, terminate_plaintext_upstream, and passthrough. Statement rules require terminate mode. |
| Decisions | Connection and statement allow, deny, approve, audit; statement-level redirect for safe read-only relation replacement. |
The DB proxy is unavoidable for processes inside the agentsh-governed process tree, for declared db_services, assuming the agentsh supervisor and proxy are not compromised. Processes outside the session, undeclared databases, and compromised supervisor/proxy paths are out of model.
Architecture#
For each protected database service, agentsh starts a per-session Unix-socket listener and generates policy that redirects the agent's TCP connect to that listener. The proxy authenticates the connecting process with Linux peer credentials, evaluates startup and statement policy, forwards approved traffic upstream, and emits database audit events.
agent process
-> TCP connect to pg.internal:5432
-> connect_redirect rewrites to session DB proxy Unix socket
-> DB proxy authenticates peer SessionID with SO_PEERCRED
-> startup packet and connection rules
-> SQL classification and statement rules
-> optional safe SQL rewrite for redirect decisions
-> upstream PostgreSQL
The proxy owns PostgreSQL BackendKeyData remapping for cancellation. Clients receive synthetic cancel keys; CancelRequest side-channel connections are correlated back through the proxy and evaluated with match_kind: cancel rules.
Point Agents At It#
Do not give the agent a proxy socket path or a separate proxy host. Keep the agent's normal database configuration pointed at the declared upstream host:port, then run the agent inside an agentsh-governed session. When policies.db.unavoidability is observe or enforce, agentsh starts the per-session DB proxy and rewrites matching outbound connects to that proxy before traffic reaches the database.
db_services:
appdb:
family: postgres
dialect: postgres
upstream: pg.internal:5432
tls_mode: terminate_reissue
policies:
db:
unavoidability: enforce
The agent or application can keep using a normal Postgres connection string or libpq environment variables:
export DATABASE_URL='postgres://app:${DB_PASSWORD}@pg.internal:5432/app?sslmode=require'
# or:
export PGHOST=pg.internal
export PGPORT=5432
export PGDATABASE=app
export PGUSER=app
agentsh wrap --policy ./policy.yaml -- claude code "analyze the customer data query"
The upstream address in DATABASE_URL, PGHOST, or the application's DB config must match the declared db_services.*.upstream. Processes outside the agentsh session are not redirected, and traffic to undeclared database destinations is governed only by the rest of your network policy.
agentsh does not expose a standalone TCP listener for agents to connect to. The agent-facing address remains the original Postgres TCP host and port; agentsh uses connect_redirect to move that connection onto a per-session Unix socket internally. The socket path is generated per session and authenticated with Linux peer credentials, so operators should not bake it into application configuration.
Configuration#
Database policy lives in the normal policy YAML. A complete Postgres read-only policy with safe user-table redirection looks like this:
version: 1
name: postgres-guard
db_services:
appdb:
family: postgres
dialect: postgres
upstream: pg.internal:5432
tls_mode: terminate_reissue
database_connection_rules:
- name: allow-app-user
db_service: appdb
db_user: ["app"]
database: app
decision: allow
- name: allow-cancel
db_service: appdb
match_kind: cancel
decision: allow
database_rules:
- name: deny-mutations
db_service: appdb
operations: [write, modify, delete, schema_create, schema_alter, schema_destroy]
decision: deny
deny_mode_in_tx: terminate
- name: redirect-user-reads
db_service: appdb
operations: [read]
relations: ["public.users"]
match_object_resolution: catalog_resolved
decision: redirect
redirect:
relation: public.safe_users
- name: allow-resolved-reads
db_service: appdb
operations: [read]
relations: ["public.safe_users", "public.orders"]
match_object_resolution: catalog_resolved
decision: allow
- name: allow-safe-session
db_service: appdb
operations: [session, transaction]
decision: allow
policies:
db:
unavoidability: enforce
log_statements: parameters_redacted
approval_statement_preview: redacted
approval_statement_preview_chars: 200
escalate_unknown_functions: true
Service Fields#
| Field | Required | Description |
|---|---|---|
family | Yes | Must be postgres today. |
dialect | Yes | postgres, aurora_postgres, redshift, or cockroachdb. |
upstream | Yes | Upstream host:port. This destination is also used to generate direct-egress deny rules. |
tls_mode | Yes | terminate_reissue, terminate_plaintext_upstream, or passthrough. |
trusted_network | No | Required when using plaintext upstream mode for a non-local destination. |
allow_function_call_protocol | No | Opt into PostgreSQL FunctionCall protocol forwarding. Default is fail-closed denial. |
allow_gss_encryption | No | Opt into GSS encryption passthrough. Statement visibility is degraded. |
Connection Rules#
database_connection_rules evaluate before any SQL statement is classified. They control normal connection startup, cancellation, and replication-mode startup.
| Field | Description |
|---|---|
match_kind | connect (default), cancel, or replication. |
db_user | List of startup user names. Available only in terminate TLS modes. |
database | Startup database name. Available only in terminate TLS modes. |
application_name | Startup application name glob. Available only in terminate TLS modes. |
client_identity | agentsh internal client identity selector. Available in every TLS mode. |
decision | allow, deny, approve, or audit. redirect is invalid for connection rules. |
Replication startup and GSS encryption are default-deny. If explicitly allowed, the proxy switches to passthrough and emits a degraded_visibility_warning because statement-level inspection is no longer available.
Statement Rules#
database_rules run after the Postgres classifier converts a statement into one or more effects. Effects include an operation group, optional subtype, object set, and object-resolution confidence. Evaluation is order-independent: every object slot in every effect must be covered by a non-deny rule, and any matching deny wins.
| Selector | Description |
|---|---|
operations | Required. Operation groups or aliases such as read, write, modify, delete, session, transaction, bulk_export, unknown, READ, MUTATE, or *. |
subtypes | Optional subtype filters, for example session-setting or unsafe-I/O subtypes. |
objects | Syntactic object-name globs from the parsed statement. |
schemas | Schema globs from syntactic or catalog-resolved references. |
relations | Catalog-resolved canonical relation names formatted as schema.name. |
functions | Catalog-resolved function identities formatted as schema.name(identity_args). Use schema.name(*) for all overloads. |
match_object_resolution | Resolution filter such as catalog_resolved, qualified_syntactic, unqualified_syntactic, catalog_unresolved, or *. |
require_where | Optional syntactic guard for modify and delete rules. When true, the rule only matches top-level UPDATE or DELETE effects that include a top-level WHERE. |
deny_mode_in_tx | For deny decisions inside a transaction: terminate or rollback_then_continue. |
audit is allow-with-observation. It forwards the statement and records decision.verb: "audit". For dangerous operations, agentsh warns at policy load unless the rule sets acknowledge_audit_on_dangerous: true.
Require WHERE#
Use require_where: true for narrow mutation rules where accidental full-table updates or deletes are the main risk:
database_rules:
- name: allow-scoped-user-mutations
db_service: appdb
operations: [modify, delete]
relations: ["public.users"]
match_object_resolution: catalog_resolved
require_where: true
decision: allow
This covers statements such as UPDATE public.users SET disabled = true WHERE id = 123, but it does not cover UPDATE public.users SET disabled = true. The guard is syntactic: a top-level WHERE must exist, but agentsh does not prove that the predicate is selective or tenant-safe.
require_where only changes whether that specific rule matches. If another unguarded allow, audit, or approve rule covers the same modify or delete effect, a no-WHERE mutation can still be permitted. Policy validation rejects require_where on rules whose operations expand beyond modify and delete.
Runtime Redirect#
Statement-level decision: redirect performs safe read-only Postgres relation replacement. It is intentionally narrow:
- The rule must expand only to read operations.
- The rule must select exactly one canonical source relation through
relations. match_object_resolutionmust becatalog_resolved.- The target must be declared as
redirect.relation: schema.name. - The service must use an eligible terminate-mode TLS configuration.
The runtime path is implemented for Simple Query and Extended Query Parse. Unsupported redirect shapes fail closed; the proxy does not forward the original SQL after a redirect decision that cannot be planned safely.
Unavoidability#
policies.db.unavoidability controls whether agentsh starts the proxy and installs generated routing and bypass-prevention rules for declared services.
| Value | Behavior |
|---|---|
off | Do not install DB proxy bypass controls. |
observe | Start proxy listeners and install generated routing and bypass-prevention rules, while treating DNS expansion failures as warnings for rollout testing. |
enforce | Start proxy listeners and generated controls, and fail closed when required bypass-prevention context cannot be built. Traffic must route through the DB proxy. |
When enabled, agentsh synthesizes policy before the session starts:
connect_redirectswithredirect_to_unixroute declared upstreamhost:portdestinations to per-session DB proxy sockets.network_rulesdeny direct DB egress by hostname and resolved IP/CIDR for non-proxy processes.unix_socket_rulesdeny direct access to common local Postgres sockets such as/var/run/postgresql/.s.PGSQL.*and/tmp/.s.PGSQL.*.- Convenience command rules catch common bypass tools such as
ssh -L,socat,kubectl port-forward,cloud-sql-proxy,gcloud sql connect,aws rds connect,chisel,gost,frpc,nc/ncat/netcat, and host-network container launches.
The destination egress deny is the security boundary. The command rules are useful diagnostics, but custom tunnel binaries are still caught when they try to reach the declared upstream destination.
Audit Events#
Database proxying emits normal agentsh events, so session reports, event streams, and OCSF export can include database activity.
| Event | When emitted | Key fields |
|---|---|---|
db_statement | Each classified SQL statement or governed cancel request. | db_service, db_user, database, effects, statement_digest, decision, result, tx_context, redirect metadata. |
db_listener_auth_fail | A process with the wrong SessionID connects directly to the proxy listener. | peer_pid, peer_uid, peer_session_id, reason. |
db_handshake_fail | Startup packet or authentication forwarding fails closed. | db_service, reason, error_code, optional sni_hostname. |
degraded_visibility_warning | Policy allows replication or GSS encryption passthrough. | degraded_reason, db_service, client_identity. |
db_bypass_attempt | A generated unavoidability rule denies direct DB access. | db_service, bypass_mode, destination, process_identity, suppressed_count. |
Statement text redaction is controlled by policies.db.log_statements. The default is parameters_redacted; set full only where storing raw SQL and parameter values is acceptable.
Policy Explain#
Use the database explain command to inspect classifier effects, object resolution, per-object coverage, warnings, and the final decision offline:
agentsh policy db explain ./policy.yaml --service appdb --sql 'SELECT * FROM users'
Catalog fixtures can be supplied for local debugging of relations and functions selectors. Fixtures are snapshots, not live DB connections; the runtime proxy resolves catalog metadata through the protected database service.
Limits & Roadmap#
- Non-Postgres adapters are not runtime targets today.
- Column-level masking, row redaction, result-set DLP, and catalog-aware result metadata controls are future work.
- Credential brokering for database passwords is not part of the database proxy contract.
- Replication and GSS encryption remain default-deny unless explicitly allowed as degraded-visibility passthrough.
- TCP listener mode is not supported for the session DB proxy; agentsh uses Unix sockets with peer-credential authentication.