Resolver o erro MySQL "Access denied for user"

Publicado a 16 min de leitura

Porque é que "Access denied for user" quase nunca tem que ver com uma palavra-passe errada: unix_socket, localhost contra 127.0.0.1, privilégios e a reposição segura da palavra-passe do root.

"Access denied for user" é a mensagem de erro mais procurada no universo do MySQL e, na maioria dos casos, a palavra-passe nem sequer é a culpada. No Debian e no Ubuntu, o início de sessão falha sobretudo por causa da autenticação por socket, da confusão entre localhost e 127.0.0.1 ou de uma linha de utilizador criada para o host errado. Este artigo percorre as causas pela ordem em que realmente ocorrem, mostra a reposição da palavra-passe através de skip-grant-tables juntamente com o caminho de volta, e descreve como reconhece que o início de sessão ficou mesmo reparado.

Todas as indicações referem-se ao Debian 13 (MariaDB 11.8), ao Debian 12 (MariaDB 10.11), ao Ubuntu 24.04 (MariaDB 10.11 ou MySQL 8.0) e ao Ubuntu 22.04 (MariaDB 10.6 ou MySQL 8.0). Um ponto importante desde já: o Debian não fornece qualquer pacote mysql-server. Se instalou "MySQL" num sistema Debian, o que está a correr lá é MariaDB, e isso explica logo uma parte da confusão.

Ler a mensagem de erro com atenção

É o texto exato que decide qual a causa em questão. Na prática, vai deparar-se com estas cinco variantes:

MensagemSignificado
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)Foi enviada uma palavra-passe que não correspondeu, ou não existe qualquer linha de conta compatível.
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)Não foi enviada palavra-passe nenhuma. Em regra falta o -p ou a aplicação não lê a configuração.
ERROR 1698 (28000): Access denied for user 'root'@'localhost'O caso clássico: a conta usa unix_socket ou auth_socket. Aqui uma palavra-passe não tem qualquer efeito.
ERROR 1044 (42000): Access denied for user 'app'@'localhost' to database 'shop'A autenticação foi bem sucedida. Faltam apenas privilégios sobre esta base de dados.
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/run/mysqld/mysqld.sock'Não é um problema de autenticação. O serviço não está a correr ou o caminho do socket não está certo.

Há dois pormenores que passam despercebidos com frequência. Primeiro: o nome de host que consta da mensagem é o host com que o servidor o viu, não aquele que escreveu. Se lá estiver 'app'@'localhost' apesar de se ter ligado com -h 127.0.0.1, então o servidor fez uma resolução inversa. Segundo: o 1045 não distingue entre "palavra-passe errada" e "a conta não existe". Ambos os casos produzem a mesma saída, e isso é propositado, para que um atacante não possa descobrir nomes de utilizador válidos por tentativa.

O caso mais frequente: unix_socket no MariaDB

Desde o MariaDB 10.4, os pacotes do Debian e do Ubuntu criam a conta root@localhost de forma a que esta se autentique através do socket Unix. A linha da conta tem, no essencial, este aspeto:

CREATE USER 'root'@'localhost' IDENTIFIED VIA mysql_native_password USING 'invalid'
  OR unix_socket;

Isto significa: quem tiver sessão iniciada como utilizador de sistema root entra sem palavra-passe. Quem enviar uma palavra-passe é verificado contra o hash da cadeia de caracteres "invalid", e essa ninguém consegue acertar. A parte com mysql_native_password só lá está porque, de outro modo, o SET PASSWORD abortaria com um erro. Consequência prática: mysql -u root -p como utilizador normal falha independentemente da palavra-passe que introduzir. O correto é:

sudo mariadb

No MySQL 8.0 do Ubuntu, o plugin chama-se auth_socket em vez de unix_socket, mas o comportamento é idêntico. Aí o comando é sudo mysql.

Verifique qual o plugin que uma conta usa na realidade, e faça-o com SHOW CREATE USER:

mariadb -e "SHOW CREATE USER 'root'@'localhost';"

A saída mostra a cadeia completa dos dois métodos:

CREATE USER `root`@`localhost` IDENTIFIED VIA mysql_native_password USING 'invalid' OR unix_socket

