Most PostgreSQL compromises come from configuration rather than code: a permissive pg_hba.conf line, an application connecting as a superuser, a schema anyone can write to, or a table that relies on row level security nobody switched on. PostgreSQL ships with sensible defaults in recent releases, but upgraded clusters, container images and managed services often carry older or looser settings forward. This guide goes engine deep on PostgreSQL 14 to 18: the role model, client authentication, transport encryption, schema and function safety, row level security, audit logging and patching. It is written for DBAs, platform engineers and security engineers who own self-managed or managed PostgreSQL. For engine-neutral principles such as the database threat model and database activity monitoring, see the Database Security Guide.
Key takeaways
- pg_hba.conf is the front door. Every line is a rule matched top to bottom; one stray
trustor0.0.0.0/0entry overrides all your careful password policy. - SCRAM, not MD5.
password_encryptiondefaults toscram-sha-256, and the PostgreSQL 18 documentation states that MD5 password support is deprecated and will be removed, so migrate stored hashes now. - Superuser is not an application role. Applications should connect as a login role with no ownership of the objects they use, and dangerous predefined roles such as
pg_execute_server_programshould be granted to nobody routinely. - Schemas and search_path are a privilege boundary. Clusters upgraded from before PostgreSQL 15 may still let every role create objects in
public, which enables trojan objects and function hijacking. - Row level security must be enabled per table. Policies do nothing until
ENABLE ROW LEVEL SECURITYis set, and owners and roles withBYPASSRLSskip them unless you force them.
The role model: logins, groups, ownership and predefined roles
PostgreSQL has one concept, the role, which acts as a user when it has the LOGIN attribute and as a group when other roles are members of it. Security depends on four things: role attributes, object ownership, granted privileges and membership in predefined roles.
- Attributes:
SUPERUSERbypasses every permission check,CREATEROLElets a role create and manage other roles,CREATEDBcreates databases,REPLICATIONallows streaming replication connections andBYPASSRLSskips row level security. Inventory them withSELECT rolname FROM pg_roles WHERE rolsuper OR rolcreaterole OR rolbypassrls OR rolreplication;. - Ownership: the owner of a table can alter, drop and grant on it and is exempt from its row level security by default. An application that owns its schema can therefore drop its own tables.
- Privileges: grant
SELECT,INSERT,UPDATE,DELETEandUSAGEon schemas explicitly to group roles, then add login roles as members. UseALTER DEFAULT PRIVILEGESso tables created by the migration role are automatically readable by the application group. - Predefined roles: PostgreSQL ships built-in roles that grant broad capability without superuser. Some are safe for monitoring, others are equivalent to operating system access.
| Predefined role | What it grants | Risk and guidance |
|---|---|---|
pg_monitor | Read access to statistics and settings views for monitoring | Low. Grant to the monitoring agent instead of superuser. |
pg_read_all_data / pg_write_all_data | Read or write every table, view and sequence regardless of grants | High. Equivalent to a full data dump or tamper right. Reserve for break-glass and backup roles. |
pg_read_server_files / pg_write_server_files | Read or write files on the database host with COPY and file functions | Very high. Can expose configuration, keys and other databases' files. |
pg_execute_server_program | Run programs on the host as the database OS user via COPY ... PROGRAM | Critical. Effectively operating system command execution. Grant to nobody in normal operation. |
pg_maintain | Run VACUUM, ANALYZE, REINDEX and similar maintenance on all relations | Low to medium. A safer alternative to superuser for maintenance jobs (PostgreSQL 17 and later). |
A practical layout for an application database uses four roles: an owner role with NOLOGIN that owns the schema, a migration login that is a member of the owner role and is used only by the deployment pipeline, an application login granted DML only through a group role, and a read-only reporting login. See the Database Credential and Access Security Guide for how these service credentials are vaulted and rotated.
Client authentication with pg_hba.conf
PostgreSQL decides how a client authenticates by reading pg_hba.conf from top to bottom and using the first line whose connection type, database, user and address match. There is no fall-through: if the first match uses trust, no password is checked. If no line matches, the connection is refused.
| Method | How it works | When to use it |
|---|---|---|
trust | Accepts the connection with no authentication | Never on TCP. The documentation notes that even local trust is only appropriate where every OS user on the host is trusted. |
peer | Uses the kernel-reported OS user name on Unix sockets | Local administration as the postgres OS user; keeps the superuser off the network. |
scram-sha-256 | Salted challenge response; the password is never sent in clear and the server stores a verifier, not the password | Default for password-based application and human logins. |
md5 | Legacy hashed password challenge; deprecated | Migration only. A md5 line will still use SCRAM if the stored password is a SCRAM verifier. |
cert | Client TLS certificate, with the certificate CN mapped to the role | Service-to-service connections where you run a private CA. See the mTLS Guide. |
ldap / radius | Password checked against an external directory | Human logins where central disable matters; the password crosses to the server, so require TLS. |
oauth | OAuth 2.0 bearer tokens from an external identity provider, validated by a configured validator module | New in PostgreSQL 18; suits single sign-on for humans and tools. Validate maturity of your validator before production use. |
reject | Explicitly refuses matching connections | Place above broad rules to carve out exceptions, for example rejecting the postgres role over TCP. |
A hardened file follows a simple pattern: local socket lines with peer for the administrator, explicit reject lines for superuser and replication roles from the network, then hostssl lines (not host) naming each database, each role or group (prefix a group with +) and the narrowest CIDR, with scram-sha-256 or cert. Add clientcert=verify-full on password lines where you also want certificate binding. Finish with no catch-all all all 0.0.0.0/0 line. Set listen_addresses to the specific private interface rather than *, keep port 5432 behind a firewall or security group. Internet scanners index exposed database ports continuously, so assume an open listener will be found.
Passwords and SCRAM migration
- Find legacy hashes:
SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%';(superuser only). - Re-set them: with
password_encryption = 'scram-sha-256', reset each password (frompsqluse\password, which hashes client side so the cleartext never reaches the server log). - Then switch rules: change
md5lines toscram-sha-256once every client driver supports SCRAM, which current libpq, JDBC, Npgsql and psycopg releases do. - Never put passwords in SQL logs:
CREATE ROLE ... PASSWORD 'x'can appear in server logs and inpg_stat_statements; use client-side hashing or a secrets manager that issues dynamic credentials. See the Secrets Management Guide.
Encryption in transit and at rest
Set ssl = on with a certificate from your internal CA and keep ssl_min_protocol_version at its default of TLSv1.2 or raise it to TLSv1.3 once clients support it. Server-side settings are only half of the control: libpq clients default to sslmode=prefer, which silently falls back to clear text and does not check the server identity. Require sslmode=verify-full with sslrootcert in every application connection string, and use hostssl in pg_hba.conf so the server refuses unencrypted sessions. See the How TLS Works Guide.
Community PostgreSQL does not include transparent data encryption. At rest, the options are volume or disk encryption (LUKS, cloud provider volume encryption), column-level encryption with the pgcrypto extension or application-side encryption, and encrypted backups. Volume encryption protects against stolen disks and snapshots but not against anyone who can log in. Column encryption with keys held outside the database protects specific fields from DBAs and from a dumped table, at the cost of indexing and querying on those columns. Treat WAL archives, base backups and logical dumps as copies of the full dataset: encrypt them and store their keys separately from the backup repository. See the Encryption Guide.
Schemas, search_path and function safety
PostgreSQL resolves unqualified names through search_path. If an untrusted role can create objects in a schema that appears in another role's search path, it can plant a function or operator with the same name as one the victim calls and have it run with the victim's privileges. The PostgreSQL documentation describes secure schema usage patterns and notes that in PostgreSQL 15 and later the default no longer grants CREATE on public to everyone, but databases upgraded from earlier versions keep the old grant.
- Fix upgraded databases: run
REVOKE CREATE ON SCHEMA public FROM PUBLIC;in every database and checktemplate1so new databases inherit the fix. Then look for objects inpublicnamed like objects inpg_catalog. - Database owners are trusted: the documentation is explicit that no secure pattern exists if an untrusted user owns the database or holds
ADMIN OPTIONon a relevant role. Do not hand database ownership to tenants or applications. - SECURITY DEFINER functions: these run as their owner, so they are privilege escalation points. Give each one a fixed
SET search_path = pg_catalog, pg_temp(or a trusted schema), schema-qualify every reference, revokeEXECUTEfromPUBLICand grant it only to the roles that need it. - Extensions and languages: only trusted languages (
plpgsql,sql) should be available to ordinary roles. Untrusted languages such asplpython3urun with database server OS rights and can only be used by superusers; leave them uninstalled unless there is a reviewed need. - COPY and file access:
COPY ... TO/FROMa server file andCOPY ... PROGRAMare limited to superusers and the file or program predefined roles. Persistence here maps to ATT&CK T1505.001.
Row level security and multi-tenant data
Row level security (RLS) adds per-row predicates to every query on a table. It is the usual way to isolate tenants in a shared schema and underpins platforms such as Supabase that expose PostgreSQL to browser clients. Three rules decide whether it actually protects anything.
- Enable it on every exposed table:
ALTER TABLE t ENABLE ROW LEVEL SECURITY;. Policies created on a table where RLS is not enabled are ignored. When RLS is enabled and no policy applies, access is default deny. - Force it where the owner connects: table owners bypass RLS unless you add
ALTER TABLE t FORCE ROW LEVEL SECURITY;. Superusers and roles withBYPASSRLSalways bypass it, so the application must never connect as either. - Write both USING and WITH CHECK:
USINGfilters rows that can be read or targeted;WITH CHECKstops a user inserting or updating rows into another tenant. Base the predicate on a value the client cannot forge, such as a role name or a setting applied by a trusted pooler, not a column the client supplies.
The Moltbook database exposure shows the failure mode: a Supabase key embedded in public JavaScript was intended to be safe, but Row Level Security had not been configured, so the key gave unauthenticated read and write access to every table, including agent API keys. Add a pipeline check that fails a deployment when any table in an exposed schema has relrowsecurity = false in pg_class.
Audit logging and detection
Native logging covers connections and statements; the open source pgaudit extension adds structured session and object audit records. pgaudit must be listed in shared_preload_libraries, and its pgaudit.log setting selects statement classes such as read, write, ddl, role and misc. Object-level audit is configured by granting privileges to the role named in pgaudit.role, which gives finer detail on sensitive tables than logging every read.
| Setting | Recommended value | What it gives you |
|---|---|---|
log_connections | PostgreSQL 18: receipt,authentication,authorization; earlier: on | Who connected, from where, to which database, with which authenticated identity. Failed authentication is always logged. |
log_disconnections | on | Session duration, useful for spotting long-lived interactive sessions on service accounts. |
log_line_prefix | Include %m %p %u@%d %r %a | Timestamp, process, user, database, remote host and application name on every line. |
log_statement | ddl | Schema changes and role changes without logging every query. |
pgaudit.log | role, ddl cluster wide; add read, write for sensitive databases or roles | Privilege grants, role creation and object changes in a parseable format. |
log_destination | jsonlog or csvlog, shipped off host | Structured events for the SIEM; local logs can be altered by a superuser. |
Alert on: failed authentication bursts against one role (ATT&CK T1110 Brute Force); successful logins by superuser or replication roles from unexpected addresses (T1078 Valid Accounts); ALTER ROLE ... SUPERUSER, GRANT pg_execute_server_program or CREATE EXTENSION outside change windows; COPY ... PROGRAM or COPY TO a file; bulk reads by an application role far above its baseline (T1213.006 Databases collection); and DROP DATABASE or mass TRUNCATE (T1485 Data Destruction). Ship logs off host as described in the Linux Logging and auditd for Defenders Guide.
Patching, versions and managed services
The PostgreSQL project supports each major version for five years and ships minor releases at least every three months, with out-of-cycle releases for critical fixes. At the time of writing, versions 14 to 18 are supported; PostgreSQL 14 reaches its final release on 12 November 2026 and 13 is already end of life. The project recommends always running the current minor release because minor upgrades are lower risk than staying behind. Major upgrades need pg_upgrade or a dump and reload, so plan them well ahead of end of life. Track the CISA KEV catalogue for PostgreSQL, extensions and poolers. See the Patch Management Guide.
On managed services (Amazon RDS and Aurora, Azure Database for PostgreSQL, Google Cloud SQL), the provider patches the engine and controls the host, and you do not get a true superuser. You still own pg_hba-equivalent network rules, TLS enforcement parameters, roles and grants, RLS, extension choices and log export. Prefer the provider's IAM database authentication where available, because it replaces stored passwords with short-lived tokens tied to cloud identities. See the Cloud Logging and Audit Trail Guide.
Where identity fits: service roles as non-human identities
In most estates the majority of PostgreSQL logins belong to applications, ETL jobs, pollers, backup agents and increasingly AI agents, not people. Each is a non-human identity with a credential that is often long-lived and copied into configuration files (ATT&CK T1552.001 Credentials In Files). Give each workload its own role per environment, store its secret in a vault or replace it with certificate or IAM token authentication, set CONNECTION LIMIT to its expected pool size, and record an owner. The PocketOS database deletion shows what happens when an automated actor holds a credential that can destroy production. See the OWASP NHI Top 10 NHI5 Overprivileged NHI Guide.
Implementation roadmap
- Inventory (week 1): list every cluster, version, listen address and pg_hba.conf. Export
pg_rolesattributes, predefined role memberships and database owners. Flag anything below PostgreSQL 15 or on an end-of-life version. - Close exposure (weeks 1 to 2): remove internet reachability, replace
hostwithhostssl, deletetrustlines and catch-all CIDRs. Test on a staging copy, keep the previous file and roll back with a reload if a client breaks. - Turn on logging first (week 2): enable connection logging and pgaudit
role, ddl, ship to the SIEM and build a two-week baseline before changing privileges, so you can see which roles are actually used. - Fix authentication (weeks 3 to 6): migrate MD5 hashes to SCRAM, require
sslmode=verify-fullin clients, move service passwords into a vault or IAM authentication. - Reduce privilege (weeks 4 to 10): introduce owner, migration, application and read-only roles; remove superuser and ownership from application logins; revoke
CREATEonpublic; reviewSECURITY DEFINERfunctions. Roll out per application in pilot rings, with the old grant scripted for quick reinstatement. - Protect data (ongoing): enable and force RLS on multi-tenant tables with automated tests, encrypt backups and sensitive columns, and test restores quarterly.
- Measure and baseline: apply a CIS PostgreSQL Benchmark profile through configuration management and alert on drift.
Choosing your approach: small versus large estates
A small team running a handful of databases gets most of the benefit from a managed service with IAM authentication, enforced TLS, the four-role pattern and pgaudit on role, ddl. Large estates with hundreds of clusters need the controls expressed as code: pg_hba.conf and parameter files templated by configuration management, roles created by a provisioning pipeline, dynamic credentials from a secrets manager, and continuous checks against the CIS benchmark. Choose certificate authentication where you run a PKI, otherwise SCRAM with vaulted rotation.
Metrics that show it is working
- Exposure: number of clusters reachable from the internet or from user networks (target: zero).
- Authentication: percentage of roles with SCRAM verifiers and percentage of sessions in
pg_stat_sslusing TLS (target: 100 percent). - Privilege: count of login roles with superuser,
BYPASSRLSor dangerous predefined roles, trending down to named break-glass accounts only. - Data isolation: exposed tables without RLS enabled (target: zero, enforced in CI).
- Patch latency: days from a PostgreSQL minor release to deployment across the estate.
- Recovery: successful restore tests per quarter and measured restore time.
Framework mapping
| Control area | NIST SP 800-53 Rev 5 | NIST CSF 2.0 | CIS Controls v8.1 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|---|
| Roles, grants, predefined roles | AC-2, AC-6 | PR.AA-05 | 5.4, 6.8 | A.5.15, A.8.2 |
| pg_hba.conf, SCRAM, certificates | IA-5 | PR.AA-05 | 4.7, 5.2 | A.8.5 |
| TLS and backup encryption | SC-8, SC-28, SC-12 | PR.DS-02, PR.DS-01 | 3.10, 3.11 | A.8.24 |
| Configuration baseline, schema hardening | CM-6, CM-7 | PR.PS-01 | 4.1 | A.8.9 |
| Logging and pgaudit | AU-2, AU-12 | PR.PS-04, DE.CM-09 | 8.2 | A.8.15 |
| Patching | SI-2, RA-5 | ID.RA-01 | 7.3 | A.8.8 |
Common failure patterns
- Catch-all pg_hba lines:
host all all 0.0.0.0/0 md5added during troubleshooting and never removed, often combined withlisten_addresses = '*'in container images. - The application as superuser: one
postgrespassword shared by the application, the migration tool and humans, which turns any SQL injection into host command execution throughCOPY ... PROGRAM. See the SQL Injection Prevention Guide. - RLS written but not enabled: policies exist in migrations but the
ENABLEorFORCEstatement is missing, or the API connects as the owner, as in the Moltbook exposure above. - Upgraded public schema: clusters taken from PostgreSQL 13 or earlier with
pg_upgradestill grantCREATEonpublicto all roles. - Client TLS on prefer: the server has a certificate, but clients use the default
sslmodeand never verify it, so a network attacker can downgrade or intercept.
Practitioner checklist
- pg_hba.conf has no
truston TCP, no catch-all CIDR, useshostssland rejects superuser and replication roles from the network. password_encryption = scram-sha-256and nomd5verifiers remain inpg_authid.- Every client connection string uses
sslmode=verify-full;pg_stat_sslshows TLS on all sessions. - Application logins own nothing, hold no superuser,
CREATEROLEorBYPASSRLS, and have a per-roleCONNECTION LIMIT. - Nobody routinely holds
pg_execute_server_program,pg_read_server_files,pg_write_server_filesorpg_write_all_data. REVOKE CREATE ON SCHEMA public FROM PUBLICapplied in every upgraded database and intemplate1.- All
SECURITY DEFINERfunctions set a fixedsearch_pathand haveEXECUTErevoked fromPUBLIC. - RLS enabled and forced on every multi-tenant or API-exposed table, with a CI test.
- pgaudit loaded with at least
role, ddl; connection logging on; logs shipped off host with alerts defined. - Every cluster on a supported major version and the latest minor release; backups encrypted and restore-tested.
Standards and references
- PostgreSQL: Client Authentication (pg_hba.conf)
- PostgreSQL: Predefined Roles
- PostgreSQL: Schemas and Secure Schema Usage Patterns
- PostgreSQL: Row Security Policies
- PostgreSQL: Error Reporting and Logging
- PostgreSQL: Versioning Policy
- pgAudit: PostgreSQL Audit Extension
- NIST: SP 800-53 Rev. 5 Security and Privacy Controls
Related NHI Mgmt Group resources: Database Security Guide · Database Credential and Access Security Guide · MySQL Security Guide · Linux Server Hardening Guide · Secrets Management Guide