Frontend Labs
⋯

Chapter 48 — Database security in depth

In one sentence

Beyond row-level security and encryption, a production database needs its own hardening layer — masking for non-privileged viewers, activity monitoring for bulk exfiltration, IAM-based short-lived authentication instead of static passwords, and the basic hygiene that a recurring wave of mass breaches shows teams still skip.

Where we are

Part 5 Ch 27 gave you row-level security for multi-tenancy; Ch 42 gave you field-level encryption. This chapter rounds out database hardening with the controls that catch what those two don't: a legitimately-authorized user querying too much, an exposed instance with no auth at all, and static credentials that outlive their purpose.

How it works

Dynamic data masking — controlling what's seen, not what's stored:

   Different from encryption: the underlying data is stored normally
   (encrypted at rest per Ch 42 if warranted), but the DATABASE ENGINE
   masks it AT QUERY TIME based on the caller's role -- a support analyst
   role sees `j***.d**@***.com` where an authorized service sees the real
   value, with NO application code changes needed for the masking itself.
   Available natively in SQL Server (Dynamic Data Masking), Snowflake
   (masking policies), and via extensions/proxies for Postgres/MySQL.
   USE FOR: giving broad internal visibility (support, debugging,
   analytics dashboards) without broad DATA EXPOSURE -- a middle ground
   between "full access" and "no access", cheaper to operate than
   building a bespoke masking API for every internal tool.
   NOT A SUBSTITUTE for Ch 42's field-level encryption where the threat
   model includes a privileged/root DB account or the DBA role itself --
   masking is enforced BY the engine, so an account with masking-bypass
   privilege (often granted broadly by default) sees everything.