Aqui espreita uma armadilha que quase nenhum guia menciona: desde o MariaDB 10.4, mysql.user é apenas uma vista (view), e os dados verdadeiros estão em JSON em mysql.global_priv. Essa vista só conhece um único método de autenticação por conta e indica, por isso, mysql_native_password para root@localhost, embora na realidade seja o unix_socket que atua. Quem tomar a consulta habitual SELECT User, Host, plugin FROM mysql.user como prova fica convencido de que o servidor se autentica por palavra-passe e procura o erro no sítio errado. O mesmo ponto cego afeta JSON_VALUE(priv,"$.plugin"), porque também este lê apenas o primeiro elemento. O segundo método está no campo auth_or e tem de ser lido à parte:

mariadb -e 'SELECT CONCAT(user,"@",host) AS conta, JSON_VALUE(priv,"$.plugin") AS plugin, JSON_QUERY(priv,"$.auth_or") AS auth_or FROM mysql.global_priv;'

Para root@localhost, a coluna plugin continua então a mostrar mysql_native_password, ao passo que em auth_or aparece [{},{"plugin":"unix_socket"}]. Só esta segunda coluna revela que a autenticação por socket está ativa.

O plugin está carregado? Depois de um upgrade defeituoso pode ficar em falta, e nesse caso o servidor responde ERROR 1524 (HY000): Plugin 'unix_socket' is not loaded:

mariadb -e "SELECT plugin_name, plugin_status FROM information_schema.plugins WHERE plugin_name LIKE '%socket%';"

Se quiser mesmo passar o root para uma palavra-passe, por exemplo porque um script de backup corre sob outro utilizador de sistema, mantenha adicionalmente a variante por socket. Caso contrário, deixam de funcionar os scripts de manutenção da distribuição e o sudo mariadb:

ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
  OR mysql_native_password USING PASSWORD('ASuaNovaPalavraPasse');

De qualquer forma, o melhor caminho é uma conta de administração própria em vez de uma palavra-passe para o root. Para as aplicações isso vale por maioria de razão: uma base de dados, um utilizador, apenas os privilégios necessários.

localhost não é 127.0.0.1

Esta distinção provoca mais mensagens "Access denied" do que qualquer palavra-passe errada. Os clientes do MySQL e do MariaDB tratam o nome de host localhost como caso especial e ligam-se através do socket Unix. Só 127.0.0.1 força uma ligação TCP. Do ponto de vista da gestão de privilégios, os dois não são o mesmo nome de máquina, porque numa ligação por socket o servidor regista o host localhost, e numa ligação TCP pelo endereço de loopback regista localhost (após resolução inversa) ou 127.0.0.1, consoante a definição de skip_name_resolve.

Uma conta que exista apenas como 'app'@'127.0.0.1' não é alcançável através do socket, e vice-versa. É exatamente isso que acontece quando uma aplicação PHP tem host=localhost na configuração: o PHP liga-se pelo socket, mas os privilégios foram atribuídos ao endereço IP. Duas contraprovas:

mariadb -u app -p'PalavraPasse' -e "SELECT USER(), CURRENT_USER();"
mariadb -h 127.0.0.1 -u app -p'PalavraPasse' -e "SELECT USER(), CURRENT_USER();"

Se apenas uma das duas falhar, a causa está encontrada. Preste aqui atenção à resolução de nomes: enquanto skip_name_resolve estiver desligado, e é assim que fica nas instalações por pacote do Debian e do Ubuntu, o servidor resolve o endereço de loopback de forma inversa para localhost. Se já existir uma conta 'app'@'localhost', a autenticação com -h 127.0.0.1 também tem sucesso, e CURRENT_USER() devolve então app@localhost em vez de app@127.0.0.1. Uma conta 'app'@'127.0.0.1' criada adicionalmente fica neste caso sem efeito, só passa a atuar quando a conta de localhost não existir. A solução limpa é criar a conta para o caminho que a aplicação realmente usa, em vez de configurar as duas variantes "por precaução".

Verifique além disso se a resolução de nomes está desligada. Com skip_name_resolve ativo, as linhas de conta com nomes de host como 'app'@'web01.intern' deixam por completo de funcionar, e passam a contar apenas os endereços IP:

