Now-Next

SaaS architecture

password authentication failed for user: causes, measured

PostgreSQL's password authentication failed for user, measured on 17: what the server log says, the Docker volume trap, md5 vs SCRAM, URL encoding and .pgpass.

Chris van Eijk, Now-Next · · 7 min read

FATAL: password authentication failed for user "postgres" (SQLSTATE 28P01) means the server rejected the login at the authentication step. The client gets the same sentence whether the password was wrong, the role does not exist, or the role has no usable password at all; the real reason is only in the server log. In a Docker setup, the cause we could reproduce most directly is a data volume that was initialised with an older POSTGRES_PASSWORD.

We measured on the official images postgres:17 (17.11) and postgres:13 (13.23), with one check on postgres:18 (18.6), connecting from psql/libpq 16.15 on the host through a published port.

Why does a missing user also say “password authentication failed”?

Because the server does not tell an unauthenticated client which part was wrong. We tried four logins against PostgreSQL 17: a wrong password for postgres, a role ghost that does not exist, a role nopw created without a password, and a wrong password for an existing role app. The client saw the same error every time, only with a different name:

psql: error: connection to server at "127.0.0.1", port 55505 failed: FATAL:  password authentication failed for user "ghost"

The server log told three of them apart; the two wrong passwords looked the same:

FATAL:  password authentication failed for user "ghost"
DETAIL:  Role "ghost" does not exist.
	Connection matched file "/var/lib/postgresql/data/pg_hba.conf" line 128: "host all all all scram-sha-256"
FATAL:  password authentication failed for user "nopw"
DETAIL:  User "nopw" has no password assigned.
	Connection matched file "/var/lib/postgresql/data/pg_hba.conf" line 128: "host all all all scram-sha-256"
FATAL:  password authentication failed for user "postgres"
DETAIL:  Connection matched file "/var/lib/postgresql/data/pg_hba.conf" line 128: "host all all all scram-sha-256"

When the stored password is a SCRAM secret, a wrong password got no reason line at all, only the pg_hba.conf line that matched; we saw this on 13, 17 and 18, under a scram-sha-256 and under an md5 rule. Only an md5-stored password logged DETAIL: Password does not match for user "…", which is what the postgres:13 image gives by default, since it stores passwords as md5. With log_error_verbosity = verbose the log also showed the code: FATAL: 28P01: password authentication failed for user "postgres". So the first step is always docker logs <container> (or the server log), not trying more passwords. A role that really does not exist is a missing CREATE ROLE, not a password problem; over the container’s trusted local socket the server says so directly, see role does not exist.

Server log DETAILCauseFix
Role "x" does not exist.wrong user name, or wrong server/database clustercreate the role, or connect where it exists
User "x" has no password assigned.role created without PASSWORDALTER ROLE x PASSWORD '…'
User "x" does not have a valid SCRAM secret.password stored as md5, pg_hba.conf says scram-sha-256set the password again with password_encryption = 'scram-sha-256'
Password does not match for user "x". (md5-stored password)wrong passwordfind which password the client really sends
only Connection matched … line N (SCRAM-stored password, under a scram-sha-256 or md5 rule)wrong passwordthe same; on Docker, check the volume first

Why does my Docker Postgres reject the password I set in POSTGRES_PASSWORD?

Because the image only applies POSTGRES_PASSWORD when it initialises an empty data directory. We started postgres:17 with POSTGRES_PASSWORD=firstpass on a named volume, removed the container, and started a new one on the same volume with POSTGRES_PASSWORD=secondpass. The log said:

PostgreSQL Database directory appears to contain a database; Skipping initialization

secondpass failed with password authentication failed for user "postgres"; firstpass still worked. The variable was present in the new container, it was just not used.

You do not need the old password to fix it. The image’s pg_hba.conf trusts local socket connections:

local   all             all                                     trust
host    all             all             127.0.0.1/32            trust
host    all             all             ::1/128                 trust
...
host all all all scram-sha-256
-- docker exec -it <container> psql -U postgres
ALTER ROLE postgres PASSWORD 'secondpass';
-- ALTER ROLE   (no password was asked)

After that, secondpass worked and firstpass failed. Connections from the host through the published port did not match the 127.0.0.1/32 trust line: the log showed them matching the last line, the scram-sha-256 one. The alternative is deleting the volume, which deletes the data.

Can md5 vs scram-sha-256 cause this error?

