Role-based access control

Stop connecting your application to PostgreSQL as the postgres superuser. This is the database equivalent of running your web server as root. If your application is compromised through SQL injection or a server-side vulnerability, the attacker inherits superuser privileges — they can read every table, drop databases, create new users, and install extensions.

Create dedicated roles with minimal privileges:

-- Application role: only the permissions it needs
CREATE ROLE app_user WITH LOGIN PASSWORD 'strong-random-password';
GRANT CONNECT ON DATABASE fintech_db TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO app_user;
-- No DELETE, no DROP, no CREATE TABLE

-- Migration role: used only during deployments
CREATE ROLE migration_user WITH LOGIN PASSWORD 'different-strong-password';
GRANT ALL ON SCHEMA public TO migration_user;

-- Read-only role: for dashboards and reporting
CREATE ROLE readonly_user WITH LOGIN PASSWORD 'another-strong-password';
GRANT CONNECT ON DATABASE fintech_db TO readonly_user;
GRANT USAGE ON SCHEMA public TO readonly_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;

Your application uses app_user, your deployment pipeline uses migration_user, and your analytics dashboard uses readonly_user. If any one is compromised, the blast radius is limited to its specific permissions. Store each credential separately in your secrets manager.

SSL/TLS connections

By default, PostgreSQL connections are unencrypted. Database credentials and query results travel in plaintext across the network. On shared hosting, within VPCs with multiple tenants, or between cloud regions, this is a data exfiltration risk.

Enable SSL in postgresql.conf:

# postgresql.conf
ssl = on
ssl_cert_file = '/etc/ssl/certs/server.crt'
ssl_key_file = '/etc/ssl/private/server.key'
ssl_ca_file = '/etc/ssl/certs/ca.crt'

Then enforce it in pg_hba.conf by using hostssl instead of host for all remote connections. On the client side, set sslmode=verify-full in your connection string to verify the server's certificate. Managed databases (AWS RDS, Google Cloud SQL, Supabase) provide SSL certificates by default — but you still need to configure your application to require them.

pg_hba.conf: the access control file

pg_hba.conf controls who can connect, from where, to which database, and how they authenticate. A misconfigured pg_hba.conf is one of the most critical PostgreSQL vulnerabilities:

# DANGEROUS — trust all local connections (no password required)
local   all   all   trust

# DANGEROUS — allow any IP to connect
host    all   all   0.0.0.0/0   md5

# CORRECT — specific IPs, SSL required, scram-sha-256 authentication
hostssl fintech_db  app_user      10.0.1.0/24   scram-sha-256
hostssl fintech_db  readonly_user 10.0.2.0/24   scram-sha-256
hostssl fintech_db  migration_user 10.0.0.5/32  scram-sha-256

Use scram-sha-256 instead of md5 for password authentication — MD5 hashing is cryptographically weak. Restrict IP ranges to your application servers only. Never expose PostgreSQL on 0.0.0.0 or allow connections from the public internet.

Pentest finding

PostgreSQL exposed to public internet

During an infrastructure assessment for a Nigerian payment gateway, we discovered their PostgreSQL instance listening on port 5432 with 0.0.0.0/0 in pg_hba.conf. The database accepted connections from any IP using MD5 authentication. Combined with a weak password, we had full read/write access to production transaction data within minutes.

Row-level security (RLS)

Row-level security adds a second layer of access control inside the database itself. Even if your application has a bug that queries the wrong user's data, RLS prevents the database from returning it:

-- Enable RLS on the transactions table
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;

-- Policy: users can only see their own transactions
CREATE POLICY user_transactions ON transactions
  FOR SELECT
  USING (user_id = current_setting('app.current_user_id')::uuid);

-- Set the user ID in the session before queries
SET app.current_user_id = 'user-uuid-here';

RLS is particularly valuable for fintech multi-tenant architectures where multiple organisations share the same database. It prevents BOLA-style data leakage at the database layer, complementing your application-level authorization checks. If your application's object-level checks fail, RLS is the safety net.

Connection pooling security (PgBouncer)

PgBouncer sits between your application and PostgreSQL, managing connection pools for performance. But it introduces its own security considerations:

Backup encryption

Database backups contain everything: user data, credentials, transaction history. An unencrypted backup stored on S3, a shared drive, or a developer's laptop is a full data breach waiting to happen.

Encrypt backups at rest using pg_dump piped to GPG or a cloud-native encryption solution:

# Encrypted backup
pg_dump fintech_db | gpg --cipher-algo AES256 --symmetric \
  --output /backups/fintech_db_$(date +%Y%m%d).sql.gpg

# Verify the backup can be restored
gpg --decrypt /backups/fintech_db_20260601.sql.gpg | psql fintech_db_test

For managed databases, enable server-side encryption on your storage (S3 SSE, GCS encryption). Store backup encryption keys separately from the backups — a backup and its decryption key in the same location provides zero protection. This is a PCI DSS requirement for any fintech handling cardholder data.

Concerned about your PostgreSQL security posture? We audit database configurations, access controls, and encryption as part of our infrastructure assessments.

Request an infrastructure security review

Audit logging

PostgreSQL's default logging captures errors but not data access patterns. For fintech compliance (CBN requirements, NDPA/NDPR), you need audit trails that record who accessed what data and when.

Enable the pgAudit extension for granular audit logging:

# postgresql.conf
shared_preload_libraries = 'pgaudit'
pgaudit.log = 'read, write, ddl'
pgaudit.log_parameter = on

This logs all SELECT, INSERT, UPDATE, DELETE operations, and schema changes with the actual parameter values. Ship these logs to a centralised logging system (ELK, Loki, CloudWatch) where they can't be tampered with by someone who compromises the database server.

Extension security

PostgreSQL extensions run with the privileges of the installing user — often the superuser. A malicious or vulnerable extension has full access to your data. Only install extensions from trusted sources. Audit installed extensions with SELECT * FROM pg_extension; and remove anything unused.

Be particularly careful with extensions that provide external access (postgres_fdw, dblink) or filesystem access (adminpack). In a fintech context, limit extensions to what your application explicitly requires: uuid-ossp, pgcrypto, pgAudit, and your application-specific needs. Restrict CREATE EXTENSION privileges to the superuser role only.

Key takeaway

Database security is infrastructure security

Your PostgreSQL instance contains everything attackers want: user credentials, financial records, PII. Securing it requires role-based access with least privilege, encrypted connections, strict pg_hba.conf rules, row-level security for multi-tenant isolation, encrypted backups, and audit logging. These aren't advanced features — they're baseline requirements for any fintech running in production.

Related reading

Blog: Cloud security checklist · Where to store API keys · Zero trust architecture

Guides: PCI DSS compliance · CBN compliance · ISO 27001 for fintech

Services: Penetration testing · Secure architecture review · Vulnerability assessment