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 Attempts | Lockout Duration |
|---|---|
| 1-2 | None |
| 3-4 | 5 seconds |
| 5-6 | 30 seconds |
| 7-9 | 2 minutes |
| 10+ | 5 minutes |
This prevents brute-force attacks while allowing legitimate users to recover from typos.
Token Types
| Type | Prefix | Lifetime | Use Case |
|---|---|---|---|
| Web Session | web_ | 1 hour | Interactive frontend use |
| API Key | dbb_ | 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:
| Variable | Description |
|---|---|
DBB_KEY | Base64-encoded 32-byte key |
DBB_KEYFILE | Path 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
| Role | Description |
|---|---|
admin | Full access to all resources and operations |
viewer | Read-only access to observability data (queries, connections, audit) |
connector | Can only connect to servers with active grants |
Users can have multiple roles. Permissions are additive.
Resource Visibility by Role
| Resource | Admin | Viewer | Connector |
|---|---|---|---|
| All users | Full | List only | Own only |
| All servers | Full details | Name/description | Granted only |
| SSH servers | Full details | None | None |
| All grants | Full | Full | Own only |
| All queries | Full | Full | None |
| All connections | Full | Full | Own only |
| Audit log | Full | Full | None |
| API keys | Own keys (all users with ?all_users=true) | Own keys | Own 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
| Constraint | Description |
|---|---|
starts_at | Grant is not valid before this time |
expires_at | Grant automatically expires after this time |
max_query_counts | Maximum queries allowed (quota) |
max_bytes_transferred | Maximum data transfer allowed (quota) |
controls | Combination 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
- Admin creates grant with constraints
- Grant becomes active at
starts_at - User can connect and execute queries
- Quotas and the time window are enforced mid-stream, not only between commands
- Grant expires at
expires_ator when revoked - Revoking a grant blocks further queries and disconnects sessions already live under it, across all protocols
- Revoked grants record
revoked_atandrevoked_byfor 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:
| Engine | Field carrying the DBBat username |
|---|---|
| PostgreSQL | application_name |
| MySQL / MariaDB | program_name |
| Oracle | AUTH_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/MariaDB —
SET SESSION TRANSACTION READ ONLYonly 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: alsoGRANT SELECTonly to the upstream MySQL user. -
Oracle — same as MySQL: regex inspection only. The defensive recommendation is to grant
CREATE SESSION+SELECTprivileges 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 = offRESET default_transaction_read_onlySET 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 DEFINERmight 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:
- SQL regex refuses the keyword in inbound client queries.
- Capability opt-out: when the proxy connects upstream it explicitly clears
CLIENT_LOCAL_FILESfrom the negotiated capabilities. The upstream then never advertises the feature on this connection, so even a compromised server cannot request aLOCAL INFILEupload through the proxy.
Audit Trail
What's Logged
| Event Type | Data Captured |
|---|---|
| Connections | User, database, source IP, timestamps, query count |
| Queries | SQL text, parameters, execution time, rows affected, errors |
| Query Results | All result rows (for replay/audit) |
| Access Changes | Grant 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_byfor accountability
API Rate Limiting
All authenticated endpoints are rate-limited:
- Per-user request limits
- Response headers indicate remaining quota
429 Too Many Requestswhen exceeded
Rate limit exempt users can be configured for automation/CI.
Network Security
Upstream Connections
DBBat supports PostgreSQL SSL modes for upstream connections:
| Mode | Description |
|---|---|
disable | No SSL |
prefer | Try SSL, fall back to plain (default) |
require | Require SSL, don't verify certificate |
verify-ca | Require SSL, verify CA |
verify-full | Require 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=truerefuses 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_KEYorDBB_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(andblock_ddl/block_copywhere useful) unless writes are required - Review audit logs regularly
- Rotate API keys periodically
- Monitor for blocked query attempts
SSH Bastions
- Verify
ssh_known_host_keyagainst 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 SELECTonly - MySQL/MariaDB:
GRANT SELECT ON db.* TO 'dbbat_ro'@'%' - Oracle:
CREATE SESSION+SELECTprivileges only
- PostgreSQL:
- Enable engine-level audit logging as an additional layer