mariadb -e "SHOW VARIABLES LIKE 'skip_name_resolve';"

Outro obstáculo é a ordem pela qual o servidor escolhe as linhas correspondentes. Ordena do específico para o genérico e fica com a primeira correspondência, não com a melhor. Se, além de 'app'@'%', existir ainda uma conta anónima ''@'localhost', numa ligação local ganha a conta anónima, e a sua autenticação falha com uma mensagem que, ainda assim, indica o seu nome de utilizador. Os pacotes atuais já não criam contas anónimas, mas em sistemas migrados ao longo de anos ainda se encontram:

mariadb -e "SELECT user, host FROM mysql.global_priv WHERE user = '';"

Palavra-passe, privilégios e maiúsculas

Restam as causas que têm mesmo que ver com as credenciais.

A shell devora os caracteres especiais

Entre -p e a palavra-passe não pode haver espaço, caso contrário o cliente interpreta a palavra-passe como nome de base de dados. Palavras-passe com $, !, & ou espaços pertencem entre plicas, porque de outro modo a shell substitui-as ou corta-as. Realmente limpo é não passar a palavra-passe de todo na linha de comandos, porque aí vai parar à lista de processos e ao ficheiro de histórico. Crie antes um ficheiro ~/.my.cnf com permissões 0600:

[client]
user=app
password=ASuaPalavraPasse

Ao contrário, um ficheiro ~/.my.cnf esquecido também é uma possível causa de erro. Sobrepõe-se em silêncio ao que indica na linha de comandos, e depois recebe "Access denied" para um utilizador que nunca escreveu.

Maiúsculas: no nome de utilizador sim, no host não

No MySQL e no MariaDB, os nomes de utilizador têm de coincidir carácter a carácter, App e app são duas contas diferentes. Os nomes de host são comparados sem distinguir maiúsculas de minúsculas, mas guardados exatamente como constavam no CREATE USER. Quem tiver criado 'app'@'LOCALHOST' por engano vê nas saídas de SHOW GRANTS e nos scripts duas contas aparentemente distintas, que no entanto servem a mesma ligação. Duplicados destes tornam a análise do erro desnecessariamente morosa, porque atribui privilégios a uma das linhas e o servidor escolhe a outra. Podem localizar-se assim:

mariadb -e "SELECT user, host FROM mysql.global_priv WHERE host <> LOWER(host);"

Faltam privilégios, não a autenticação

Se aparecer ERROR 1044 em vez de 1045, a autenticação foi bem sucedida. Faltam então privilégios sobre uma determinada base de dados. Verifique o que a conta pode mesmo fazer:

mariadb -e "SHOW GRANTS FOR 'app'@'localhost';"

Só precisa de FLUSH PRIVILEGES se tiver alterado as tabelas de privilégios diretamente por INSERT ou UPDATE. Depois de GRANT, CREATE USER ou ALTER USER é supérfluo, e por vezes esconde o facto de o comando propriamente dito não ter surtido efeito.

O cliente não percebe o plugin

O MySQL 8.0 usa por omissão caching_sha2_password. Clientes e bibliotecas mais antigos respondem a isso com ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded. Não é um problema de privilégios, é uma incompatibilidade. No MySQL 8.0 o mysql_native_password ainda está disponível como alternativa, no MySQL 8.4 foi removido. Na dúvida, atualize antes o cliente em vez de recuar na cifra.

Repor a palavra-passe do root

Quando já nada resulta, o servidor tem de arrancar uma vez sem verificação de privilégios. Há dois caminhos que levam ao objetivo. O caminho através de --init-file é o mais seguro, porque nele o servidor corre sempre com a verificação de privilégios ativa.

Variante 1: init-file (recomendada)

O ficheiro SQL tem de estar num local que o serviço possa ler. Em /tmp ou /root isso falha com regularidade no Ubuntu, por causa do AppArmor e do PrivateTmp no serviço systemd. Coloque-o por isso no diretório de dados:

sudo systemctl stop mariadb
sudo tee /var/lib/mysql/kh-reset.sql >/dev/null <<'SQL'
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
  OR mysql_native_password USING PASSWORD('NovaPalavraPasseRoot');
