Today’s Lesson
Security for Legal SaaS — Episode 32: Database Security Hardening
Your Database Is the Crown Jewels
Every security control we have discussed in this series — authentication, encryption, input validation, access control — exists to protect the data that ultimately lives in your database. Client communications, case strategy, financial records, privileged documents — they all converge in one place. If an attacker reaches your database with unrestricted access, every other defence has failed.
This episode covers the layers of protection that surround a production database: network isolation, connection security, access control, audit logging, and backup protection. We will use PostgreSQL as the primary example because it is the most common database in modern legal SaaS, but the principles apply to any relational database.
Network Isolation: Databases Should Never Face the Internet
The most fundamental database security rule is also the most frequently violated: your database should not be reachable from the public internet. It should sit in a private subnet — a network segment that has no public IP address and no route to the internet. The only systems that can connect to it are your application servers, which sit in a separate subnet with controlled ingress.
| Architecture | Risk Level | Description |
|---|---|---|
| Database with public IP | Critical | Directly attackable from anywhere on the internet |
| Database in private subnet, app in public subnet | Moderate | Only reachable via the application; but app compromise = DB access |
| Database in private subnet, app in private subnet, load balancer public | Recommended | Database is two network hops from the internet |
| Database in private subnet with VPC peering / Private Link | Strongest | No internet path exists; accessed only via private cloud network1 |
On AWS, this means placing your RDS instance in a VPC (Virtual Private Cloud — an isolated private network, as we covered in Episode 11) with no public accessibility. On GCP, Cloud SQL instances should use private IP only. Security groups (AWS) or firewall rules (GCP) should restrict inbound connections to only the IP addresses or security groups of your application servers — not `0.0.0.0/0`, which means "anywhere."2
Legal SaaS reality check: Halcyon tracked over 200 ransomware incidents targeting law firms between 2025 and early 2026. The INC Ransom group specifically exploits known vulnerabilities in remote access tools to reach internal databases. Network isolation is not theoretical — it is the primary barrier against these attacks.3
Connection Security: TLS Required, Always
Every connection between your application and the database must be encrypted using TLS (Transport Layer Security, which we covered in Episode 13). PostgreSQL supports TLS natively, but it is not enabled by default in many configurations.
Configuration requirements:
- Set `sslmode=verify-full` on the client. This ensures the client verifies the server's TLS certificate and checks that the hostname matches. Weaker modes like `sslmode=prefer` will silently fall back to unencrypted connections if TLS negotiation fails — which is exactly what a man-in-the-middle attacker causes.4
- Use certificates from your internal CA or cloud provider. AWS RDS provides managed certificates. For self-hosted PostgreSQL, generate certificates through your PKI (public key infrastructure, from Episode 15) or HashiCorp Vault's PKI secrets engine.
- Reject unencrypted connections. In `pg_hba.conf`, use `hostssl` instead of `host` to require TLS for all connections.
Connection Pooling Credentials
Most production applications use a connection pool — a set of pre-established database connections shared across request threads. The pool itself authenticates to the database using a service account. This creates a credential management challenge:
- The pooler (e.g., PgBouncer, built-in pool in your ORM) needs database credentials.
- Those credentials must be rotated without dropping active connections.
- Dynamic secrets from a secrets manager (as covered in Episode 30) are ideal: the pool authenticates with short-lived credentials that are automatically rotated.
Principle of Least Privilege: Minimal Permissions
Your application's database account should have the minimum permissions necessary for its function. In Episode 8, we introduced this concept in the context of SQL injection. Here it applies to the database account itself:
| Permission | Your Application Needs | Your Application Does NOT Need |
|---|---|---|
| SELECT, INSERT, UPDATE, DELETE on application tables | Yes | — |
| CREATE TABLE, ALTER TABLE, DROP TABLE | No (run migrations with a separate admin account) | Yes — your app should never restructure the schema at runtime |
| SUPERUSER / rds_superuser | Never | Yes — this bypasses all access controls |
| Access to `pg_catalog` system tables | Rarely | Attackers use these to map the database structure |
Create separate database roles for different application functions. Your read-only reporting service should not have write access to the case management tables. Your search indexer should not have access to billing data. If your application has a background job that sends email notifications, it needs access to the notification queue — not to client documents.5
Row-Level Security Revisited
In Episode 8, we covered PostgreSQL's Row-Level Security (RLS) as a defence against SQL injection escalation. RLS is equally critical for multi-tenant legal SaaS: every query is automatically filtered by the current tenant's identifier, enforced by the database engine — not by application code that might have a bug.
-- Enable RLS on the documents table
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Policy: users can only see documents belonging to their tenant
CREATE POLICY tenant_isolation ON documents
USING (tenant_id = current_setting('app.current_tenant')::uuid);
Even if application code fails to include a `WHERE tenant_id = ?` clause, the database rejects cross-tenant access.6
Audit Logging: Who Queried What, When
Database-level audit logging records every operation — who connected, what they queried, and when. This is distinct from application-level logging (which records what the application did) and is critical because it captures direct database access that bypasses the application.
For PostgreSQL, the pgAudit extension provides detailed session and object-level audit logging:7
| Log Type | What It Captures | Use Case |
|---|---|---|
| Connection logging (`log_connections`, `log_disconnections`) | Every connection attempt and disconnection | Detecting unauthorised access attempts |
| Statement logging (pgAudit) | SQL statements executed, with parameters | Forensic investigation; compliance audits |
| Object-level audit (pgAudit) | Access to specific tables or columns | Monitoring access to privileged client data |
The CIS PostgreSQL Benchmark — a prescriptive collection of security controls from the Center for Internet Security — mandates enabling `log_connections` as a baseline requirement. It also recommends configuring `log_statement = 'ddl'` at minimum to capture schema changes.8
Storage and retention: Audit logs must be shipped to a separate system (a SIEM — Security Information and Event Management, as we introduced in Episode 7) where they cannot be tampered with by someone who compromises the database server. If the attacker can delete the logs, the audit trail is worthless.
Backup Encryption and Access Controls
Database backups are copies of your crown jewels. They deserve the same protection as the production database — possibly more, because backups are often stored in less-monitored locations.
Requirements:
- Encrypt backups at rest. AWS RDS supports automated encrypted backups using KMS keys (from Episode 29). For self-hosted databases, use `pg_basebackup` with output piped through encryption.9
- Restrict backup access. The IAM roles or accounts that can access backup storage should be separate from those that access the production database.
- Test restore procedures. An encrypted backup you cannot restore is not a backup. Regularly test that you can decrypt and restore from backups — including with rotated keys.
- Apply the same network isolation to backup storage. S3 buckets (or equivalent) holding database backups should not be publicly accessible. Enable S3 Block Public Access and use VPC endpoints for access.10
The Hardening Checklist
| Control | Default State | Hardened State |
|---|---|---|
| Public accessibility | Often enabled | Disabled; private subnet only |
| TLS for connections | Optional | Required (`hostssl` in pg_hba.conf) |
| Default `postgres` superuser | Exists with password | Renamed or disabled; no direct login |
| Application account permissions | Often SUPERUSER for convenience | Minimal: DML only, no DDL, no superuser |
| Audit logging | Disabled | pgAudit enabled; logs shipped to SIEM |
| Backup encryption | Depends on provider | Always on; KMS-managed keys |
| pg_hba.conf | Permissive defaults | Strict IP allowlist per role |
What's Next
Episode 33 begins Module 8: AI-Specific Security with Prompt Injection Attacks — why the most dangerous vulnerability in AI-powered legal tools is fundamentally different from SQL injection, and why there is no parameterised query equivalent to fix it.
Sources & Further Reading
Sources & references
- AWS, Amazon VPC — Private Subnets.
- AWS, Security Best Practices for Amazon RDS.
- Halcyon, INC Ransom Group Mounts Rapid Campaign Against Law Firms.
- PostgreSQL, SSL Support — Client Verification.
- CIS, CIS PostgreSQL Benchmarks.
- PostgreSQL, Row Security Policies.
- pgAudit, PostgreSQL Audit Extension.
- SOCFortress, PostgreSQL Hardening Guide Aligned with CIS Benchmark.
- Crunchy Data, Secure PostgreSQL 14 with the CIS Benchmark.
- Embroker, Law Firm Cyberattacks: Stats and Trends for 2025.
- HexaCluster, PGDSAT — PostgreSQL Database Security Assessment Tool.
- CodeOps Trek, Secure Your PostgreSQL Database: Dockerized & Hardened with CIS Benchmarks.