Fixing PostgreSQL SCRAM-SHA-256 Auth Failure: password authentication failed for user with pg_hba.conf md5
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 5–15 mins
TL;DR
- What broke: PostgreSQL 14+ defaults
password_encryptiontoscram-sha-256, but yourpg_hba.confstill specifiesmd5— or vice versa — causing a handshake mismatch that hard-blocks every login forappuser. - How to fix it: Align
pg_hba.confauth method withpassword_encryption, then re-hash the password (ALTER USER appuser WITH PASSWORD '...') so the stored verifier matches the negotiated method. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your
pg_hba.confand get a corrected diff without sending secrets to any server.
The Incident (What Does the Error Mean?)
Raw error from PostgreSQL logs / client:
FATAL: password authentication failed for user "appuser"
DETAIL: Connection matched pg_hba.conf line 87: "host all appuser 10.0.0.0/8 md5"
Or the inverse — client library error:
ERROR: auth method "scram-sha-256" requires libpq >= 10
FATAL: password authentication failed for user "appuser"
What is actually failing: PostgreSQL's authentication handshake is a two-part contract. The server advertises an auth method (from pg_hba.conf). The stored password verifier in pg_authid must have been hashed with a compatible algorithm at CREATE USER / ALTER USER time. If these two are misaligned — or if the client library is too old to speak SCRAM — the handshake aborts before a single query runs. Every connection attempt fails. Your app is down.
The Attack Vector / Blast Radius
This is not just an inconvenience — it is a security regression in disguise.
Why MD5 is the problem, not the fix:
MD5 password hashing for PostgreSQL auth (RFC: md5 in pg_hba.conf) stores a salted MD5 hash of password + username. MD5 is cryptographically broken. A leaked pg_authid table — via a SQL injection COPY TO attack, a misconfigured pg_dump, or a compromised replica — exposes password hashes that can be cracked offline with commodity hardware in minutes using hashcat.
SCRAM-SHA-256 (RFC 5802) is the mandated replacement because:
- The verifier stored in
pg_authidis not directly usable for replay attacks. - The protocol performs mutual authentication — the client proves it knows the password without transmitting it in a recoverable form.
- Channel binding (
scram-sha-256-plus) ties the auth to the TLS session, defeating MITM.
Blast radius of leaving MD5 in place:
- Leaked replica or backup → offline crack of all MD5-hashed passwords.
- If
appuserpassword is reused elsewhere (common in legacy apps) → lateral movement. - PCI-DSS, SOC 2, and HIPAA auditors will flag MD5 auth as a finding.
Blast radius of the mismatch itself:
- 100% connection failure for the affected user/host combination.
- If this is a connection pool (PgBouncer, RDS Proxy), the pool exhausts retries and cascades into app-layer 500s within seconds.
How to Fix It
Step 1 — Diagnose the stored hash type
-- Run as superuser
SELECT rolname, rolpassword FROM pg_authid WHERE rolname = 'appuser';
- Starts with
md5→ password was hashed with MD5. Must be re-set. - Starts with
SCRAM-SHA-256$→ hash is correct; the problem ispg_hba.conf. NULL→ no password set at all.
Step 2 — Check server default
SHOW password_encryption;
-- Should return: scram-sha-256
Basic Fix — Align pg_hba.conf and re-hash
pg_hba.conf correction:
# /etc/postgresql/15/main/pg_hba.conf
- host all appuser 10.0.0.0/8 md5
+ host all appuser 10.0.0.0/8 scram-sha-256
Force re-hash of the stored password (critical — do not skip):
- -- Password verifier currently stored as MD5 in pg_authid
- -- ALTER USER appuser WITH PASSWORD 'yourpassword'; ← run when password_encryption=md5
+ -- Ensure server will hash with SCRAM before running ALTER USER
+ SET password_encryption = 'scram-sha-256';
+ ALTER USER appuser WITH PASSWORD 'yourpassword';
Then reload (no restart needed for pg_hba.conf):
sudo systemctl reload postgresql
# or in psql:
SELECT pg_reload_conf();
Enterprise Best Practice — Enforce at the cluster level + secret rotation
postgresql.conf — lock the default:
- #password_encryption = md5
+ password_encryption = scram-sha-256
Use channel binding if TLS is enforced (highest security):
- host all appuser 10.0.0.0/8 scram-sha-256
+ hostssl all appuser 10.0.0.0/8 scram-sha-256
Rotate via Vault / AWS Secrets Manager (never hardcode):
- # App reads password from environment variable set in Dockerfile
- ENV DB_PASSWORD=plaintextpassword
+ # App reads password from Secrets Manager at runtime
+ # aws secretsmanager get-secret-value --secret-id prod/appuser/db
+ # Inject via ECS task role — no static secret in image
Verify the fix:
psql "host=yourhost dbname=yourdb user=appuser sslmode=require" -c "SELECT current_user;"
# Expected: 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 — scan Terraform RDS / PostgreSQL modules:
# .checkov.yml
checks:
- CKV_AWS_129 # RDS: ensure IAM auth enabled (supplements password auth)
- CKV2_AWS_60 # RDS: enforce SSL
# Add custom check: flag any pg_hba.conf committed to repo with 'md5'
2. OPA/Conftest policy — block md5 in pg_hba.conf committed to IaC:
package postgresql
deny[msg] {
line := input.pg_hba_lines[_]
contains(line, "md5")
msg := sprintf("pg_hba.conf line uses deprecated md5 auth: %v", [line])
}
3. GitHub Actions — validate pg_hba.conf on PR:
- name: Reject md5 auth in pg_hba.conf
run: |
if grep -rE '\bmd5\b' ./postgres/pg_hba.conf; then
echo "ERROR: md5 auth method detected. Use scram-sha-256."
exit 1
fi
4. Terraform aws_db_parameter_group — enforce password_encryption:
resource "aws_db_parameter_group" "postgres" {
family = "postgres15"
+ parameter {
+ name = "password_encryption"
+ value = "scram-sha-256"
+ apply_method = "immediate"
+ }
}
5. Monitor for auth failures in production:
# CloudWatch Logs Insights (RDS) or grep for on-prem
filter @message like /password authentication failed/
| stats count(*) by bin(5m)
# Alert if > 5 failures/min — may indicate credential stuffing or misconfigured deploy