pg_hba.conf (PostgreSQL), mysql_secure_installation (MySQL), security.authorization: enabled (MongoDB) as the first hardening step per engine.| # | Hardening Domain | PostgreSQL | MySQL | MongoDB |
|---|---|---|---|---|
| 1 | Authentication | scram-sha-256 in pg_hba.conf | caching_sha2_password plugin | SCRAM-SHA-256 (security.authorization: enabled) |
| 2 | Authorization | GRANT/REVOKE + Row-Level Security (RLS) | GRANT/REVOKE + dynamic privileges (8.0+) | Role-Based Access Control (RBAC) with built-in roles |
| 3 | Network binding | listen_addresses = 'localhost' in postgresql.conf | bind-address = 127.0.0.1 in my.cnf | net.bindIp: localhost in mongod.conf |
| 4 | Encryption in transit | ssl = on + hostssl in pg_hba.conf | require_secure_transport = ON | net.tls.mode: requireTLS |
| 5 | Encryption at rest | pgcrypto extension + OS-level disk encryption | InnoDB tablespace encryption / TDE (Enterprise) | Encrypted Storage Engine + Client-Side Field Level Encryption |
| 6 | Audit logging | pgAudit extension (pgaudit.log = 'all') | Audit Log Plugin (Enterprise) or general_log | auditLog.destination: file (Enterprise) |
| 7 | Default cleanup | Remove template DBs access; no trust auth | mysql_secure_installation removes test DB + anon users | Create admin user, disable --noauth |
| 8 | Patching | Minor releases every quarter; subscribe to pgsql-announce | Critical Patch Updates (CPU) quarterly from Oracle | Check MongoDB release notes for security patches |
| 9 | Backup security | pg_dump + GPG encryption | mysqldump + encrypted storage | mongodump + encrypted storage; KMIP key management |
| 10 | OS hardening | Run as postgres user; data dir permissions 700 | Run as mysql user; datadir permissions 750 | Run as mongod user; dbPath permissions 700 |
START: Which database engine?
├── PostgreSQL?
│ ├── YES → Configure pg_hba.conf (scram-sha-256), set ssl=on, create app-specific roles, enable pgAudit
│ └── NO ↓
├── MySQL/MariaDB?
│ ├── YES → Run mysql_secure_installation, set require_secure_transport, create least-privilege users, enable audit log
│ └── NO ↓
├── MongoDB?
│ ├── YES → Enable authorization in mongod.conf, bind to localhost, enable TLS, create RBAC roles
│ └── NO ↓
├── Cloud-managed (RDS/Cloud SQL/Atlas)?
│ ├── YES → Focus on IAM authentication, VPC networking, encryption options, audit logs
│ └── NO ↓
└── DEFAULT → Apply all 7 domains: auth, authz, network, encryption, audit, patching, backup
Replace default or weak authentication methods with strong password hashing. [src1]
# PostgreSQL pg_hba.conf -- require scram-sha-256 over SSL
hostssl myapp_db myapp_user 10.0.1.0/24 scram-sha-256
host all all 0.0.0.0/0 reject
-- MySQL: enforce strong auth plugin
ALTER USER 'app_user'@'10.0.1.%'
IDENTIFIED WITH caching_sha2_password BY 'strong_password';
ALTER USER 'app_user'@'10.0.1.%'
FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LOCK_TIME 1;
# MongoDB mongod.conf -- enable authentication
security:
authorization: enabled
Verify: Attempt to connect without credentials -- connection should be refused.
Create dedicated roles for each application with only the permissions it needs. [src1]
-- PostgreSQL: app role with minimal privileges + RLS
CREATE ROLE myapp_readonly;
GRANT CONNECT ON DATABASE myapp_db TO myapp_readonly;
GRANT USAGE ON SCHEMA public TO myapp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO myapp_readonly;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant')::int);
Verify: SHOW GRANTS; or \du+ -- should show limited privileges.
Bind the database to localhost or a private interface. Use firewall rules to restrict connections. [src6]
# PostgreSQL: listen_addresses = 'localhost,10.0.1.5'
# MySQL: bind-address = 127.0.0.1
# MongoDB: net.bindIp: localhost,10.0.1.5
# Firewall: allow only app server
iptables -A INPUT -p tcp --dport 5432 -s 10.0.1.10 -j ACCEPT
iptables -A INPUT -p tcp --dport 5432 -j DROP
Verify: nmap -p 5432,3306,27017 <db_host> from unauthorized host -- ports should be filtered.
All database connections must use TLS 1.2+ in production. [src3]
# PostgreSQL postgresql.conf
ssl = on
ssl_cert_file = '/etc/ssl/certs/server.crt'
ssl_key_file = '/etc/ssl/private/server.key'
ssl_min_protocol_version = 'TLSv1.2'
# MySQL my.cnf
[mysqld]
require_secure_transport = ON
ssl-ca = /etc/mysql/ssl/ca.pem
ssl-cert = /etc/mysql/ssl/server-cert.pem
ssl-key = /etc/mysql/ssl/server-key.pem
Verify: openssl s_client -connect db_host:5432 -starttls postgres -- should show TLS handshake.
Enable audit logging to track authentication attempts, privilege changes, and data access. [src2]
-- PostgreSQL: install pgAudit
CREATE EXTENSION pgaudit;
-- postgresql.conf: shared_preload_libraries = 'pgaudit'
-- pgaudit.log = 'write, ddl, role'
-- log_connections = on
-- log_disconnections = on
Verify: Perform a login and query, then check the audit log for recorded events.
Encrypt all backups and store encryption keys separately. Maintain a regular patching schedule. [src6]
# PostgreSQL: encrypted backup
pg_dump -U backup_user myapp_db | gzip | gpg --encrypt --recipient [email protected] > backup.sql.gz.gpg
# MySQL: encrypted backup
mysqldump -u backup_user -p myapp_db | gzip | openssl enc -aes-256-cbc -salt -pbkdf2 -out backup.sql.gz.enc
Verify: Restore backup to a test environment quarterly to confirm recoverability.
# /etc/postgresql/17/main/pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
hostssl myapp_db myapp_user 10.0.1.0/24 scram-sha-256
hostssl replication repl_user 10.0.2.0/24 scram-sha-256
hostssl all monitor_user 10.0.3.5/32 scram-sha-256
host all all 0.0.0.0/0 reject
hostnossl all all 0.0.0.0/0 reject
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_read ON orders FOR SELECT
USING (tenant_id = current_setting('app.current_tenant')::int);
CREATE POLICY tenant_write ON orders FOR INSERT
WITH CHECK (tenant_id = current_setting('app.current_tenant')::int);
-- App sets: SET app.current_tenant = '42';
-- Create app user with minimal privileges (MySQL 8.0+)
CREATE USER 'app_svc'@'10.0.1.%'
IDENTIFIED WITH caching_sha2_password BY 'generated_password'
PASSWORD EXPIRE INTERVAL 90 DAY
FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LOCK_TIME 1;
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'app_svc'@'10.0.1.%';
CREATE USER 'app_reader'@'10.0.1.%'
IDENTIFIED WITH caching_sha2_password BY 'another_password'
REQUIRE SSL;
GRANT SELECT ON myapp.* TO 'app_reader'@'10.0.1.%';
# /etc/mongod.conf -- production hardened
storage:
dbPath: /var/lib/mongodb
journal:
enabled: true
net:
port: 27017
bindIp: localhost,10.0.1.5
tls:
mode: requireTLS
certificateKeyFile: /etc/ssl/mongodb.pem
CAFile: /etc/ssl/ca.pem
disabledProtocols: TLS1_0,TLS1_1
security:
authorization: enabled
setParameter:
authenticationMechanisms: SCRAM-SHA-256
# BAD -- trust means no password required
host all all 0.0.0.0/0 trust
# GOOD -- require strong authentication and encryption
hostssl all all 10.0.1.0/24 scram-sha-256
host all all 0.0.0.0/0 reject
-- BAD -- app accounts should never have admin privileges
GRANT ALL PRIVILEGES ON *.* TO 'app_user'@'%' WITH GRANT OPTION;
-- GOOD -- only the permissions the application needs
GRANT SELECT, INSERT, UPDATE ON myapp.* TO 'app_user'@'10.0.1.%';
# BAD -- no security block = auth disabled, open to all interfaces
net:
bindIp: 0.0.0.0
# GOOD -- authentication enabled, network restricted
security:
authorization: enabled
net:
bindIp: localhost,10.0.1.5
tls:
mode: requireTLS
# BAD -- credentials in source code end up in git history
conn = psycopg2.connect(host="db.example.com", password="my_secret_123")
# GOOD -- credentials from environment or vault
import os
conn = psycopg2.connect(host=os.environ["DB_HOST"], password=os.environ["DB_PASSWORD"])
trust auth for local connections; MySQL allows root login without password on some distros; MongoDB runs without auth by default. Fix: Harden immediately after installation. [src1]0.0.0.0 are reachable from any interface. Fix: Bind to localhost or specific private IPs; combine with firewall rules. [src5]sslmode=prefer. Fix: Monitor expiry; use sslmode=verify-full. [src3]SELECT pg_reload_conf(); or systemctl reload postgresql. [src3]SELECT user, File_priv FROM mysql.user;. [src4]# === PostgreSQL ===
# Check authentication settings
psql -U postgres -c "SELECT * FROM pg_hba_file_rules;"
# List roles and privileges
psql -U postgres -c "\du+"
# Check SSL status
psql -U postgres -c "SELECT ssl, version FROM pg_stat_ssl WHERE pid = pg_backend_pid();"
# Check RLS status
psql -U postgres -d myapp -c "SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';"
# === MySQL ===
# Check user privileges and auth plugins
mysql -u root -p -e "SELECT user, host, plugin, ssl_type FROM mysql.user;"
# Check dangerous privileges
mysql -u root -p -e "SELECT user, host, Super_priv, File_priv FROM mysql.user WHERE Super_priv='Y' OR File_priv='Y';"
# === MongoDB ===
# Check if auth is enabled
mongosh --eval "db.adminCommand({getParameter: 1, authenticationMechanisms: 1})"
# List users and roles
mongosh --eval "db.getSiblingDB('admin').system.users.find({}, {user:1, roles:1}).pretty()"
| Engine | Version | Status | Key Security Feature |
|---|---|---|---|
| PostgreSQL 17 | Current (2024) | Active | Incremental backup, enhanced pg_stat_ssl |
| PostgreSQL 16 | Supported | Active | Default scram-sha-256, pg_stat_io |
| PostgreSQL 15 | Supported | Active | Public schema privilege change |
| PostgreSQL 14 | Supported | Active | SCRAM as default in new clusters |
| MySQL 8.4 LTS | Current (2024) | Active | Long-term support, caching_sha2_password |
| MySQL 8.0 | Supported until 2026-04 | Maintenance | Dynamic privileges, caching_sha2_password |
| MongoDB 8.0 | Current (2024) | Active | Queryable Encryption GA |
| MongoDB 7.0 | Supported | Active | Improved audit logging, role management |
| MongoDB 6.0 | Supported | Active | Auth required by default |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Setting up a new self-managed database | Using fully managed cloud service (RDS, Cloud SQL, Atlas) | Cloud provider security docs (IAM auth, VPC, managed encryption) |
| Hardening an existing database with default settings | Addressing application-level SQL injection | SQL Injection Prevention unit |
| Preparing for security audit (SOC 2, PCI-DSS, HIPAA) | Securing application server or web tier | Web server hardening guides |
| Multi-tenant apps requiring row-level data isolation | Single-user desktop databases (SQLite local apps) | SQLite does not support multi-user access control |
FORCE ROW LEVEL SECURITY is set on the table