Yes, one way round. On 17 we stored a password as md5 and kept the image’s scram-sha-256 rule:

SET password_encryption = 'md5';
ALTER ROLE app PASSWORD 'right';
-- ALTER ROLE    (no warning on 17.11)
SELECT left(rolpassword, 6) FROM pg_authid WHERE rolname = 'app';
-- md5f34

Logging in with the correct password failed with the usual client error; the log said User "app" does not have a valid SCRAM secret. Setting the password again with the default password_encryption (the stored value then began with SCRAM-SHA-256$) fixed it. The other way round works: with the rule changed to md5, both an md5-stored and a SCRAM-stored password logged in. The documentation says the md5 method then “automatically” chooses SCRAM.

This matters when an old dump or an old role script carries md5 hashes into a new server. Note that the official postgres:13 image differs from 17: its rule is host all all all md5 and password_encryption is md5. On 18, storing an md5 password produced a warning that 17 did not give:

WARNING:  01P01: setting an MD5-encrypted password
DETAIL:  MD5 password support is deprecated and will be removed in a future release of PostgreSQL.

Why does my connection URL fail with a password that works in psql?

Because special characters in the URL change what is sent. With the password p@ss#1, the URL postgresql://app:p@ss#1@127.0.0.1:55505/postgres never reached the server: could not translate host name "ss#1@127.0.0.1" to address. (Errors that come before the server is reached, such as Connection refused, are in connection refused: is the server running.) Encoded as p%40ss%231, it logged in. The case that does produce our error is a password that contains a valid percent sequence: for the password pa%41ss, libpq decoded %41 to A, sent paAss, and the server answered password authentication failed for user "app". Written as pa%2541ss, it worked. The documentation says the URI “needs to be encoded with percent-encoding if it includes symbols with special meaning”. We tested libpq only; other drivers parse URLs themselves, and environment variables in tools such as n8n, DBeaver or a hosted Postgres dashboard all end up in some URL or connection string that follows its own rules.

Is PGPASSWORD or .pgpass sending a different password?

We measured the order in libpq: a password in the connection string beat PGPASSWORD, and PGPASSWORD beat .pgpass. A correct .pgpass with a stale PGPASSWORD in the shell failed. When the wrong password came from the file, psql added a line that tells you so:

FATAL:  password authentication failed for user "app"
password retrieved from file "/home/you/.pgpass"

A .pgpass that was not used at all produced a different error, fe_sendauth: no password supplied: once because the file was mode 644 (psql warned permissions should be u=rw (0600) or less), once because the entry said 127.0.0.1 and we connected to localhost.

Where libpq took the password from, in the order we measured

Is there a password in the connection string or URL?

  • YesThat one is sentEven with a correct PGPASSWORD set. Check its percent-encoding.
  • No, but PGPASSWORD is setPGPASSWORD is sentEven with a correct .pgpass entry.
  • Neither, and .pgpass is 0600 with a matching hostThe .pgpass entry is sentA wrong one adds "password retrieved from file".
  • Neither, and .pgpass is unusableNothing is sentfe_sendauth: no password supplied, not 28P01.

Measured with psql/libpq 16.15 against PostgreSQL 17.11. Other drivers may read other variables or none.

Another FATAL at connection time, measured the same way, is remaining connection slots are reserved; if a pooler sits between the application and the database, it is a second place that holds a password, and how we set one up is in PgBouncer and row-level security. Other SQLSTATEs are in our overview of PostgreSQL error codes.

What we did not measure

Other drivers than libpq (Node, JDBC, Python, Go) and how they parse URLs. Docker Compose itself, and Supabase, DBeaver and n8n. Connecting to a different server than intended (a local PostgreSQL on the same port as the container). POSTGRES_HOST_AUTH_METHOD and Docker secrets (POSTGRES_PASSWORD_FILE). LDAP, certificate and ident authentication. Whether ALTER ROLE … PASSWORD ends up in the server log with statement logging on. The volume behaviour on postgres:18, where the data directory moved to /var/lib/postgresql/18/docker.

Sources: PostgreSQL 17 documentation on password authentication and connection strings, read on 22 October 2026. All measurements are our own, on PostgreSQL 17.11, 13.23 and 18.6 in the official Docker images, on 22 October 2026.

Let's talk

What are we building?

One conversation is enough to know whether we fit. Tell us what you have in mind — we will tell you how fast it can happen, and whether we are the right people for it.

Start the conversation