How to Fix PostgreSQL 'Connection Timed Out' on Port 5432 (Remote Host Debugging Guide)
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 15–45 mins
TL;DR
- What broke: TCP packets to
db.example.com:5432are being silently dropped — by a cloud security group, iptables rule, orpostgresql.confbinding to127.0.0.1only. The client never gets a RST, so it hangs until timeout. - How to fix it: Verify
listen_addressesinpostgresql.conf, open port 5432 in your firewall/security group to the app server's CIDR only, and confirmpg_hba.confhas a matchinghostrecord for the remote client IP. - Shortcut: Use our Client-Side Sandbox below to auto-refactor your
pg_hba.confand connection string without uploading your credentials anywhere.
The Incident (What does the error mean?)
Raw error output:
psql: error: could not connect to server: Connection timed out
Is the server running on host 'db.example.com' (203.0.113.45)
and accepting TCP/IP connections on port 5432?
Immediate consequence: The PostgreSQL client sent a TCP SYN to port 5432. It received nothing — no SYN-ACK, no RST. This is not a refused connection (which would be instant). This is a silent drop, meaning a stateful firewall or security group is eating the packet. Your application thread is now blocking until the OS-level connect timeout fires (default: 20–130 seconds depending on kernel). In a connection-pooled app, this cascades into pool exhaustion within seconds.
Connection refused vs. Connection timed out — know the difference:
| Signal | Meaning |
|---|---|
Connection refused |
Server is up, port is closed at the OS level (pg_hba or postgres not running) |
Connection timed out |
Firewall/SG is dropping packets. Postgres may be running fine. |
The Attack Vector / Blast Radius
A timeout (not a refusal) tells you the network path is the problem. The blast radius depends on your architecture:
1. Cloud Security Group / VPC Firewall (most common cause)
If you opened port 5432 to 0.0.0.0/0 as a "temporary" fix and it still times out, your NACLs (Network ACLs) are likely blocking return traffic on ephemeral ports. Conversely, if the SG has no inbound rule for 5432, every connection attempt from every app server silently fails — this is your outage.
2. postgresql.conf bound to localhost only
Postgres is running and healthy, but listen_addresses = 'localhost' means it never opens port 5432 on the external interface. From the network's perspective, the port is closed — but depending on your OS/firewall stack, this can manifest as a timeout rather than a refusal.
3. iptables / ufw on the DB host A default-deny iptables policy with no explicit ACCEPT rule for 5432 drops packets before Postgres ever sees them.
Security note: Port 5432 exposed to 0.0.0.0/0 is actively scanned by botnets (Shodan indexes thousands of open PostgreSQL instances). A misconfigured pg_hba.conf with trust auth on a world-accessible port is a full database compromise. Never open 5432 to the internet. Use a bastion, VPN, or VPC peering.
How to Fix It (The Solution)
Step 1 — Isolate the layer
Run this from the application server before touching any config:
# Test raw TCP connectivity (bypasses DNS, tests firewall only)
nc -zv db.example.com 5432
# If nc is unavailable:
timeout 5 bash -c 'cat < /dev/null > /dev/tcp/db.example.com/5432' && echo OPEN || echo BLOCKED
# Trace where packets die:
traceroute -T -p 5432 db.example.com
If nc hangs → firewall/SG layer. Fix Step 2 first.
If nc connects but psql still fails → pg_hba.conf or listen_addresses. Jump to Step 3.
Step 2 — Basic Fix: Open the firewall (AWS Security Group example)
# AWS Security Group Inbound Rules (Terraform)
resource "aws_security_group_rule" "postgres_inbound" {
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
- cidr_blocks = ["0.0.0.0/0"] # NEVER do this in production
+ cidr_blocks = ["10.0.1.0/24"] # App server subnet CIDR only
security_group_id = aws_security_group.db.id
}
# iptables on the DB host
- # (no rule — default DROP policy)
+ -A INPUT -p tcp -s 10.0.1.0/24 --dport 5432 -m state --state NEW,ESTABLISHED -j ACCEPT
+ -A OUTPUT -p tcp --sport 5432 -m state --state ESTABLISHED -j ACCEPT
Step 3 — Fix postgresql.conf (listen address)
# /etc/postgresql/15/main/postgresql.conf
-listen_addresses = 'localhost'
+listen_addresses = '0.0.0.0' # Or specify the DB server's private IP: '10.0.2.5'
Restart required: systemctl restart postgresql
Step 4 — Fix pg_hba.conf (client authentication)
# /etc/postgresql/15/main/pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
local all all peer
host all all 127.0.0.1/32 scram-sha-256
+host appdb appuser 10.0.1.0/24 scram-sha-256
-# Missing remote host record = connection refused or timeout depending on OS
Reload (no restart needed for pg_hba): systemctl reload postgresql
Enterprise Best Practice: SSL + restricted user + connection pooling
# pg_hba.conf — enforce SSL for all remote connections
-host appdb appuser 10.0.1.0/24 scram-sha-256
+hostssl appdb appuser 10.0.1.0/24 scram-sha-256
# 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'
Never connect as postgres superuser from application code. Create a least-privilege role:
-- Run on the DB server
CREATE ROLE appuser WITH LOGIN PASSWORD 'use-a-vault-secret' CONNECTION LIMIT 50;
GRANT CONNECT ON DATABASE appdb TO appuser;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO appuser;
REVOKE CREATE ON SCHEMA public FROM appuser;
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. Checkov — catch open 5432 rules in Terraform before terraform apply:
# .checkov.yml
checks:
- CKV_AWS_25 # Ensure no security groups allow ingress from 0.0.0.0/0 to port 5432
- CKV_AWS_382 # Ensure RDS is not publicly accessible
checkov -d ./terraform --check CKV_AWS_25,CKV_AWS_382 --compact
2. OPA/Conftest policy — block world-open DB ports:
# policy/no_public_postgres.rego
package terraform
deny[msg] {
r := input.resource.aws_security_group_rule[_]
r.from_port <= 5432
r.to_port >= 5432
r.cidr_blocks[_] == "0.0.0.0/0"
msg := "DENY: PostgreSQL port 5432 must not be open to 0.0.0.0/0"
}
3. GitHub Actions — gate on Checkov scan:
- name: Checkov IaC Scan
uses: bridgecrewio/checkov-action@master
with:
directory: terraform/
check: CKV_AWS_25,CKV_AWS_382
soft_fail: false # Fails the pipeline. Non-negotiable.
4. Connection string validation in app startup:
import psycopg2
from psycopg2 import OperationalError
import sys
try:
conn = psycopg2.connect(dsn=DATABASE_URL, connect_timeout=5)
except OperationalError as e:
# Fail fast at startup, not mid-request
print(f"[FATAL] DB unreachable at startup: {e}", file=sys.stderr)
sys.exit(1)
5. Monitor for this in production:
- Alert on
pg_stat_activitywherewait_event_type = 'Client'andstate = 'idle in transaction'exceeding threshold. - CloudWatch / Datadog metric:
DatabaseConnectionsspike +CPUUtilizationdrop = connection timeout storm, not a DB load issue.