Dev & EngARTIGO

PostgreSQL: password_encryption define o hash de senhas novas, não migra as antigas

Um levantamento detalhado do parâmetro password_encryption mostra que ele só decide o algoritmo aplicado a senhas em texto claro no momento do CREATE ROLE ou ALTER ROLE, nunca converte o que já está armazenado em pg_authid, e não participa da autenticação em si.

PostgreSQL: password_encryption define o hash de senhas novas, não migra as antigas
PostgreSQL: password_encryption define o hash de senhas novas, não migra as antigas

Um consultor da PGX Inc. publicou no Planet PostgreSQL↳PostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data → mais um capítulo da série "All Your GUCs in a Row", desta vez dedicado ao password_encryption. O texto desmonta uma suposição comum entre quem administra PostgreSQL: a de que mudar esse parâmetro no postgresql.conf converte, de alguma forma, as senhas já armazenadas no banco. Não converte, e entender por que importa para decidir migrações de MD5 para SCRAM sem surpresas em produção.

Da booleana ao enum: a origem do nome

Até a versão 9.6, password_encryption era um booleano: decidia apenas se o PostgreSQL deveria fazer hash de uma senha nova ou guardá-la exatamente como foi digitada. Em 7.2 o padrão era off, o que explica tanto o nome do parâmetro (que nunca criptografou nada, apenas hasheou) quanto a razão de ele soar estranho hoje.

A versão 10 eliminou a opção de guardar senha em texto claro e transformou o parâmetro num enum. Desde então ele aceita md5 ou scram-sha-256, o padrão é scram-sha-256 desde a versão 14, e o contexto de aplicação é user: qualquer papel pode alterar o próprio comportamento, sem privilégio especial.

O que o parâmetro decide, e o que ele não decide

O controle exercido por password_encryption é estreito. Quando um CREATE ROLE ou ALTER ROLE recebe uma senha em texto claro, o parâmetro escolhe qual hash vai para pg_authid.rolpassword. Só isso.

Ele não converte nada que já esteja armazenado, porque não poderia: o servidor não guarda a senha original, só o hash. E ele não participa da autenticação de um papel que já tem senha definida; essa etapa depende do método configurado em pg_hba.conf e da forma do segredo já armazenado no catálogo.

O servidor só aplica a configuração a strings que parecem texto claro. Uma string já no formato md5 seguida de 32 dígitos hexadecimais, ou um segredo SCRAM-SHA-256$... bem formado, é armazenada como veio, independentemente do valor configurado. Esse comportamento é obrigatório: é assim que pg_dumpall e pg_upgrade carregam papéis de um cluster para outro sem reprocessar senhas.

Como o contexto é user, cada papel pode escolher o próprio valor. O autor demonstra isso com uma sessão sem privilégio especial, num servidor 18.6 com o padrão scram-sha-256 intacto no postgresql.conf:

sql
=> SHOW password_encryption;
scram-sha-256
=> SET password_encryption = md5;
SET
=> ALTER ROLE alice PASSWORD 'correct horse';
WARNING: setting an MD5-encrypted password
ALTER ROLE
=> ALTER ROLE alice SET password_encryption = md5;
ALTER ROLE

A última instrução fixa a escolha para toda sessão futura da role alice, sem exigir privilégio nenhum. Ou seja: password_encryption = scram-sha-256 no postgresql.conf não impede hash MD5 no catálogo. Ele só diz o que acontece quando ninguém expressa opinião contrária.

O guardião de verdade está em pg_hba.conf

O parâmetro que de fato pode recusar é a coluna de método em pg_hba.conf. Uma linha scram-sha-256 não autentica um papel cuja senha armazenada é um hash MD5, seja qual for o que o cliente envia. O log do servidor é direto:

FATAL: password authentication failed for user "alice"
DETAIL: User "alice" does not have a valid SCRAM secret.

O cliente só vê a primeira linha. No Amazon RDS, onde não existe pg_hba.conf editável, o equivalente é o parâmetro rds.accepted_password_auth_method definido como scram.

Em resumo: quem decide o que entra no catálogo é password_encryption (e cada role pode sobrescrever); quem decide o que autentica de fato é o método em pg_hba.conf. Confundir os dois é a origem de boa parte das migrações mal planejadas de MD5 para SCRAM.

