Introducción.
Todo lo que necesitas para integrar Vetro, configurar reglas, usar el API REST y entender el proxy AST.
Quick Start
Vetro protects your databases by evaluating every SQL query against security rules before it reaches your data. Two integration modes — choose based on your use case:
Option A: TCP Proxy (production databases)
Deploy a lightweight container in your infrastructure. Your app connects to the proxy instead of directly to the database — same credentials, different host.
# 1. Create account → Dashboard → Add Database → copy database_id
# 2. Deploy the proxy container
docker run -d --name vetro-proxy \
-e VETRO_API_URL=https://api.vetro.dev \
-e VETRO_API_KEY=vtro_your_key \
-e VETRO_DATABASE_ID=your_database_id \
-e UPSTREAM_PG_HOST=your-db-host \
-e UPSTREAM_PG_PORT=5432 \
-p 5433:5433 \
ghcr.io/donkan168/vetro-proxy:latest
# 3. Update your app's connection string
DATABASE_URL=postgres://user:pass@<proxy-host-or-ip>:5433/mydb
Option B: HTTP API (CI/CD & pipelines)
Evaluate queries via REST — no proxy deployment needed. Ideal for validating migrations before deploy.
curl -X POST https://api.vetro.dev/api/v1/proxy/query \
-H "Authorization: Bearer vtro_your_key" \
-H "Content-Type: application/json" \
-d '{"query": "DROP TABLE users", "database_id": "your_db_id", "dialect": "postgres"}'
# → { "decision": "BLOCKED", "rule_code": "VETRO-010" }
Zero stored credentials: Vetro never stores your database credentials or sends them to Vetro Cloud. The proxy runs in your infrastructure and relays the PostgreSQL authentication handshake to your database unchanged — credentials are verified by your database, not by Vetro.
Architecture
Vetro uses deterministic AST (Abstract Syntax Tree) parsing to evaluate SQL queries. No AI, no probabilistic models, no false positives — the same input always produces the same result.
How it works
Your App → Vetro Proxy (your infrastructure, port 5433)
│
├─ Parse SQL into full AST (pg_query for Postgres, sqlparser-rs for MySQL)
├─ Evaluate AST against active security rules
├─ Resolve enforcement action (BLOCK / FLAG / MONITOR)
│
├── SAFE? → Forward to your real database → relay response
└── DESTRUCTIVE? → Return SQLSTATE 42501 (query never reaches DB)
Components
| Component | Where it runs | Purpose |
|---|---|---|
| vetro-proxy | Your infrastructure | TCP wire-protocol interception. Evaluates queries in-process using vetro-engine. Reports telemetry to Vetro API. |
| Vetro API | Vetro Cloud | Dashboard, rule management, telemetry storage, audit trail, HTTP query evaluation. |
| vetro-engine | Embedded in proxy | Rust AST parser + rule evaluator. P99 < 2ms. No network calls on the hot path. |
TCP Proxy (PostgreSQL wire protocol)
The TCP proxy is a transparent interception layer. Any driver that uses the PostgreSQL wire protocol works without code changes. Your database credentials never leave your infrastructure.
Zero stored credentials
Vetro never stores your database credentials or transmits them to Vetro Cloud. The proxy runs in your infrastructure and relays the PostgreSQL authentication exchange — the Authentication / PasswordMessage flow that follows the StartupMessage — straight through to your database. Your app connects to the proxy with the same user/password it would use to connect directly, and the credentials are verified by your database, not by Vetro. With SCRAM-SHA-256 (the modern PostgreSQL default) the password is never transmitted in cleartext at all; the proxy only relays the challenge-response.
Setup
Step 1: Create a database entry in the Vetro dashboard (Settings → Databases). You'll receive a database_id.
Step 2: Deploy the proxy container with these environment variables:
# Required — connects the proxy to the Vetro control plane
VETRO_API_URL=https://api.vetro.dev
VETRO_API_KEY=vtro_xxxxxxxx # from Dashboard → API Keys
VETRO_DATABASE_ID=40eed96f-... # from Dashboard → Databases
# Required — your real database (stays in your network)
UPSTREAM_PG_HOST=my-rds.amazonaws.com
UPSTREAM_PG_PORT=5432
# Proxy listener
PROXY_PG_LISTEN_PORT=5433
Step 3: Update your application's connection string to point to the proxy:
# Before (direct to DB)
DATABASE_URL=postgres://user:pass@my-rds.amazonaws.com:5432/mydb
# After (via Vetro proxy — same credentials, different host)
DATABASE_URL=postgres://user:pass@<proxy-host-or-ip>:5433/mydb
Docker Compose example
services:
vetro-proxy:
image: ghcr.io/donkan168/vetro-proxy:latest
ports:
- "5433:5433"
environment:
VETRO_API_URL: "https://api.vetro.dev"
VETRO_API_KEY: "${VETRO_API_KEY}"
VETRO_DATABASE_ID: "${VETRO_DATABASE_ID}"
UPSTREAM_PG_HOST: "your-db-host"
UPSTREAM_PG_PORT: "5432"
PROXY_PG_LISTEN_PORT: "5433"
Optional configuration
# Telemetry buffering (default: memory)
VETRO_TELEMETRY_BUFFER=memory # or "disk" (survives restarts)
VETRO_TELEMETRY_DISK_PATH=/var/lib/vetro/spool
VETRO_TELEMETRY_BATCH_SIZE=100
VETRO_TELEMETRY_FLUSH_SECS=5
# Rule sync interval (default: 300s)
VETRO_RULES_SYNC_INTERVAL_SECS=300
# Upstream TLS — encrypt the proxy → database hop (default: disable)
UPSTREAM_PG_SSLMODE=require # disable | require | verify-full
UPSTREAM_PG_SSLROOTCERT=/etc/vetro/rds-ca.pem # CA bundle, only for verify-full
UPSTREAM_PG_SSLCERT=/etc/vetro/proxy-client.crt # optional: mutual TLS to the DB
UPSTREAM_PG_SSLKEY=/etc/vetro/proxy-client.key # (cert auth, single service identity)
# Client TLS — terminate TLS on the app → proxy hop (default: disable)
PROXY_TLS_MODE=require # disable | require
PROXY_TLS_CERT=/etc/vetro/server.crt
PROXY_TLS_KEY=/etc/vetro/server.key
Wire protocol compatibility
Vetro operates as a fully transparent layer within the PostgreSQL wire protocol. The proxy automatically detects and evaluates SQL from both protocol modes — no configuration or driver changes required:
| Protocol Mode | Description | Used by |
|---|---|---|
Simple Query ('Q') |
The client sends the complete SQL statement as a single message. Vetro extracts and evaluates the full query text inline. | psql, raw SQL execution, admin scripts, legacy applications |
Extended Protocol ('P') |
The client sends a prepared statement with parameter placeholders ($1, $2...). Vetro evaluates the query structure at the Parse phase — before values are bound — ensuring deterministic protection regardless of runtime parameters. |
Prisma, SQLAlchemy, TypeORM, Drizzle, pg (node-postgres), Hibernate, ActiveRecord |
The proxy handles both modes transparently within the same connection. All other protocol traffic (authentication handshake, query results, server notifications, COPY operations) passes through unmodified. There is no configuration needed to select between modes — Vetro inspects each message's type byte and only intercepts SQL-carrying frames.
What Vetro evaluates (not just the obvious cases)
Because Vetro walks the full PostgreSQL AST (via libpg_query — the same parser the database uses), it catches destructive statements that surface-level / regex inspection misses:
- Multi-statement queries: every statement in a batch is parsed and evaluated, and the highest-severity violation wins. A destructive tail cannot hide behind a benign head —
SELECT 1; DROP TABLE users;is blocked on theDROP. - Data-modifying CTEs & subqueries: destructive operations nested inside
WITHclauses or subqueries are detected — e.g.WITH x AS (DELETE FROM users RETURNING id) SELECT * FROM x. - Parameterized statements: prepared statements are evaluated at Parse time on the query structure, so protection is deterministic regardless of the bound values.
- Injection tautologies: always-true predicates such as
OR 1=1in aWHEREclause are flagged as SQL-injection patterns. - Multi-dialect: PostgreSQL, MySQL, Oracle and SQL Server are parsed with dialect-aware rules (e.g. MySQL
DELETE … LIMITis valid; Postgres has no such form).
TLS / Encryption
Proxy → database (upstream): set UPSTREAM_PG_SSLMODE to encrypt the hop to your database. Use require for encryption only, or verify-full to also validate the server certificate chain and hostname (provide the CA bundle via UPSTREAM_PG_SSLROOTCERT, or omit it to use the public CA roots). This is recommended whenever the database is remote (e.g. a managed RDS instance): the proxy performs the PostgreSQL SSLRequest negotiation and tunnels the session through TLS. To authenticate to a database that uses cert auth, set UPSTREAM_PG_SSLCERT + UPSTREAM_PG_SSLKEY — the proxy then presents that client certificate (one service identity) for mutual TLS.
Client → proxy: by default the proxy declines client TLS because it is designed to run inside the same trusted boundary as your application (sidecar, same host, or private subnet), where that hop never leaves your network. To encrypt it, set PROXY_TLS_MODE=require with PROXY_TLS_CERT + PROXY_TLS_KEY; the proxy answers the SSLRequest with 'S' and terminates TLS as the server. This is server-side TLS (the client authenticates the proxy).
When Vetro recommends terminating client TLS at the proxy:
- The app → proxy hop crosses an untrusted network (different host, VPC, or availability zone) rather than a sidecar / private subnet.
- Your driver enforces
sslmode=require(or stricter) and you cannot or prefer not to put a TLS-terminating load balancer (AWS NLB, HAProxy) in front. - A compliance baseline mandates encryption in transit on every hop.
If the proxy already runs as a sidecar or inside the same private subnet, this is unnecessary — leave it disabled. Inspecting SQL requires the connection to be decrypted at the proxy, so end-to-end TLS that the proxy cannot read is incompatible with query enforcement by design.
Authentication compatibility: the proxy relays the PostgreSQL authentication exchange end-to-end, so the common methods work unchanged — scram-sha-256, md5, cleartext password, and cloud token auth (AWS IAM, GCP IAM, Azure Entra). Two methods are not supported when the proxy terminates client TLS, by design: SCRAM channel binding (SCRAM-SHA-256-PLUS) and per-end-client cert auth — both bind authentication to the TLS channel specifically to detect a proxy in the middle.
Native error handling: A blocked query returns a standard PostgreSQL error with SQLSTATE 42501 (insufficient_privilege). Your application receives it through the same error handling path as any other database error — no Vetro-specific SDK, wrapper, or error parsing required.
Dashboard-managed settings (hot-reload)
The following proxy settings can be configured from the Vetro dashboard (Databases → Advanced Settings) and are applied automatically without restarting the proxy:
| Setting | Default | Description |
|---|---|---|
Rules Sync Interval | 300s | How often the proxy polls for rule updates (min: 30s) |
Telemetry Batch Size | 100 | Max events per telemetry POST |
Telemetry Flush Interval | 5s | How often the telemetry buffer is flushed |
Telemetry Memory Capacity | 10,000 | Max events in the ring buffer before oldest are dropped |
The proxy reads these settings from the /sync/rules response on every poll cycle. Changes take effect within one sync interval — no proxy restart required.
Note: TLS to your database (UPSTREAM_PG_SSLMODE and the CA certificate) is configured via environment variables in the proxy container, not the dashboard — it requires mounting a certificate file in your infrastructure.
Telemetry buffering: Memory vs Disk
The proxy buffers evaluation events before sending them to the Vetro API. Two strategies are available:
| Mode | Behavior | Best for |
|---|---|---|
memory (default) |
Events are held in a fixed-size ring buffer (default: 10,000 events). If the API is unreachable, the oldest events are dropped when the buffer fills. Lost on container restart. | Low-latency environments where occasional event loss during API outages is acceptable. Zero disk I/O overhead. |
disk |
Events are written to an append-only spool on disk. Survives container restarts and long API outages. The reporter reads and deletes from the spool after successful delivery. | Compliance-critical environments (SOC2, ISO 27001) where no event must be lost. Requires a persistent volume mount. |
# Memory mode (default — zero config)
VETRO_TELEMETRY_BUFFER=memory
VETRO_TELEMETRY_MEMORY_CAPACITY=10000 # max events before ring drops oldest
# Disk mode (persistent — requires volume)
VETRO_TELEMETRY_BUFFER=disk
VETRO_TELEMETRY_DISK_PATH=/var/lib/vetro/spool
Important: Telemetry is always non-blocking. Regardless of buffer mode, the SQL evaluation path is never delayed by telemetry I/O. Events are pushed to the buffer asynchronously after the decision is made.
Resource recommendations
The proxy is designed to be lightweight. The AST engine is compiled to native code and evaluates queries in-process — no network calls on the hot path.
| Workload | CPU | Memory | Disk | Queries/sec |
|---|---|---|---|---|
| Small (dev, staging) | 0.25 vCPU | 64 MB | None (memory buffer) | Up to 1,000 q/s |
| Medium (production) | 0.5 vCPU | 128 MB | 100 MB (if disk buffer) | Up to 10,000 q/s |
| High (high-throughput) | 1 vCPU | 256 MB | 500 MB (disk buffer) | 50,000+ q/s |
Key factors: CPU scales with query complexity (multi-JOIN queries take more AST nodes to parse). Memory scales with connection count (each TCP session holds ~4 KB state). Disk is only needed for disk-mode telemetry buffering.
Latency target: P99 < 2ms for rule evaluation. The proxy adds negligible overhead compared to network round-trip to your database.
HTTP API (alternative — no proxy deployment)
For CI/CD pipelines and pre-deploy checks, you can evaluate queries via REST without deploying the TCP proxy:
curl -X POST https://api.vetro.dev/api/v1/proxy/query \
-H "Authorization: Bearer vtro_xxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"query": "DELETE FROM users WHERE id = 1",
"database_id": "40eed96f-...",
"dialect": "postgres"
}'
# Response: { "decision": "BLOCKED", "rule_code": "VETRO-001", ... }
Decisions & Actions
Vetro separates severity (how dangerous: Critical / High / Medium / Low / Informational) from enforcement action (what to do: BLOCK / FLAG / MONITOR). The action is resolved by the workspace's enforcement policy.
| Decision | Behavior | HTTP status |
|---|---|---|
ALLOWED |
Query does not violate any active rule. TCP proxy forwards to upstream. HTTP API returns the decision. | 200 |
FLAGGED |
Query violates a rule with action FLAG: forwarded to upstream but generates telemetry + alert. Example: SELECT without LIMIT (Medium → FLAG). | 200 |
MONITORED |
Query violates a rule with action MONITOR: forwarded and logged, no alert. Example: INSERT without explicit columns (Low → MONITOR). | 200 |
BLOCKED |
Query violates a rule with action BLOCK. TCP proxy returns SQLSTATE 42501. Response includes rule_code, ast_node_path, and suggested_safe_query. |
403 |
PARSE_ERROR |
Query has invalid syntax. Default: fail-open (forward + log). Configurable to fail-closed (BLOCK) per workspace. | 200 / 403 |
Default enforcement policy
| Severity | Default Action | Example rules |
|---|---|---|
| Critical / High | BLOCK | VETRO-001 (DELETE without WHERE), VETRO-010 (DROP TABLE) |
| Medium | FLAG | VETRO-050 (SELECT without LIMIT) |
| Low / Informational | MONITOR | VETRO-060 (INSERT without columns) |
| Parse error | Allow + report | Invalid SQL syntax |
Observe mode (dry-run): When enabled, all BLOCK actions are downgraded to FLAG — nothing is blocked. Use this to validate your ruleset impact before enabling enforcement in production.
Query telemetry & privacy
For every query it evaluates, the proxy emits a telemetry event to the Vetro API (status, severity, rule code, latency, and the query text). This powers the dashboard, metrics, and audit trail. Your database credentials are never part of telemetry — but the query text can contain sensitive values in its literals (e.g. WHERE email = 'alice@acme.com').
The query telemetry mode is configurable per workspace (Dashboard → Settings → Privacidad de la telemetría):
| Mode | What is reported | Trade-off |
|---|---|---|
| raw (default) | The full query text, exactly as executed. | Maximum forensic detail in the audit trail. Literal values (potential PII) leave your network and are stored by Vetro. |
| sanitized | The query with literals normalized to placeholders ($1, $2, …) before it leaves the proxy. |
No user data leaves your network. You lose the exact values, but keep the query structure for metrics and grouping. |
Enforcement is unaffected. Rule evaluation always runs against the full query in-process; sanitization only changes what is reported. Switching to sanitized never weakens blocking — it only redacts telemetry.
Sanitization is performed structurally on the parsed AST (for PostgreSQL via libpg_query's query normalization), so it is deterministic and does not rely on regular expressions. Example:
# raw
DELETE FROM users WHERE email = 'alice@acme.com' AND tenant_id = 42
# sanitized (what Vetro reports)
DELETE FROM users WHERE email = $1 AND tenant_id = $2
Recommendation: use sanitized for workspaces handling regulated or personal data (GDPR, HIPAA, Ley 1581 de 2012), and raw when full forensic fidelity in the audit trail is the priority and queries do not embed sensitive literals.
Authentication & HTTP Integration
All programmatic access to Vetro uses API keys. Create an API key from the dashboard (API Keys page) and include it in every request as a Bearer token.
Authorization: Bearer vtro_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
API keys are scoped to a workspace and inherit the workspace's plan limits. You can create multiple keys with different names for different environments (CI, staging, production).
Query evaluation endpoint
Evaluate a SQL query against the active ruleset. Returns a decision (ALLOWED, BLOCKED, FLAGGED) without executing the query against any database. Ideal for CI/CD pipelines, pre-deploy checks, and code review automation.
Request
{
"query": "DELETE FROM users WHERE active = false",
"database_id": "40eed96f-5602-41ef-807c-a4e821a8f4fa",
"dialect": "postgres"
}
Response (BLOCKED)
{
"decision": "BLOCKED",
"rule_code": "VETRO-001",
"severity": "critical",
"ast_node_path": "DeleteStmt > WhereClause = NULL",
"suggested_safe_query": "DELETE FROM users WHERE active = false AND id = $1",
"event_id": "uuid"
}
Response (ALLOWED)
{
"decision": "ALLOWED",
"rule_code": null,
"severity": null,
"latency_ms": 0.42,
"event_id": "uuid"
}
Integration examples
curl -X POST https://api.vetro.dev/api/v1/proxy/query \
-H "Authorization: Bearer vtro_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"query": "DROP TABLE users",
"database_id": "40eed96f-5602-41ef-807c-a4e821a8f4fa",
"dialect": "postgres"
}'
# Response (403):
# { "decision": "BLOCKED", "rule_code": "VETRO-010", ... }
Response codes
| HTTP Status | Meaning |
|---|---|
200 | Query evaluated — decision is ALLOWED, FLAGGED, or MONITORED |
400 | Query has syntax errors (PARSE_ERROR) — cannot be evaluated |
403 | Query is BLOCKED — violates an active rule |
401 | Invalid or missing API key |
429 | Rate limit exceeded — retry after the indicated interval |
Security Rules
| Code | Detection | Severity | Dialects |
|---|---|---|---|
VETRO-001 | DELETE sin WHERE | CRITICAL | Todos |
VETRO-003 | DELETE con WHERE siempre verdadero (1=1) | CRITICAL | Todos |
VETRO-010 | DROP TABLE / DATABASE | CRITICAL | Todos |
VETRO-011 | TRUNCATE TABLE | CRITICAL | Todos |
VETRO-012 | DROP SCHEMA | CRITICAL | Todos |
VETRO-030 | UPDATE sin WHERE (tablas principales) | CRITICAL | Todos |
VETRO-042 | UPDATE sin WHERE | CRITICAL | Todos |
VETRO-002 | DELETE con LIMIT 0 | HIGH | MySQL, SQLite |
VETRO-013 | DROP INDEX sin IF EXISTS | HIGH | Todos |
VETRO-015 | ALTER TABLE DROP COLUMN | HIGH | Todos |
VETRO-016 | ALTER TABLE RENAME | HIGH | Todos |
VETRO-031 | UPDATE sin WHERE en CTE | HIGH | Postgres |
VETRO-033 | DELETE sin WHERE en subquery/CTE | HIGH | Todos |
VETRO-040 | INSERT INTO … SELECT sin filtro | HIGH | Todos |
VETRO-070 | SLEEP() / PG_SLEEP() | HIGH | Todos |
VETRO-090 | SQL injection — tautología OR en WHERE (OR 1=1) | CRITICAL | Todos |
VETRO-050 | SELECT sin LIMIT | MEDIUM | Todos |
VETRO-051 | SELECT * sin WHERE | MEDIUM | Todos |
VETRO-060 | INSERT sin columnas explícitas | MEDIUM | Todos |
VETRO-061 | INSERT batch > 10k filas | MEDIUM | Todos |
VETRO-PARSE-ERROR | Sintaxis SQL inválida (fail-closed) | CRITICAL | Todos |
See the complete list in the AST Rules Reference.
Error Codes
| HTTP | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid or missing parameters |
| 401 | UNAUTHORIZED | Invalid, expired, or missing API key / token |
| 403 | FORBIDDEN | Insufficient permissions for the operation |
| 403 | PLAN_UPGRADE_REQUIRED | Feature requires a higher plan |
| 403 | QUERY_BLOCKED | Query blocked by AST security rule |
| 404 | NOT_FOUND | Resource not found or purged by retention policy |
| 409 | CONFLICT | Duplicate email or name |
| 423 | ACCOUNT_LOCKED | Account locked after failed attempts (15 min) |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests. See retry_after_seconds in response |
Plans & Limits
| Feature | Free | Builder | Team | Enterprise |
|---|---|---|---|---|
| Queries / month | 500K | 5M | Unlimited | Unlimited |
| Databases | 1 | 3 | 10 | Unlimited |
| Standard rules (VETRO-*) | ✓ | ✓ | ✓ | ✓ |
| Custom rules (YAML) | — | 5 | 20 | Unlimited |
| Audit trail retention | — | 30 days | 90 days | Configurable |
| Audit trail export (CSV/JSON) | — | — | ✓ | ✓ |
| Slack / webhook alerts | — | ✓ | ✓ | ✓ |
| CI/CD integration (HTTP API) | — | ✓ | ✓ | ✓ |
| SOC2 / ISO 27001 compliance | — | — | ✓ | ✓ |
| SSO (SAML/OIDC) | — | — | — | ✓ |
| Dedicated deployment | — | — | — | ✓ |
| Uptime SLA | — | 99.5% | 99.9% | Custom |