SQL
sudo chown mysql:mysql /var/lib/mysql/kh-reset.sql
sudo systemctl set-environment MYSQLD_OPTS="--init-file=/var/lib/mysql/kh-reset.sql"
sudo systemctl start mariadb

A seguir limpe tudo sem falta, caso contrário o servidor volta a arrancar com o ficheiro em cada início:

sudo systemctl unset-environment MYSQLD_OPTS
sudo rm /var/lib/mysql/kh-reset.sql
sudo systemctl restart mariadb

Para o MySQL 8.0 no Ubuntu, o serviço chama-se mysql e a linha SQL é:

ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'NovaPalavraPasseRoot';

Variante 2: skip-grant-tables

Esta variante desliga por completo a verificação de privilégios. Sem --skip-networking, durante esse período qualquer pessoa que alcance a porta teria acesso total a todos os dados. Por isso a opção não é opcional, é obrigatória.

sudo systemctl stop mariadb
sudo systemctl set-environment MYSQLD_OPTS="--skip-grant-tables --skip-networking"
sudo systemctl start mariadb
sudo mariadb

Na sessão, carregue primeiro as tabelas de privilégios, caso contrário o servidor recusa o ALTER USER com uma mensagem de erro:

FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket
  OR mysql_native_password USING PASSWORD('NovaPalavraPasseRoot');
EXIT;

E depois de volta ao funcionamento normal:

sudo systemctl unset-environment MYSQLD_OPTS
sudo systemctl restart mariadb

Se a sua distribuição não avaliar a variável MYSQLD_OPTS, funciona sempre o ficheiro drop-in em /etc/systemd/system/mariadb.service.d/reset.conf com o conteúdo [Service] e Environment="MYSQLD_OPTS=--skip-grant-tables --skip-networking", seguido de sudo systemctl daemon-reload. Apague o ficheiro a seguir e recarregue o systemd.

Quando a reposição corre mal

É precisamente aqui que a maioria dos guias termina. Os quatro problemas subsequentes mais frequentes:

O serviço deixa de arrancar. Na maioria dos casos é um erro de escrita no ficheiro SQL. O servidor aborta então no arranque. O Debian regista o MariaDB por omissão no journal, o Ubuntu com MySQL escreve adicionalmente para um ficheiro:

sudo journalctl -u mariadb -n 50 --no-pager
sudo tail -n 50 /var/log/mysql/error.log

Para onde o servidor escreve pode consultar-se com SHOW VARIABLES LIKE 'log_error';. Mas não espere aí um caminho de ficheiro: nas instalações por pacote do Debian e do Ubuntu o valor está vazio, porque o serviço corre com --skip-log-error. Tudo passa então para a saída de erro padrão e acaba no journal ou em /var/log/syslog, consultável com journalctl -u mariadb. Para resolver, remova a variável de ambiente --init-file e reinicie.

O servidor fica permanentemente sem verificação de privilégios. Isso acontece quando o unset-environment foi esquecido, e é a variante mais perigosa, porque visto de fora tudo parece normal. Duas verificações:

systemctl show-environment
ps -o args= -C mariadbd

Se skip-grant-tables aparecer numa das duas saídas, a verificação de privilégios continua desligada. O systemctl show-environment pressupõe que o systemd corre como PID 1. Num servidor normal isso verifica-se, mas num container ou num sistema com SysV-Init o comando não existe. Aí lê-se o ambiente diretamente do processo em execução:

cat /proc/$(pgrep -n mariadbd)/environ | tr '\0' '\n'

Também não deve confiar apenas na lista de argumentos: iniciado através do systemd, o ps mostra muitas vezes só /usr/sbin/mariadbd sem opção nenhuma, ao passo que através de um script SysV-Init aparece a lista completa. Outro indício: com skip-grant-tables, o SHOW GRANTS responde com uma mensagem de erro em vez de mostrar privilégios.

O AppArmor bloqueia o ficheiro init. O sintoma é um servidor que não arranca sem motivo aparente. Uma vista de olhos ao log do kernel esclarece a situação:

sudo dmesg | grep -i denied

