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.

AreaBehavior
ProtocolPostgreSQL wire protocol v3, including Simple Query, Extended Query, SQL prepared statements, COPY handling, CancelRequest mapping, and transaction state tracking.
Dialectspostgres, aurora_postgres, beta redshift, beta cockroachdb.
PlatformsLinux runtime only today. Use native Linux, WSL2, or a Linux VM for enforcement.
TLS modesterminate_reissue, terminate_plaintext_upstream, and passthrough. Statement rules require terminate mode.
DecisionsConnection and statement allow, deny, approve, audit; statement-level redirect for safe read-only relation replacement.
Scope of the unavoidability claim

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.

Unix socket internally, TCP upstream for clients

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#

FieldRequiredDescription
familyYesMust be postgres today.
dialectYespostgres, aurora_postgres, redshift, or cockroachdb.
upstreamYesUpstream host:port. This destination is also used to generate direct-egress deny rules.
tls_modeYesterminate_reissue, terminate_plaintext_upstream, or passthrough.
trusted_networkNoRequired when using plaintext upstream mode for a non-local destination.
allow_function_call_protocolNoOpt into PostgreSQL FunctionCall protocol forwarding. Default is fail-closed denial.
allow_gss_encryptionNoOpt 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.

FieldDescription
match_kindconnect (default), cancel, or replication.
db_userList of startup user names. Available only in terminate TLS modes.
databaseStartup database name. Available only in terminate TLS modes.
application_nameStartup application name glob. Available only in terminate TLS modes.
client_identityagentsh internal client identity selector. Available in every TLS mode.
decisionallow, 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.

SelectorDescription
operationsRequired. Operation groups or aliases such as read, write, modify, delete, session, transaction, bulk_export, unknown, READ, MUTATE, or *.
subtypesOptional subtype filters, for example session-setting or unsafe-I/O subtypes.
objectsSyntactic object-name globs from the parsed statement.
schemasSchema globs from syntactic or catalog-resolved references.
relationsCatalog-resolved canonical relation names formatted as schema.name.
functionsCatalog-resolved function identities formatted as schema.name(identity_args). Use schema.name(*) for all overloads.
match_object_resolutionResolution filter such as catalog_resolved, qualified_syntactic, unqualified_syntactic, catalog_unresolved, or *.
require_whereOptional 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_txFor 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.

Rule-local guard

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 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.

ValueBehavior
offDo not install DB proxy bypass controls.
observeStart proxy listeners and install generated routing and bypass-prevention rules, while treating DNS expansion failures as warnings for rollout testing.
enforceStart 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:

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.

EventWhen emittedKey fields
db_statementEach classified SQL statement or governed cancel request.db_service, db_user, database, effects, statement_digest, decision, result, tx_context, redirect metadata.
db_listener_auth_failA process with the wrong SessionID connects directly to the proxy listener.peer_pid, peer_uid, peer_session_id, reason.
db_handshake_failStartup packet or authentication forwarding fails closed.db_service, reason, error_code, optional sni_hostname.
degraded_visibility_warningPolicy allows replication or GSS encryption passthrough.degraded_reason, db_service, client_identity.
db_bypass_attemptA 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#