Skip to main content

Security

DBBat implements multiple security layers to protect both the proxy infrastructure and the target databases.

Authentication

Password Hashing

User passwords are hashed using Argon2id, the winner of the Password Hashing Competition:

  • Memory-hard algorithm resistant to GPU/ASIC attacks
  • Configurable memory, time, and parallelism parameters
  • Includes salt to prevent rainbow table attacks

Password Requirements

  • Mandatory change: Users must change their initial password before accessing the API
  • Minimum length: 8 characters (configurable)
  • Login attempts before password change return 403 password_change_required

Authentication Rate Limiting

Failed login attempts trigger exponential backoff per username:

Failed AttemptsLockout Duration
1-2None
3-45 seconds
5-630 seconds
7-92 minutes
10+5 minutes

This prevents brute-force attacks while allowing legitimate users to recover from typos.

Token Types

TypePrefixLifetimeUse Case
Web Sessionweb_1 hourInteractive frontend use
API Keydbb_Configurable (or permanent)Programmatic access

API Key Restrictions

API keys have intentional limitations:

  • Cannot create other API keys
  • Cannot revoke API keys
  • These operations require web session or basic auth

This prevents a compromised API key from being used to create persistent backdoor access.

Encryption

Database Credentials

Target database passwords are encrypted at rest using AES-256-GCM:

  • 256-bit encryption keys
  • Authenticated encryption (integrity + confidentiality)
  • Random nonce per encryption operation
  • AAD binding: Ciphertext is bound to the database ID, preventing credential transplant attacks

Key Management

Encryption keys are provided via environment:

VariableDescription
DBB_KEYBase64-encoded 32-byte key
DBB_KEYFILEPath to file containing the key