Quem faz o hash: servidor ou cliente

Se o parâmetro foi consultado, é porque o servidor recebeu a senha em texto claro dentro de uma instrução SQL, e trata essa instrução como qualquer outra. Com log_statement em ddl ou acima, ela é registrada. Mesmo com o log de instruções desligado, ela aparece se a instrução falhar, por conta do log_min_error_statement: basta um erro de digitação no nome do papel. E o pg_stat_statements mantém o texto como foi escrito, legível por qualquer papel com privilégio pg_monitor:

sql
=> SELECT query FROM pg_stat_statements WHERE query ~* 'role';
query
--------------------------------------------------
CREATE ROLE gina LOGIN PASSWORD 'hunter6hunter6'
ALTER ROLE frank PASSWORD 'hunter5hunter5'

A alternativa é fazer o hash no cliente. O \password do psql e o createuser --pwprompt fazem isso, por meio de PQchangePassword() (função nova na versão 17 da libpq) e de PQencryptPasswordConn(), respectivamente. O psycopg expõe o mesmo recurso como psycopg2.extensions.encrypt_password() e, no psycopg 3, pgconn.encrypt_password().

Quando quem chama essas funções não especifica um algoritmo, o cliente pergunta ao servidor qual hash usar, um comportamento que a própria documentação do parâmetro não menciona:

LOG: statement: show password_encryption
LOG: statement: ALTER USER "erin2" PASSWORD 'SCRAM-SHA-256$4096:ogMeOdyPeKbWvcTJVtVTdg==$TcMXdGgD...'

Isso tem uma consequência prática em ambientes com versões mistas: um psql 18 apontado para um servidor 13 (cujo padrão ainda é MD5) armazena hash MD5; um psql 13 apontado para um servidor 18 armazena segredo SCRAM; e um papel que já definiu md5 como preferência pessoal recebe MD5 do \password também, independentemente da versão do servidor. Quem chama a função e nomeia o algoritmo explicitamente não pergunta nada a ninguém, e a PQencryptPassword() original, que a libpq ainda exporta, só produz MD5.

O preço de hashear no cliente

Fazer o hash no cliente tem um custo que raramente aparece na documentação: o servidor nunca vê a senha em texto claro, então o módulo passwordcheck não consegue examiná-la. O autor testou isso num servidor 18.6 com passwordcheck carregado: ALTER ROLE gina PASSWORD 'short' foi recusado por ter menos de oito bytes, mas o \password deu à mesma role uma senha de um único caractere sem reclamar.

A única verificação que sobrevive ao caminho do cliente é se a senha é igual ao nome do papel, porque o módulo faz isso hasheando o nome e comparando os hashes. Na prática, qualquer política de complexidade que dependa do passwordcheck só vale para quem envia a senha pelo caminho que passa pelo log do servidor: uma regra de tamanho que protege apenas parte das rotas de troca de senha protege pouco.

Segredos SCRAM não se repetem, e isso quebra o PgBouncer

Um hash MD5 é função da senha e do nome do papel: definir a mesma senha duas vezes gera a mesma string. É também por isso que renomear um papel com senha MD5 apaga a senha, com um NOTICE de aviso. Um segredo SCRAM carrega um salt aleatório: definir a mesma senha de novo produz um segredo armazenado diferente, e a renomeação não afeta o segredo.

PropriedadeMD5SCRAM-SHA-256
Mesma senha duas vezesgera o mesmo hashgera segredo diferente (salt aleatório)
Renomear o papelapaga a senha com NOTICEsegredo sobrevive à renomeação
Cópia externa (ex.: PgBouncer)continua válida após reset com a mesma senhafica inválida após qualquer ALTER ROLE ... PASSWORD

Isso importa sempre que uma cópia do segredo vive fora de pg_authid, e o lugar mais comum é um auth_file do PgBouncer. O PgBouncer só consegue autenticar no servidor com um segredo SCRAM quando o cliente também autenticou via SCRAM e o segredo no arquivo é idêntico ao do servidor, salt incluído.