Fica completamente sem acesso. Isso sucede quando se passa o root por ALTER USER ... IDENTIFIED BY para uma palavra-passe pura, se perde nesse processo a autenticação por socket e depois já não se sabe a nova palavra-passe. A saída é repetir a mesma reposição, desta vez com a variante dupla unix_socket OR mysql_native_password mostrada acima. Antes de cada intervenção, faça uma cópia das tabelas de privilégios: demora segundos e poupa horas quando as coisas correm mal:

sudo mariadb-dump mysql > /root/mysql-grants.sql

A opção --single-transaction, habitual noutros casos, pode dispensá-la aqui. Corre sem erros, mas não produz efeito, porque as tabelas da base de dados mysql assentam em Aria ou MyISAM e, como tal, não são transacionais.

Num alojamento web gerido não tem acesso ao sistema e, por isso, nenhuma destas opções. Aí os utilizadores da base de dados e as palavras-passe são repostos através do painel de controlo. Num servidor root KVM ou num servidor dedicado da KernelHost tem acesso root completo e, caso o servidor deixe de estar acessível pela ligação de rede, chega ao sistema através da consola na área de cliente.

Verificar se está mesmo resolvido

O facto de um comando correr sem erros ainda não significa que a autenticação funcione de forma duradoura. Estas quatro verificações revelam os erros residuais típicos.

Primeiro, a diferença entre USER() e CURRENT_USER(). A primeira função mostra por quem se fez passar, a segunda mostra que linha de conta o servidor usa na realidade:

mariadb -u app -p'PalavraPasse' -e "SELECT USER(), CURRENT_USER();"

Se aí estiverem dois valores diferentes, por exemplo app@localhost e app@%, então os seus privilégios atuam por uma linha diferente da esperada. Isso explica os erros 1044 que surgem mais tarde, antes que deem nas vistas em produção.

Segundo, um acesso real à base de dados de destino em vez de apenas uma autenticação, e a avaliação do valor de retorno:

mariadb -u app -p'PalavraPasse' minha_db -e "SELECT 1;" ; echo "Valor de retorno: $?"

Terceiro, um reinício do serviço e, a seguir, a mesma autenticação outra vez. Assim garante que a alteração está mesmo nas tabelas e não apenas na memória em execução, e que não sobrou nenhuma variável de ambiente da reposição:

sudo systemctl restart mariadb
sudo systemctl is-active mariadb

Quarto, a própria aplicação. Um teste bem sucedido na linha de comandos diz pouco sobre uma aplicação PHP que se liga sob o utilizador do servidor web e através do socket. Teste por isso sob o mesmo utilizador de sistema:

sudo -u www-data mariadb -u app -p'PalavraPasse' minha_db -e "SELECT CURRENT_USER();"

Diferenças entre as distribuições

Um guia que afirme o mesmo para os quatro sistemas está errado em pelo menos um ponto. Esta tabela resume os desvios relevantes:

SistemaBase de dados padrãoPlugin de socketNota
Debian 13MariaDB 11.8unix_socketnão existe pacote mysql-server disponível; mysql é apenas uma ligação simbólica para mariadb e emite um aviso quando é invocado
Debian 12MariaDB 10.11unix_socketnão existe pacote mysql-server disponível
Ubuntu 24.04MariaDB 10.11 ou MySQL 8.0unix_socket ou auth_socketos dois pacotes coexistem nas fontes, com risco de confusão nos guias
Ubuntu 22.04MariaDB 10.6 ou MySQL 8.0unix_socket ou auth_socketJSON_VALUE e JSON_QUERY sobre mysql.global_priv funcionam aqui exatamente como na 10.11 e na 11.8; também na 10.6 mysql.user é apenas uma vista, e para a cadeia de autenticação completa SHOW CREATE USER continua a ser o caminho mais curto