Keys are:

  • Never logged or exposed via API
  • Never transmitted over the network
  • Required at startup (DBBat won't start without a valid key)

Role-Based Access Control

Roles

RoleDescription
adminFull access to all resources and operations
viewerRead-only access to observability data (queries, connections, audit)
connectorCan only connect to servers with active grants

Users can have multiple roles. Permissions are additive.

Resource Visibility by Role

ResourceAdminViewerConnector
All usersFullList onlyOwn only
All serversFull detailsName/descriptionGranted only
SSH serversFull detailsNoneNone
All grantsFullFullOwn only
All queriesFullFullNone
All connectionsFullFullOwn only
Audit logFullFullNone
API keysOwn keys (all users with ?all_users=true)Own keysOwn keys

SSH servers are admin-only. A server with protocol: ssh is a bastion definition, not a proxied target: it can never be the target of a grant, and non-admins never see it. Its only role is to be referenced by another server's via_uid.

API key listing is caller-scoped. GET /api/v1/keys returns only the calling user's own keys, including for admins. Admins can pass ?all_users=true to list every user's keys.

Access Grants

Grants control which users can connect to which servers through the proxy.

Grant Constraints

ConstraintDescription
starts_atGrant is not valid before this time
expires_atGrant automatically expires after this time
max_query_countsMaximum queries allowed (quota)
max_bytes_transferredMaximum data transfer allowed (quota)
controlsCombination of read_only, block_copy, block_ddl. Empty = full write access.

Recommendation: Always set all constraints. Time-limited grants with quotas minimize blast radius if credentials are compromised.

Grant Lifecycle

  1. Admin creates grant with constraints
  2. Grant becomes active at starts_at
  3. User can connect and execute queries
  4. Quotas and the time window are enforced mid-stream, not only between commands
  5. Grant expires at expires_at or when revoked
  6. Revoking a grant blocks further queries and disconnects sessions already live under it, across all protocols
  7. Revoked grants record revoked_at and revoked_by for audit

Mid-stream limit enforcement

Grant time windows and byte quotas are checked continuously while data flows, not only at the boundary between commands. A single long query that streams past its remaining time or byte budget is cut off partway through its result set rather than being allowed to run to completion.

Bytes already transferred by such an aborted query are still persisted, so quota accounting stays accurate even when the query never finished.

The same applies to revocation: revoking a grant does not merely refuse new connections — sessions already established under that grant are torn down.

Upstream Identity

DBBat encodes the acting DBBat username into the upstream connection metadata, so monitoring on the database side attributes queries to the real human rather than to a shared service account:

EngineField carrying the DBBat username
PostgreSQLapplication_name
MySQL / MariaDBprogram_name
OracleAUTH_PROGRAM_NM

This means pg_stat_activity, performance_schema.session_connect_attrs, v$session, and engine-level audit logs all name the individual user. Auditability therefore does not depend solely on DBBat's own records — the upstream database keeps a correlatable trace of its own.

Read-Only Mode

When a grant has read_only in its controls, DBBat enforces read-only access through defense in depth.

Layer 1: Query Inspection (all engines)

Queries are inspected and blocked if they match write patterns:

  • DML: INSERT, UPDATE, DELETE, MERGE, REPLACE
  • DDL: CREATE, ALTER, DROP, TRUNCATE
  • DCL: GRANT, REVOKE
  • Other: COPY FROM (PG), CALL (procedures), LOAD DATA, SELECT … INTO OUTFILE, SELECT … INTO DUMPFILE (MySQL)

Layer 2: Engine-level session flag

  • PostgreSQL — at connection establishment, DBBat sets:

    SET SESSION default_transaction_read_only = on;

    PostgreSQL then blocks any write regardless of SQL syntax.

  • MySQL/MariaDBSET SESSION TRANSACTION READ ONLY only applies to the next transaction and is trivially bypassable, so DBBat does not rely on it. Layer 1 (regex inspection) is the active control. Recommendation: also GRANT SELECT only to the upstream MySQL user.

  • Oracle — same as MySQL: regex inspection only. The defensive recommendation is to grant CREATE SESSION + SELECT privileges to the upstream Oracle user, nothing more.

Layer 3: Bypass Prevention (PostgreSQL)

Attempts to disable read-only mode are blocked:

  • SET default_transaction_read_only = off
  • RESET default_transaction_read_only
  • SET SESSION AUTHORIZATION (privilege escalation)
  • SET ROLE (privilege escalation)

Limitations

Read-only mode is defense in depth for trusted users, not a security boundary against malicious actors:

  • Regex-based inspection may miss edge cases
  • New SQL syntax could bypass detection
  • Functions with SECURITY DEFINER might execute writes

For untrusted access: also restrict the upstream database user to read-only privileges.

MySQL LOCAL INFILE Defense

LOAD DATA LOCAL INFILE lets a MySQL server ask a connected client to upload an arbitrary local file. A compromised upstream server could issue this request mid-query against any client. DBBat blocks it on two layers:

  1. SQL regex refuses the keyword in inbound client queries.
  2. Capability opt-out: when the proxy connects upstream it explicitly clears CLIENT_LOCAL_FILES from the negotiated capabilities. The upstream then never advertises the feature on this connection, so even a compromised server cannot request a LOCAL INFILE upload through the proxy.

Audit Trail

What's Logged

Event TypeData Captured
ConnectionsUser, database, source IP, timestamps, query count
QueriesSQL text, parameters, execution time, rows affected, errors
Query ResultsAll result rows (for replay/audit)
Access ChangesGrant creation, revocation, user changes

Audit Log Integrity

  • Audit logs are append-only (no UPDATE/DELETE via API)
  • Protected from modification via proxy (internal table protection)
  • Includes performed_by for accountability

API Rate Limiting

All authenticated endpoints are rate-limited:

  • Per-user request limits
  • Response headers indicate remaining quota
  • 429 Too Many Requests when exceeded

Rate limit exempt users can be configured for automation/CI.

Network Security

Upstream Connections

DBBat supports PostgreSQL SSL modes for upstream connections:

ModeDescription
disableNo SSL
preferTry SSL, fall back to plain (default)
requireRequire SSL, don't verify certificate
verify-caRequire SSL, verify CA
verify-fullRequire SSL, verify CA and hostname

Recommendation: Use require or stronger for production.

Client Connections

  • PostgreSQL listener: plain protocol only. Deploy behind a TLS-terminating load balancer, a VPN, or a private network.
  • Oracle listener: plain TNS only. Same recommendation.
  • MySQL listener: TLS termination is built in. Configure DBB_MYSQL_TLS_CERT_FILE / DBB_MYSQL_TLS_KEY_FILE (PEM-encoded) for production. If unset, the proxy auto-generates a self-signed cert and an RSA-2048 keypair at startup — fine for development, not for production. DBB_MYSQL_TLS_DISABLE=true refuses TLS and stays plaintext-only.

SSH Bastions

A server can set via_uid pointing at another server whose protocol is ssh. Its upstream connection is then dialled through that SSH bastion. This works for all four proxied protocols (PostgreSQL, Oracle, MySQL/MariaDB, MongoDB).

Host-key trust model (TOFU)

Bastion host keys are pinned trust-on-first-use:

  • The first successful connection to a bastion records the host key it presented.
  • Every later connection must present that same key, or the connection is refused.
  • The pinned key is exposed read-only on the server as ssh_known_host_key, so operators can inspect and compare it out of band.

What this does protect against: a man-in-the-middle appearing after the key was pinned, and silent substitution of the bastion.

What this does not protect against: an attacker already in position on the very first connection — that key is trusted without prior knowledge. Verify ssh_known_host_key against the bastion's real fingerprint after creating the server, particularly on untrusted networks.

Private key storage

SSH private keys and their passphrases are write-only. They can be set or replaced through the API, but are never returned by any read endpoint — the same encryption-at-rest treatment as target database credentials.

Security Checklist

Deployment

  • Set strong encryption key (DBB_KEY or DBB_KEYFILE)
  • Use separate database for DBBat storage
  • Enable TLS for upstream connections (ssl_mode: require)
  • Deploy in private network or behind VPN
  • Change default admin password immediately

Operations

  • Use time-limited grants (hours/days, not years)
  • Set query and byte quotas on all grants
  • Prefer read_only (and block_ddl / block_copy where useful) unless writes are required
  • Review audit logs regularly
  • Rotate API keys periodically
  • Monitor for blocked query attempts

SSH Bastions

  • Verify ssh_known_host_key against the bastion's real host-key fingerprint after first connection
  • Use a dedicated, unprivileged SSH account for DBBat on each bastion
  • Prefer key authentication over passwords; keep the private key write-only in DBBat and never re-export it
  • Restrict the bastion account to port forwarding only (no shell), and to the target host/port
  • Remember that SSH servers are admin-only and can never be granted to a user directly

For Target Databases

  • Use a dedicated upstream user for each target (PostgreSQL, Oracle, MySQL/MariaDB, MongoDB)
  • Grant minimum required privileges to that user
  • For read-only grants, also restrict the upstream user to read-only privileges
    • PostgreSQL: GRANT SELECT only
    • MySQL/MariaDB: GRANT SELECT ON db.* TO 'dbbat_ro'@'%'
    • Oracle: CREATE SESSION + SELECT privileges only
  • Enable engine-level audit logging as an additional layer