Quem administra servidores compartilhados com cPanel, CloudLinux e dezenas de revendedores já deve ter passado por este pesadelo: do nada, dezenas de sites de um mesmo cliente ou revenda começam a exibir erro de conexão com o banco de dados.
O sintoma clássico aparece nos logs do PHP: Access denied for user 'user_db'@'localhost'.
A primeira reação costuma ser verificar se o serviço do MySQL caiu. Mas não: o daemon mysqld ou mariadbd está operando normalmente, outros sites do servidor funcionam sem qualquer instabilidade, e a falha fica restrita a contas pertencentes a um revendedor ou owner específico.
Trocar a senha de cada banco na mão pelo painel resolveria no desespero, mas quebraria a aplicação se ela não for atualizada no arquivo de configuração. Além disso, quando são cinquenta ou cem sites afetados, fazer isso manualmente é inviável.
Neste artigo, vamos dissecar o que causa esse comportamento, como rastrear a origem real do erro com ferramentas do sistema operacional e como resolver o problema em lote com scripts seguros.
1. Anatomia do incidente e análise de causa raiz#
Quando uma aplicação em PHP (como WordPress, Drupal, Magento ou Laravel) tenta abrir uma conexão com o banco de dados, o fluxo de execução cruza várias camadas de abstração e isolamento do Linux:
[Aplicação PHP] -> [PHP-FPM Pool / CageFS LVE Namespace] -> [Socket UNIX / Loopback TCP] -> [MySQL Grant Tables (Memória / InnoDB)]
A perda de sincronia de credenciais restrita a um subconjunto de usuários quase sempre aponta para falhas nas camadas intermediárias de gerenciamento de estado.
Hipótese 1: Dessincronização do mecanismo de mapeamento do cPanel (db-map cache)#
O cPanel não consulta as tabelas internas do MySQL em tempo real para exibir limites e usuários na interface web. Ele mantém um cache indexado em arquivos Berkeley DB (BDB) ou SQLite dentro do diretório /var/cpanel/databases/.
O mecanismo funciona assim: quando ocorre uma alteração de conta (como suspensão e reativação temporária da revenda, troca de pacotes de limites LVE pelo WHM ou atualizações automáticas via upcp), o script /usr/local/cpanel/scripts/update_db_cache é chamado para consolidar permissões e regerar os mapas.
O ponto de falha surge se esse script sofrer interrupção forçada (por falta de memória via OOM Killer, sinal de encerramento prematuro ou corrupção de arquivo durante a escrita). A interface web do cPanel continua listando os usuários do banco, mas os privilégios correspondentes na memória do MySQL ou no arquivo de grants deixam de ser propagados corretamente.
Hipótese 2: Isolamento do sistema de arquivos virtualizado (CageFS opaque mounts)#
O CloudLinux usa o CageFS para isolar cada usuário em seu próprio namespace de sistema de arquivos (chroot avançado baseado em Mount Namespaces do Kernel Linux).
O arquivo de configuração global /etc/my.cnf e o socket de comunicação /var/lib/mysql/mysql.sock são espelhados para dentro do CageFS de cada usuário por meio de montagens do tipo bind (mount --bind).
Se ocorrer uma falha de sincronização no CageFS, ou se o esqueleto base (/usr/share/cagefs-skeleton) tiver sido modificado enquanto existiam sessões ativas do revendedor afetado, os arquivos em /etc/my.cnf.shadow ou as permissões de leitura do socket dentro do namespace podem divergir do ambiente global. O PHP tenta abrir a conexão, o kernel falha em mapear o descritor de arquivo e a camada PDO ou MySQLi do PHP reporta isso genericamente como falha de autenticação.
Hipótese 3: Alterações dinâmicas de escopo globais versus locais (host locking)#
No MySQL e MariaDB, a identidade de um usuário depende da combinação exata de usuário e origem: 'usuario'@'host'.
Aplicações web geralmente conectam informando localhost. No Linux, conectar através da string localhost força o uso de sockets UNIX. Conectar via 127.0.0.1 força o uso da pilha de rede TCP/IP em loopback.
Se as tabelas de privilégios forem reconstruídas de modo que o usuário receba permissão para [email protected], mas não para user_db@localhost, o handshake falha na hora. Além disso, o MySQL varre sua tabela de permissões sequencialmente. Se existir uma entrada genérica ou corrompida (como ''@'localhost') com prioridade de leitura antes da regra do usuário, o MySQL rejeita a conexão com erro de senha inválida, mesmo que a senha fornecida no script esteja certa.
2. Rastreabilidade de erros na camada do PHP e syscalls#
O que acontece no código da aplicação e extensões c#
Quando a aplicação não consegue conectar, a extensão nativa em C do PHP (mysqli ou pdo_mysql) realiza chamadas ao driver de baixo nível (libmysqlclient ou mysqlnd).
No código-fonte do driver nativo do PHP (mysqlnd), a função mysqlnd_connect realiza a negociação inicial. Se o servidor MySQL devolver o pacote de erro ER_ACCESS_DENIED_ERROR (código de erro 1045), a extensão preenche a mensagem de aviso:
// Representação conceitual do tratamento de erro na libmysqlclient / mysqlnd
if (packet->error_no == 1045) {
php_error_docref(NULL, E_WARNING, "Access denied for user '%s'@'%s' (using password: %s)", ...);
SET_CONN_STATE(CONN_STATE_CLOSE);
return FAIL;
}
Parâmetros de compilação e variáveis de ambiente relevantes#
PDO::ATTR_PERSISTENT: quando definido comotrue, o pool do PHP-FPM mantém conexões persistentes em segundo plano. Se as credenciais forem alteradas ou a tabela de privilégios for recarregada no MySQL, os workers do PHP-FPM podem continuar tentando usar dados obsoletos até que o processo do pool seja reciclado.MYSQLI_CLIENT_SSL: se habilitado via configuração ou código e o certificado CA não puder ser lido dentro do namespace do CageFS, a conexão é abortada com erro genérico de autenticação.
3. Diagnóstico prático no terminal sem suposições#
Antes de alterar qualquer configuração no servidor, o caminho mais seguro é coletar evidências em tempo de execução para separar problemas de permissão no sistema operacional de falhas reais nas tabelas do MySQL.
Passo 1: Inspeção de chamadas do sistema com strace#
Para verificar se o PHP está tentando abrir o socket correto e onde exatamente a conexão trava, executamos um strace simulando o ambiente do usuário afetado dentro de sua jaula:
su -s /bin/bash - user_cpanel -c "strace -f -s 128 -e trace=network,open,openat,connect php -r 'mysqli_connect(\"localhost\", \"user_db\", \"senha_aqui\");'"
Análise do comportamento observado no strace#
O retorno das chamadas de sistema revela o ponto exato da falha:
Cenário A: Problema de isolamento no CageFS
openat(AT_FDCWD, "/var/lib/mysql/mysql.sock", O_RDWR) = -1 ENOENT (No such file or directory)
O arquivo do socket não existe dentro do namespace desse usuário. O problema é de montagem de volumes virtuais do CloudLinux, não de senha.
Cenário B: Problema de permissão no arquivo de socket
connect(3, {sa_family=AF_UNIX, sun_path="/var/lib/mysql/mysql.sock"}, 110) = -1 EACCES (Permission denied)
O socket está visível, mas as permissões do sistema (mysql:mysql contra o grupo do usuário) impedem a abertura do descritor de arquivo.
Cenário C: Falha real de credencial ou tabela de privilégios
connect(3, {sa_family=AF_UNIX, sun_path="/var/lib/mysql/mysql.sock"}, 110) = 0
write(3, "\24\0\0\0\n8.0.35\0...", 24) = 24
read(3, "G\0\0\2\377\25\004#28000Access denied for user 'user_db'@'localhost'...", 16384) = 75
A chamada connect retornou 0 (sucesso). O socket UNIX foi aberto e o kernel realizou a operação de I/O normalmente. Porém, o pacote retornado pelo MySQL contém o erro 28000Access denied. O bloqueio está integralmente nas tabelas internas do banco de dados.
Passo 2: Investigação do estado do MySQL e tabelas de privilégios#
Pelo terminal com privilégios administrativos, consultamos os registros de usuários correspondentes:
mysql -u root -e "SELECT User, Host, plugin, authentication_string FROM mysql.user WHERE User LIKE 'user\_%';"
Se a coluna plugin estiver definida como auth_socket ou unix_socket em vez de caching_sha2_password ou mysql_native_password, o MySQL ignora a senha informada no código PHP e tenta validar se o UID do processo no Linux confere exatamente com o nome do usuário do banco, quebrando conexões normais de CMSs.
4. Cenários de borda e como descartá-los#
Cenário de borda 1: Exaustão de conexões mascarada como falha de autenticação#
Quando uma conta atinge o limite configurado de conexões simultâneas (MAX_USER_CONNECTIONS), algumas versões de drivers do PHP fecham a conexão abruptamente e reportam erro de autenticação.
Para checar os limites globais e as conexões ativas por usuário:
mysql -u root -e "SHOW GLOBAL VARIABLES LIKE 'max_user_connections';"
mysql -u root -e "SELECT USER, COUNT(*) FROM INFORMATION_SCHEMA.PROCESSLIST GROUP BY USER;"
Cenário de borda 2: Diferenciação de maiúsculas e minúsculas com lower_case_table_names#
Se o arquivo global /etc/my.cnf foi atualizado e o parâmetro lower_case_table_names mudou de valor (por exemplo, de 0 para 1), o MySQL altera o cálculo de hashes e comparações de identificadores. Usuários ou bancos criados com letras maiúsculas podem perder o mapeamento de privilégios.
Para verificar o valor atual:
mysqladmin variables | grep lower_case_table_names
5. Decomposição de comandos e onde o Bash costuma pregar peças#
Durante incidentes sob pressão, é comum tentar listar domínios e testar requisições em uma única linha no terminal. No entanto, pequenos descuidos na sintaxe do Bash produzem resultados caóticos.
Vamos analisar duas construções que frequentemente falham:
# Exemplo 1 com erro de sintaxe comum
USERS=$(grep conected /etc/trueuserowners | awk -F: '{print $1}') && grep -ri $USERS /etc/trueuserdomains
# Exemplo 2 com erro de aspas e loop quebrado
grep -E "$(grep ': conected' /etc/trueuserowners | cut -d: -f1 | tr '\n' '|' | sed 's/|$//')" /etc/trueuserdomains | awk -F: '{print $1}' && for CHECKSITE in $(echo '\n$DOMAINS); do curl -sklI $CHECKSITE | grep HTTP; done
Por que a sintaxe original falhou#
- Expansão de variável sem aspas (word splitting): No primeiro comando, a variável
USERScontém vários nomes de usuários separados por quebras de linha (\n). Ao chamar$USERSsem aspas no comando seguinte, o interpretador quebra a lista em múltiplos argumentos. O comando executado passa a ser:
grep -ri user1 user2 user3 /etc/trueuserdomains
Para o binário do grep, o primeiro argumento (user1) é o termo de busca, enquanto todos os outros (user2, user3) são tratados como caminhos de arquivos onde buscar. O resultado é uma enxurrada de mensagens de erro No such file or directory.
- Flag de recursão em arquivo simples: O arquivo
/etc/trueuserdomainsé um arquivo de texto plano. Usar-r(recursivo) faz o utilitário esperar diretórios para varrer, o que não faz sentido para um arquivo estático.
- Aspas simples abertas sem fechamento: No segundo comando, a expressão
$(echo '\n$DOMAINS)usa aspas simples que impedem a expansão da variável e deixam a string sem fechamento, travando o analisador do Bash antes mesmo do loop iniciar.
6. Scripts defensivos para diagnóstico e recuperação em lote#
Aqui estão dois scripts desenvolvidos para resolver o problema com segurança e sem alterar senhas para valores aleatórios, o que quebraria os sites dos clientes.
Script 1: Verificação rápida de status HTTP para identificar sites fora do ar#
Este comando extrai os domínios da revenda afetada e executa uma consulta HTTP HEAD leve para apontar quais sites estão retornando erro 500:
DOMAINS=$(grep -E "$({ grep ': conected' /etc/trueuserowners || echo "NENHUM_OWNER_ENCONTRADO"; } | cut -d: -f1 | tr '\n' '|' | sed 's/|$//')" /etc/trueuserdomains | awk -F: '{print $1}') && for site in $DOMAINS; do printf "%-40s : " "$site"; STATUS=$(curl -Is --connect-timeout 4 --max-time 6 -o /dev/null -w "%{http_code}" "http://$site"); if [ "$STATUS" -eq 200 ]; then echo -e "\e[32m$STATUS OK\e[0m"; else echo -e "\e[31m$STATUS ERRO\e[0m"; fi; done
O comando funciona porque:
grep -E "$(... | tr '\n' '|' | sed 's/|$//')"transforma a lista vertical de usuários em uma expressão regular no padrãouser1|user2|user3. Osedremove a barra vertical final para evitar que uma string vazia case com todas as linhas do arquivo/etc/trueuserdomains.-w "%{http_code}"instrui ocurla imprimir somente o código de resposta HTTP, descartando o corpo da requisição com-o /dev/nullpara manter a saída limpa no terminal.
Script 2: Extração automática de credenciais e sincronização de privilégios#
Este script lê a senha configurada no arquivo da aplicação (wp-config.php ou configuration.php), descobre o usuário do banco e utiliza as rotinas do cPanel para atualizar os hashes no MySQL sem alterar as senhas dos sites.
#!/bin/bash
# ==============================================================================
# Sincronização em lote de privilégios MySQL para revendas no cPanel
# ==============================================================================
TARGET_OWNER="conected"
TRUEUSEROWNERS_FILE="/etc/trueuserowners"
TRUEUSERDOMAINS_FILE="/etc/trueuserdomains"
# Validação de privilégios de execução
if [ "$EUID" -ne 0 ]; then
echo "[-] Erro: Este script precisa ser executado como root."
exit 1
fi
echo "[+] Coletando contas vinculadas ao revendedor: $TARGET_OWNER"
USERS=$(grep -E ": $TARGET_OWNER$" "$TRUEUSEROWNERS_FILE" | cut -d: -f1)
if [ -z "$USERS" ]; then
echo "[-] Nenhum usuário encontrado para o revendedor especificado."
exit 0
fi
echo "[+] Iniciando processamento em massa..."
echo "------------------------------------------------------------------------"
for user in $USERS; do
# Obtém dinamicamente o diretório home via getent para evitar caminhos fixos
HOMEDIR=$(getent passwd "$user" | cut -d: -f6)
if [ ! -d "$HOMEDIR" ]; then
echo "[-] [USUARIO: $user] Diretório home não encontrado: $HOMEDIR"
continue
fi
# Arquivos padrão de configuração dos principais CMSs
CONFIG_PATHS=(
"$HOMEDIR/public_html/wp-config.php"
"$HOMEDIR/public_html/configuration.php"
)
for config in "${CONFIG_PATHS[@]}"; do
if [ -f "$config" ]; then
echo "[*] [USUARIO: $user] Analisando configuração: $config"
# Extração de usuário e senha via expressão regular com fallback de aspas
DB_USER=$(grep -E "DB_USER|dbuser" "$config" | head -n 1 | awk -F"'" '{print $4}' || true)
if [ -z "$DB_USER" ]; then
DB_USER=$(grep -E "DB_USER|dbuser" "$config" | head -n 1 | awk -F'"' '{print $4}' || true)
fi
DB_PASS=$(grep -E "DB_PASSWORD|dbpass" "$config" | head -n 1 | awk -F"'" '{print $4}' || true)
if [ -z "$DB_PASS" ]; then
DB_PASS=$(grep -E "DB_PASSWORD|dbpass" "$config" | head -n 1 | awk -F'"' '{print $4}' || true)
fi
# Quando as credenciais são encontradas, injeta o hash via script oficial do cPanel
if [ -n "$DB_USER" ] && [ -n "$DB_PASS" ]; then
echo " -> Usuário do banco identificado: $DB_USER"
echo " -> Sincronizando hash de credenciais com o MySQL..."
/usr/local/cpanel/scripts/set_mysql_password --user="$DB_USER" --password="$DB_PASS" > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo " -> [SUCESSO] Credenciais sincronizadas com êxito."
else
echo " -> [FALHA] Não foi possível atualizar a senha para $DB_USER."
fi
fi
fi
done
done
echo "------------------------------------------------------------------------"
echo "[+] Atualizando caches e remontando namespaces..."
# Reconstrói os mapas internos de banco de dados do cPanel
if [ -x "/usr/local/cpanel/scripts/update_db_cache" ]; then
echo "[*] Atualizando cache de banco de dados do cPanel..."
/usr/local/cpanel/scripts/update_db_cache
fi
# Remonta os namespaces virtuais do CageFS
if command -v cagefsctl &> /dev/null; then
echo "[*] Atualizando esqueleto e remontando o CageFS..."
cagefsctl --force-update > /dev/null 2>&1
cagefsctl --remount-all > /dev/null 2>&1
echo "[+] CageFS sincronizado e remontado com sucesso."
fi
# Aplica o flush de privilégios no MySQL
echo "[*] Executando FLUSH PRIVILEGES no motor do banco de dados..."
mysql -e "FLUSH PRIVILEGES;"
echo "[+] Processo em lote concluído com sucesso."
7. Arquivos de log e rotinas preventivas para manter o ambiente estável#
Caminhos fundamentais para monitoramento e auditoria#
Para acompanhar o comportamento do banco de dados e diagnosticar falhas de comunicação futuras, estes são os principais registros a observar:
/var/log/mariadb/mariadb.logou/var/log/mysqld.log: registra conexões rejeitadas e erros do motor de dados, especialmente quando a diretivalog_warningsestá configurada com valor igual ou superior a2./usr/local/cpanel/logs/error_log: se a rotina/scripts/update_db_cachefalhar em segundo plano, os detalhes da falha em Perl serão gravados aqui./var/log/cagefs.log: documenta problemas de inicialização de namespaces, permissões de montagem bind e falhas de visibilidade no socket do MySQL.
Cuidados essenciais no dia a dia#
- Evite redefinir senhas manualmente sem ler o arquivo de configuração: trocar senhas diretamente pelo phpMyAdmin ou WHM faz a aplicação parar de conectar até que os arquivos locais sejam editados um a um.
- Cheque o socket antes de culpar credenciais: quase metade das falhas que parecem de senha em servidores com CloudLinux são, na prática, descompassos de montagem do socket dentro do CageFS.
- Mantenha o cache do cPanel sincronizado: se fizer alterações manuais em permissões de banco pelo MySQL CLI, execute
/usr/local/cpanel/scripts/update_db_cacheem seguida para evitar que a interface do painel sobrescreva suas alterações na rotina diária de manutenção.
Este artigo foi útil?
Deixe uma reação rápida para apoiar o conteúdo:
Este post está licenciado sob CC BY-NC.



Comentários
Participe da discussão abaixo.
0 comentários