Outras diferenças que custam tempo na prática: os ficheiros de configuração do MariaDB estão em /etc/mysql/mariadb.conf.d/50-server.cnf, os do MySQL em /etc/mysql/mysql.conf.d/mysqld.cnf. Os nomes dos serviços são mariadb e mysql, sendo que o MariaDB traz adicionalmente um alias mysql. No MariaDB já não existe, desde a versão 10.4, o utilizador debian-sys-maint, e aí o ficheiro /etc/mysql/debian.cnf aponta para o root através do socket. No MySQL do Ubuntu, o utilizador de manutenção continua a existir, e esse é o melhor acesso de emergência quando a palavra-passe do root se perdeu e não quer reiniciar o servidor:

sudo mysql --defaults-file=/etc/mysql/debian.cnf

Se seguir esta ordem, ou seja, ler primeiro a mensagem com atenção, depois verificar o plugin e a linha de host, depois o caminho da ligação, e só no fim repor a palavra-passe, a grande maioria dos casos resolve-se em poucos minutos e sem tempo de indisponibilidade. A reposição através de skip-grant-tables é o último recurso, não o primeiro passo.

Perguntas frequentes

Porque é que mysql -u root -p não funciona, apesar de a palavra-passe estar correta?
Porque no Debian e no Ubuntu a conta root@localhost é autenticada através do socket Unix. O pacote cria-a como IDENTIFIED VIA mysql_native_password USING 'invalid' OR unix_socket. O hash da palavra-passe pertence à cadeia de caracteres invalid e ninguém o consegue acertar. A cadeia completa dos dois métodos só é mostrada por SHOW CREATE USER para root@localhost, porque a vista mysql.user devolve apenas o primeiro deles e indica aí mysql_native_password. Inicie sessão antes com sudo mariadb, no MySQL com sudo mysql.
Qual é a diferença entre o ERROR 1045 e o ERROR 1698?
O ERROR 1045 traz o acrescento (using password: YES ou NO) e significa que não foi encontrada nenhuma linha de conta compatível ou que a palavra-passe não está correta. O ERROR 1698, sem esse acrescento, aponta para a autenticação por socket: a conta existe, mas não aceita palavra-passe nenhuma, verifica antes o utilizador de sistema.
localhost é o mesmo que 127.0.0.1?
Não. Os clientes tratam localhost como caso especial e ligam-se através do socket Unix, ao passo que 127.0.0.1 força uma ligação TCP. Para a gestão de privilégios são indicações de host diferentes. Uma conta criada apenas como 'app'@'127.0.0.1' não é alcançável através do socket.
As maiúsculas são relevantes no nome de utilizador e no host?
O nome de utilizador é comparado carácter a carácter, App e app são duas contas diferentes. O nome de host é comparado sem distinguir maiúsculas de minúsculas, mas guardado exatamente como constava no CREATE USER. Daí resultam duplicados aparentes como 'app'@'LOCALHOST', que dificultam a análise do erro.
Como reponho a palavra-passe do root sem skip-grant-tables?
Através da opção --init-file. Coloca um ficheiro SQL com o comando ALTER USER no diretório de dados /var/lib/mysql, ativa-o no arranque através de MYSQLD_OPTS e remove ambos a seguir. A vantagem: a verificação de privilégios mantém-se ativa todo o tempo, não se abre qualquer janela com acesso total livre.
Como reconheço que o servidor ainda está a correr sem verificação de privilégios?
Verifique se a entrada skip-grant-tables aparece em systemctl show-environment e em ps -o args= -C mariadbd. Os dois caminhos têm limites: systemctl show-environment pressupõe o systemd como PID 1, caso contrário lê-se o ambiente com cat /proc/$(pgrep -n mariadbd)/environ diretamente do processo, e o ps mostra muitas vezes, num arranque através do systemd, apenas o nome do programa sem argumentos. Outro indício é o facto de SHOW GRANTS responder neste modo de funcionamento com uma mensagem de erro em vez de mostrar privilégios. O unset-environment esquecido é a causa mais frequente.
Existe no Debian um pacote mysql-server?
Não. O Debian 13 e o Debian 12 fornecem exclusivamente MariaDB, nas versões 11.8 e 10.11 respetivamente. Só o Ubuntu 24.04 e o 22.04 oferecem mysql-server 8.0 além do MariaDB. Os guias que mandam instalar mysql-server no Debian estão errados.

MySQL MariaDB Debian Ubuntu Base de dados Resolução de problemas Administração Linux