Database Activity Monitoring (DAM) — catching bulk exfiltration:

   The threat masking and RLS don't catch: a LEGITIMATELY authorized
   account (a compromised support engineer's credentials, an insider)
   running `SELECT * FROM users` with no WHERE clause and exporting the
   result -- every individual row access was "authorized"; the PATTERN is
   the anomaly.
   DAM tools (Imperva, IBM Guardium, or NATIVE options: AWS RDS/Aurora
   Activity Streams, Google Cloud SQL audit logs, PostgreSQL's pgAudit
   extension) watch and can ALERT on:
     - queries returning unusually large result sets for that role/user's
       normal pattern.
     - a `SELECT *` / broad export pattern from an account that normally
       does narrow, parameterised lookups.
     - access outside normal hours / from a new source IP for that
       credential.
     - schema/permission changes outside a change window.
   Pair with QUERY RESULT SIZE CAPS and paginated-by-default APIs at the
   application layer (G1 Ch 45/this guide's Ch 28) as a structural
   backstop, not just a detection rule.

Authentication — IAM-based over static passwords:

   STATIC DB PASSWORDS (even rotated ones, G1 Ch 49) are a standing
   secret that can leak, get committed, or get shared between environments.
   IAM DATABASE AUTHENTICATION (AWS RDS/Aurora IAM auth, GCP Cloud SQL
     IAM auth, Azure AD authentication for SQL) issues SHORT-LIVED AUTH
     TOKENS derived from the calling workload's IAM/cloud identity
     (Part 2 Ch 5, Part 4 Ch 20's workload identity) instead of a
     password -- the same short-lived-credential principle applied to the
     database connection itself. No password to leak, rotate, or store;
     access is governed by the SAME IAM policy graph as everything else
     (Part 2 Ch 6), so revoking a compromised workload's IAM access also
     revokes its DB access immediately.
   Where IAM auth isn't available: DYNAMIC SECRETS from a vault (G1 Ch 49)
     -- a short-lived DB credential minted per session and auto-expired --
     is the next-best option, strictly better than a long-lived static
     password.

Least privilege and account hygiene:

   [ ] a SEPARATE DB account per SERVICE (not one shared "app" account
       for everything) -- so a compromised service's blast radius is
       exactly its own grants, and query-pattern anomalies (DAM, above)
       are attributable to a specific service, not "the app" generically.
   [ ] NO service account is a superuser, the table owner (unless FORCE
       RLS is relied on, Part 5 Ch 27), or has BYPASSRLS.
   [ ] STORED PROCEDURES built with dynamic SQL are just as
       injection-prone as application code (G1 Ch 42) -- parameterise
       inside procedures too; a proc that concatenates a caller-supplied
       string isn't safer for being server-side.
   [ ] REVIEW extensions/plugins: some (Postgres `dblink`, untrusted
       procedural languages) can be privilege-escalation vectors if
       available to non-superuser roles -- audit what's installed and
       who can invoke it.
   [ ] PATCH the engine on a defined cadence; database CVEs are as real
       as application CVEs (G1 Ch 47) and often more severe given the
       blast radius.

The exposed-database problem — still a leading mass-breach cause:

   A recurring, entirely preventable pattern: a MongoDB/Elasticsearch/
   Redis instance bound to 0.0.0.0 and exposed directly to the internet
   with NO AUTHENTICATION -- automated scanners (Shodan-driven) find and
   exfiltrate/ransom these within HOURS of exposure. This traces back to
   defaults: pre-2.6 MongoDB shipped with NO auth enabled by default,
   causing a wave of mass exposures that continues to recur as new
   instances get spun up from old images/tutorials/defaults.
   THE FIX IS BASIC AND NON-NEGOTIABLE: authentication ALWAYS enabled
   (never rely on "it's only reachable internally" as the sole control --
   G1 Ch 39/this guide's Part 2 Ch 8's network layer is defence IN DEPTH,
   not a substitute); bind to private interfaces only; use the engine's
   real access-control layer (Elasticsearch/OpenSearch security
   plugin/X-Pack, Redis AUTH + ACLs since Redis 6, MongoDB's role-based
   access control) -- and CONTINUOUSLY SCAN your own external attack
   surface (an internal Shodan-equivalent check, or your cloud's exposed-
   resource finder) so you find an accidental exposure before an
   attacker's scanner does.

Worked example

Catching a slow-motion insider exfiltration that RLS and encryption both missed.

   A support engineer's LEGITIMATE role (needed for ticket resolution)
   has broad SELECT on the `users` and `orders` tables -- correctly
   scoped by RLS to be read-only and (per Ch 42) field-encrypted PII
   returns as ciphertext for THIS role specifically (only a narrower
   "support-lookup" service role, invoked per-ticket with an authorization
   check, ever gets plaintext -- Ch 42's worked example, applied).

   Over three weeks, the engineer's credentials (phished, unknown to
   anyone yet) are used to run a SLOWLY INCREASING number of broad,
   unparameterised queries -- staying just under any single-query size
   cap, but climbing well above this account's historical baseline
   volume.

   RLS: not violated (queries stay within the account's own tenant scope
   in this case -- it's a single-tenant internal support account with
   legitimately broad internal access). Field encryption: not
   violated (ciphertext returned for encrypted fields, as designed --
   this exfiltration is of NON-field-encrypted metadata: order IDs,
   timestamps, product categories, city-level location -- still
   commercially sensitive and still personal data, just not the specific
   fields chosen for field-level encryption).

   WHAT ACTUALLY CATCHES IT: Database Activity Monitoring, comparing this
   account's DAILY QUERY VOLUME and DISTINCT-ROW-COUNT against its
   90-day baseline -- flags a gradual, sustained deviation that no
   single-query threshold would trip. The alert routes to the detection
   pipeline (Ch 54) with the account, the query pattern, and the volume
   trend attached.

   LESSON: RLS and field encryption are necessary but answer different
   questions ("is this query scoped to the right tenant" and "is this
   specific field protected from broad access") than DAM answers ("is
   this account's OVERALL BEHAVIOUR consistent with its normal, legitimate
   use") -- you need all three layers, because each one is blind to what
   the others catch.

Practice (75 min)

  1. Set up pgAudit (or your cloud's native DB audit logging) on a Postgres instance; run a mix of narrow and broad (SELECT *, no LIMIT) queries; confirm the audit log distinguishes them with enough detail to build a volume/pattern baseline.
  2. Configure dynamic data masking (or a proxy-based equivalent) for one column; confirm a "support" role sees masked values while an authorized service role sees real ones, with zero application code changes for the masking itself.
  3. Migrate one service's DB connection from a static password to IAM database authentication (or a Vault dynamic secret if IAM auth isn't available for your engine); confirm revoking the workload's IAM role/policy immediately breaks its DB access.
  4. Audit a real (or sandbox) database for: shared service accounts, superuser/ BYPASSRLS grants on app roles, installed extensions with elevated capability, and any instance reachable without authentication. Fix what you find.
  5. Run an external exposure scan (your cloud's public-resource finder, or a Shodan search for your own IP ranges) and confirm no database is internet-reachable without your knowledge.

Common mistakes

  • Treating RLS or encryption as covering "database security" entirely. Neither catches a legitimately-scoped account behaving anomalously at volume — that needs activity monitoring.
  • A shared "app" database account across services. Erases attribution and maximises blast radius; one account per service.
  • Static, long-lived DB passwords, especially shared across environments. Move to IAM auth or dynamic secrets.
  • Assuming "it's on a private subnet" is sufficient auth. Defence in depth means the database's OWN authentication must always be on, regardless of network placement — the exposed-database breach wave happened to instances people also assumed were "internal only."
  • Dynamic SQL inside stored procedures. Just as injectable as application code with string concatenation.
  • Never auditing installed extensions/plugins. Some carry privilege- escalation risk if reachable by non-superuser roles.
  • No baseline for "normal" query volume/pattern per account. Without one, DAM has nothing to compare against and produces noise instead of signal.

Check yourself

  1. How does dynamic data masking differ from field-level encryption, and what threat does masking NOT cover that encryption does?
  2. What does Database Activity Monitoring catch that row-level security and encryption both miss?
  3. Why is IAM database authentication preferable to static passwords, and what happens to DB access when you revoke the underlying IAM identity?
  4. Give four items on the least-privilege/account-hygiene checklist for databases.
  5. What historical default caused a wave of exposed-database mass breaches, and what's the non-negotiable fix?
  6. In the worked example, why did neither RLS nor field encryption catch the exfiltration, and what did?

Further reading

  • Docs: PostgreSQL pgAudit extension documentation; AWS "RDS/Aurora Database Activity Streams"; Snowflake and SQL Server dynamic-masking documentation.
  • IAM auth: AWS "IAM database authentication for MariaDB, MySQL, and PostgreSQL"; GCP "Cloud SQL IAM database authentication".
  • History: the "MongoDB ransom" wave write-ups (2017 and recurring) and Shodan's own blog posts on exposed-database scanning — sobering and educational on how fast unauthenticated instances are found.
  • Standard: CIS Benchmarks for PostgreSQL/MySQL/MongoDB — a concrete hardening checklist to run against any instance you operate.

Generated from real-life-security-guide-v1.md · 8 Oct 2026 16:36