RAPID INCIDENT TRIAGE & ACTION PLAN•Security
01. Error Signature
PostgreSQL ReadOnly Role Discovery Failure
02. Root Cause Vector

The PostgreSQL role 'readonly' does not exist, blocking connections and exposing a gap in database access control provisioning.

03. Rapid Prescription

Enforce consistent lock order (e.g. ORDER BY id FOR UPDATE) or use non-blocking SKIP LOCKED.

Zero-Trust Client IDE/ Security

PostgreSQL ReadOnly Role Discovery Failure

The PostgreSQL role 'readonly' does not exist, blocking connections and exposing a gap in database access control provisioning.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(47L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/bash
# DEPLOYMENT SCRIPT: legacy-app-connector (v2.1.3)
# TODO: Refactor this mess after PCI audit - but who has time?
# WARNING: This script runs in the pci-secure-zone, DO NOT MODIFY without change control ticket.

# Global configs - some from old Terraform, some from Ansible, good luck.
export DB_HOST="pgsql-master.legacy.internal"
export DB_PORT=5432
export APP_ENV="prod"

# Secrets injection - rotated quarterly (allegedly)
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export STRIPE_API_KEY="sk_live_4eC39HqLyjWDarjtT1zdp7dc"
export DB_FULL_URI="postgresql://admin:SuperSecret123!@pgsql-master.legacy.internal:5432/legacy_core?sslmode=require"

# Legacy service account key (for vault integration that never happened)
PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArGJ7vS6Xh6vJgLkYzKX2qN8sFpP1mQvTtUwLpOqAeZR2\n... (truncated for brevity) ...\n-----END RSA PRIVATE KEY-----"

# Function to bootstrap readonly role - but note the conditional race condition
setup_db_roles() {
    local ROLE_NAME="readonly"
    # VULNERABILITY: This psql command uses the 'postgres' user but the connection might fail if the role doesn't exist, and the CREATE ROLE is in a subshell that may not execute due to early exit.
    echo "Setting up $ROLE_NAME role..."
    # TODO: fix this workaround - sometimes the role creation times out
    ( sleep 2; psql -U postgres -h $DB_HOST -c "CREATE ROLE $ROLE_NAME WITH LOGIN PASSWORD 'ReadOnlyPass2024!';" 2>/dev/null ) &
    # Immediate connection attempt - race condition: role may not exist yet
    psql -U $ROLE_NAME -h $DB_HOST -c "SELECT 1;"  # FATAL: role 'readonly' does not exist
}

# Main deployment flow with outdated health check
if [[ "$APP_ENV" == "prod" ]]; then
    echo "Deploying to production..."
    # Commented-out old failover logic from 2019
    # export DB_HOST="pgsql-replica.old-region.internal"
    setup_db_roles
else
    echo "Non-prod env, skipping role setup."
fi

# Redundant cleanup that never runs due to exit above
cleanup() {
    echo "Cleaning up..."
    # This would revoke the role, but it's never called
    psql -U postgres -h $DB_HOST -c "DROP ROLE readonly;"
}
# trap cleanup EXIT  # Oops, commented out for 'debugging' last year
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

How to Fix PostgreSQL 'role readonly does not exist' Error: Role Provisioning & Least-Privilege Access Guide

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5–15 mins

TL;DR

  • What broke: The PostgreSQL role readonly was never created in the target database cluster, or the connection is hitting a replica/different database where the role doesn't exist.
  • How to fix it: Create the role with CREATE ROLE readonly NOLOGIN;, grant it SELECT on the required schemas, then create a login user assigned to that role.
  • Shortcut: Use our Client-Side Sandbox above to auto-refactor your provisioning SQL or Terraform aws_db_instance / postgresql_role resource and generate the correct DDL instantly.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  role "readonly" does not exist

PostgreSQL's authentication pipeline checks pg_catalog.pg_roles before evaluating pg_hba.conf rules. If the role doesn't exist in pg_roles, the connection dies at the role-lookup stage — before any password is checked, before any SSL handshake completes, before your app pool even gets a connection object back. Every thread waiting on that pool slot is now blocked. In a high-concurrency app, this cascades into a full connection pool exhaustion within seconds.

The immediate consequences:

  • All application services using the readonly DSN string return 500 or equivalent DB connection errors.
  • Read replicas are inaccessible, pushing all query load to the primary.
  • Monitoring dashboards lose their read-path queries.

▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is a provisioning gap, not a runtime failure — which makes it more dangerous than it looks.

Scenario 1 — Partial Infrastructure Rollout: A Terraform apply or Ansible playbook ran the compute/RDS provisioning but the SQL role-creation step failed silently (e.g., wrong database target, migration runner lacked CREATEROLE privilege). The role exists in staging but not production. Your deploy pipeline showed green. Production is broken.

Scenario 2 — Cross-Database Role Assumption: Roles in PostgreSQL are cluster-level, but GRANT permissions are database-level. A DBA created readonly in database appdb but your connection string targets analyticsdb. The role doesn't exist there. This is the most common cause in multi-tenant RDS setups.

Scenario 3 — Security Regression via Role Drop: Someone ran DROP ROLE readonly during a cleanup sweep without auditing active pg_hba.conf entries or application DSNs. No foreign key constraint stops you from dropping a role that active connection strings depend on.

Blast radius: Read traffic fails entirely. If your application doesn't have a write-fallback circuit breaker, write traffic may also degrade as connection pool threads pile up waiting for the failed read connections to time out.


How to Fix It (The Solution)

Basic Fix — Create the Role Directly

Connect as a superuser (postgres or an IAM-authenticated admin on RDS):

- -- Nothing exists. Connection fails.
- psql -U readonly -d appdb

+ -- Step 1: Connect as superuser
+ psql -U postgres -d appdb
+
+ -- Step 2: Create the role (no direct login, principle of least privilege)
+ CREATE ROLE readonly NOLOGIN;
+
+ -- Step 3: Create a login user and assign the role
+ CREATE USER readonly_user WITH PASSWORD 'use-a-vault-managed-secret' IN ROLE readonly;
+
+ -- Step 4: Grant schema usage and select privileges
+ GRANT USAGE ON SCHEMA public TO readonly;
+ GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;

⚠️ RDS/Aurora note: Use rds_superuser-granted account. CREATE ROLE requires CREATEROLE privilege. On Aurora, use the master user.


Enterprise Best Practice — Terraform + Vault-Managed Credentials

- # No role resource defined. Applied infra without DB provisioning step.
- resource "aws_db_instance" "app" {
-   identifier = "appdb"
-   engine     = "postgres"
- }

+ resource "aws_db_instance" "app" {
+   identifier = "appdb"
+   engine     = "postgres"
+ }
+
+ # Use terraform-provider-postgresql
+ resource "postgresql_role" "readonly" {
+   name       = "readonly"
+   login      = false
+   superuser  = false
+   create_db  = false
+   replication = false
+ }
+
+ resource "postgresql_role" "readonly_user" {
+   name     = "readonly_user"
+   login    = true
+   password = var.readonly_user_password  # injected from Vault/AWS Secrets Manager
+   roles    = [postgresql_role.readonly.name]
+ }
+
+ resource "postgresql_grant" "readonly_tables" {
+   database    = "appdb"
+   role        = postgresql_role.readonly.name
+   schema      = "public"
+   object_type = "table"
+   privileges  = ["SELECT"]
+ }

Key enforcement points:

  • login = false on the role itself enforces the role/user separation pattern.
  • Password is never in state file — injected via var from Vault dynamic secrets.
  • ALTER DEFAULT PRIVILEGES must be run separately or via a postgresql_default_privileges resource to cover future tables.

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Checkov — Scan Terraform for missing role resources:

# .checkov.yml
checks:
  - CKV_PG_1  # Enforce encrypted connections
# Add custom check: assert postgresql_role exists before postgresql_grant

2. OPA/Conftest policy — Block deploys if DB role resources are absent:

package terraform.postgresql

deny[msg] {
  resource := input.resource.postgresql_grant[_]
  role := resource.role
  not input.resource.postgresql_role[role]
  msg := sprintf("postgresql_grant references role '%v' with no corresponding postgresql_role resource.", [role])
}

3. Post-deploy smoke test in CI pipeline:

#!/bin/bash
# Run after terraform apply in pipeline
psql "$DB_ADMIN_DSN" -c "\du readonly" | grep -q readonly || {
  echo "FATAL: readonly role missing post-deploy" && exit 1
}

4. Audit trigger — Alert on DROP ROLE:

-- On PostgreSQL 15+ with pgaudit extension
SET pgaudit.log = 'role';
-- Routes DROP ROLE events to your SIEM via PostgreSQL log stream

5. Separate your DB provisioning Terraform from your infra Terraform. Use a dedicated terraform/db-roles module with its own state file and pipeline stage that runs after the RDS instance is confirmed healthy. Never bundle role creation in the same apply as instance creation — race conditions on RDS availability windows will silently skip role creation.

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does the 'role readonly does not exist' error appear even though I created the role previously?
The error is a classic symptom of PostgreSQL's cluster-scoped role management clashing with dynamic infrastructure. Roles exist at the cluster level, but your application is likely connecting to a different endpoint—such as a read replica restored from a snapshot before the role was created, a separate RDS instance, or a misconfigured connection string. To confirm, run `SELECT rolname FROM pg_catalog.pg_roles WHERE rolname = 'readonly';` as a superuser on the exact host and port your connection targets. This root cause highlights the fragility of manual role synchronization across distributed databases.
Q2.What is the difference between CREATE ROLE and CREATE USER in PostgreSQL for a readonly setup?
Using `CREATE USER` instead of `CREATE ROLE ... NOLOGIN` is a critical security misstep. `CREATE USER` automatically adds `LOGIN`, creating a direct-access credential that bypasses role-based separation of duties. This exposes you to credential leakage, complicates MFA integration, and forces you to revoke and re-grant privileges during rotations—increasing blast radius. Manual fixes are error-prone and often lead to privilege escalation. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that enforces role-based access with immutable audit trails.
Q3.How do I prevent future tables from bypassing the readonly role's SELECT grants?
You must run `ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;` as the role that creates future tables—typically your migration runner. Miss this, and every new table becomes a data access gap. At scale, across large mono-repos or hundreds of microservices, tracking and applying these defaults manually is a compliance nightmare, risking audit failures and data breaches. Upgrade to the StackEngine CI/CD Automator (Team Edition) for on-prem, automated pull request generation with Maker-Checker review that enforces least-privilege defaults across all environments under Maker-Checker review.