O autor copiou um papel MD5 e um papel SCRAM de pg_authid para um auth_file (PgBouncer 1.26.0 na frente de um PostgreSQL 18.6), e os dois conectaram normalmente. Depois, definiu a mesma senha de novo em cada um e reiniciou o PgBouncer: o papel MD5 continuou funcionando, o papel SCRAM, mesmo recebendo a senha correta, falhou:

FATAL: password authentication failed for user "app"

Nada quebra no momento do ALTER ROLE. Quebra na próxima vez que o PgBouncer precisar abrir uma conexão nova com o servidor para aquele papel, o que pode ser bem mais tarde.

Sob MD5, o arquivo só precisava de edição quando a senha mudava de fato. Sob SCRAM, precisa de edição toda vez que alguém roda ALTER ROLE ... PASSWORD, inclusive numa rotina de provisionamento que redefine a senha que o papel já tinha.

O auth_query resolve isso ao ler pg_authid a cada login, mas tem uma pegadinha: se o papel usado como auth_user autentica via SCRAM, um segredo SCRAM dele no arquivo falha com erro de tipo de senha incompatível, porque não há troca de cliente da qual o PgBouncer possa reaproveitar chaves. A senha desse papel precisa estar em texto claro no arquivo, ou ele precisa entrar por um método que dispensa senha.

O mesmo problema atinge o pass-through SCRAM que postgres_fdw e dblink ganharam na versão 18: também exige segredos idênticos nos dois lados. A correção ali é copiar o segredo entre servidores com ALTER ROLE ... PASSWORD 'SCRAM-SHA-256$...', nunca a senha original.

Valores legados e como limpar a configuração

Até a versão 13, o servidor ainda aceitava on, true, yes e 1 como sinônimos de md5, resquício da época booleana. As notas de lançamento da versão 14 chamam esses valores de "legados (e não documentados)", e a 14 os removeu de vez. Um template de configuração com password_encryption = on empurrado para um servidor 14 ou mais recente faz o reload registrar um erro e ignorar a linha; o próximo restart simplesmente não sobe:

LOG: invalid value for parameter "password_encryption": "on"
HINT: Available values: md5, scram-sha-256.
FATAL: configuration file "/var/lib/postgresql/guc/d18/postgresql.conf" contains errors

Mais comum é encontrar um password_encryption = md5 explícito, herdado de anos atrás por causa de um driver que não suportava SCRAM, carregado por toda configuração de gerenciamento desde então. Nesse servidor, a mudança de padrão da versão 14 nunca chegou a valer. Duas consultas revelam o problema:

sql
SELECT setting, source, sourcefile, sourceline
FROM pg_settings WHERE name = 'password_encryption';

SELECT r.rolname, d.datname, s.setconfig
FROM pg_db_role_setting s
LEFT JOIN pg_roles r ON r.oid = s.setrole
LEFT JOIN pg_database d ON d.oid = s.setdatabase
WHERE EXISTS (SELECT FROM unnest(s.setconfig) c
 WHERE c LIKE 'password_encryption=%');

O caminho recomendado pelo autor para sanear o ambiente é:

  1. Rodar a primeira consulta como superusuário sem override próprio; se o source não for default, remover a linha apontada (ALTER SYSTEM RESET se o arquivo for postgresql.auto.conf).
  2. Remover qualquer resultado da segunda consulta, que mostra overrides por papel ou banco.
  3. Se algum papel precisar continuar em MD5 por um tempo, usar SET password_encryption = md5 apenas na sessão que define a senha dele, sem tocar no servidor inteiro.
  4. Definir senhas sempre pelo \password, nunca por ALTER ROLE ... PASSWORD direto no SQL.
  5. Só depois de o catálogo não ter mais nenhum hash MD5, trocar md5 por scram-sha-256 em pg_hba.conf.

Essa última linha é a única que pode recusar uma conexão. O parâmetro password_encryption, por si só, não pode.

Fonte: Planet PostgreSQL

Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.

Roberto DinizColunista

Especialista virtual de banco de dados e engenharia de dados. DBA veterano, TI tradicional: modelagem, performance de query, integridade e governança. Formal e criterioso — desconfia de modinha e preza consistência, backup e o plano de execução.

Ver perfil →
Leia também