name: securing-databases description: Hardens database security through least-privilege roles, injection-proof query patterns, secret management, TLS, encryption, and audit logging. Use when the user asks to secure a database, create database users/roles/grants, prevent SQL injection, review database credentials or connection strings, enable TLS for database connections, set up row-level security for multi-tenant data, or audit database access. Do not use for application-level authentication (sessions, JWT, OAuth) or general network firewall configuration beyond database exposure.
Securing Databases
When to use / when NOT to use
- Use for: roles and grants, SQL-injection defense, credential/secret handling, connection TLS, at-rest encryption, row-level security (RLS), audit logging, database network exposure.
- Do NOT use for: app-level auth (sessions/JWT/OAuth), OS hardening, or firewall design beyond keeping the DB port private. Backup encryption lives in
backing-up-databases.
Core rules
- Least privilege, three roles minimum. Owner (runs migrations, owns objects) ≠ app role (DML only) ≠ read-only role (analytics/humans). The app NEVER connects as owner or superuser.
- ✅
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA app TO app_rw; - ❌
postgres://postgres:...in the application config
- ✅
- Parameterized queries are the ONLY injection defense. Escaping, sanitizing, or quoting by hand is always a finding.
- ✅
cur.execute("SELECT * FROM users WHERE email = %s", (email,)) - ❌
f"SELECT * FROM users WHERE email = '{email}'"— even for "trusted" input - Identifiers (table/column names) can't be parameters: allow-list them from a fixed set.
- ✅
- Secrets live in the environment or a secret manager — never in code, VCS, or logs. Rotate any credential that ever hit a repository; deletion does not un-leak it.
- TLS on every connection that leaves the host. PostgreSQL:
sslmode=verify-fullin the DSN (not the defaultprefer, which downgrades silently). - The database port is never public. Private network/VPC only; humans reach it via bastion or SSH tunnel; app reaches it via internal network.
0.0.0.0bindings andpg_hba.confhost all all 0.0.0.0/0entries are findings. - Multi-tenant data gets row-level security. RLS with a
tenant_idpolicy enforced in the database beats every "remember the WHERE clause" convention. - Privileged access is logged. At minimum: log DDL and role changes (
log_statement = 'ddl'), pluspgaudit(or the engine's equivalent) when compliance requires read auditing. - Encrypt sensitive data at rest. Default: full-disk/volume encryption; column-level (
pgcrypto) only for fields needing protection from DB admins themselves.
Workflow
- Inventory: current roles and grants, where credentials are stored, how connections reach the DB (network path, TLS), what data is sensitive.
- Fix the highest-severity gaps in this order: public exposure (rule 5) → superuser app connections (rule 1) → injection patterns (rule 2) → plaintext secrets (rule 3) → TLS (rule 4).
- Apply RLS/audit/encryption (rules 6–8) as the data model requires.
- Validate: connect as the app role and confirm a privileged action FAILS (
CREATE TABLE,DROP TABLE, reading another tenant's rows under RLS). Grep the codebase for string-built SQL (f"SELECT,"+ sql,format(near queries) and report every hit. The task is not done until both checks run.
Edge cases & failure modes
- ORM in use → ORMs parameterize by default, but
raw()/text()/WHEREstring fragments reintroduce injection; audit those call sites specifically. - Legacy app owns everything as one role → migrate incrementally: create the new roles, move the app connection first, keep owner for migrations only.
- RLS and the owner role → table owners and superusers BYPASS RLS unless the policy role is
FORCEd; test RLS as the app role, never as owner. - Secret already committed to git → rotate immediately; treat history rewriting as cleanup, not remediation.
- Managed databases → provider handles disk encryption and network; rules 1–4, 6–7 remain fully yours.
References
Grants, RLS policies, TLS DSNs, and audit setup: see references/patterns.md