Por dentro da trincheira: debugando uma quebra em cascata no aaPanel, Python 3.12 e Nginx
Voltar para blog

Por dentro da trincheira: debugando uma quebra em cascata no aaPanel, Python 3.12 e Nginx

30/09/2026 · 4 min · Servidores

A trincheira: quando painel, runtime e proxy quebram em cadeia#

A linha entre uma manutenção de rotina e um ambiente em colapso é tênue. Recentemente, encarei uma daquelas longas sessões de troubleshooting em um servidor de produção rodando Rocky Linux com aaPanel. O que começou como uma simples tentativa de restaurar o acesso ao painel de controle evoluiu para uma sequência de armadilhas envolvendo WSGI/Gevent, tipagem estrita no Python 3.12, conflitos de UID no Nginx e um loop de proxy reverso invisível.

Abaixo, documento o passo a passo da investigação, os erros encontrados no terminal e as decisões adotadas.

1. O ponto de partida: painel silencioso e o enigma do exit_group(0)#

O cenário inicial era de indisponibilidade total do painel web. As tentativas de resetar arquivos de controle e redefinir a porta via CLI não surtiam efeito:

touch /www/server/panel/data/setup.pl
touch /www/server/panel/data/install.pl
echo "8888" > /www/server/panel/data/port.pl
rm -f /www/server/panel/logs/panel.pid
fuser -k 8888/tcp
./pyenv/bin/python tools.py port 8888
# Retorno: ERROR: Parameter error

Ao tentar invocar o binário BT-Panel manualmente com o PYTHONPATH ajustado, o processo encerrava sem mensagens no terminal.

Rastreando com strace#

Para entender o comportamento do interpretador, executei o rastreio de chamadas de sistema:

cd /www/server/panel
export PYTHONPATH=$PYTHONPATH:/www/server/panel/class
strace -e trace=openat,write,exit_group ./pyenv/bin/python BT-Panel

A saída mostrou a carga de dependências (gevent, greenlet, flask), leitura do PID em /www/server/panel/logs/panel.pid e, em seguida:

exit_group(0)                           = ?
+++ exited with 0 +++

O processo não sofria um crash com SIGSEGV. Tratava-se de um encerramento lógico limpo: o BT-Panel implementa controle de instância única (Singleton). Se detecta travas de arquivo residuais, encerra imediatamente.

2. A camada de proxy interno e a recuperação do admin_path#

Ao reiniciar os serviços via bt 1, o comando ss confirmou a porta 8888 aberta:

ss -tulnp | grep 8888
# tcp LISTEN 0 511 0.0.0.0:8888 users:(("webserver",pid=251923,fd=7)...)

A porta não pertencia diretamente ao binário Python, mas sim ao Nginx interno do painel (webserver). Contudo, acessar a raiz pelo navegador retornava 404.

O teste via curl revelou a causa:

curl -I http://127.0.0.1:8888$(cat /www/server/panel/data/admin_path.pl)
# cat: /www/server/panel/data/admin_path.pl: No such file or directory
# HTTP/1.1 404 NOT FOUND

O arquivo com o caminho secreto (admin_path.pl) havia sido apagado. O aaPanel recusa requisições na raiz por segurança. Alterei a porta para 1606 e recriei o arquivo:

echo "/evolya" > /www/server/panel/data/admin_path.pl
bt 1

O login voltou a responder em http://IP_DO_SERVIDOR:1606/evolya.

3. Erro de desserialização no Python 3.12 (panelplugin.py)#

Ao autenticar no painel, a rota POST /v2/ajax?action=RepPanel gerou uma exceção:

Traceback (most recent call last):
  File "/home/www/server/panel/class/panelPlugin.py", line 1585, in getPluginList
    arr[i]['end'] = '--'
TypeError: 'str' object does not support item assignment

Causa raiz#

No Python 3.12, o tratamento de tipos é rigoroso. A rotina getPluginList esperava uma lista de dicionários com metadados de plugins. Por conta de cache truncado da API remota, itens da lista foram carregados como strings simples (str), quebrando a atribuição de chaves.

Adicionalmente, ao ajustar o ambiente virtual, o pacote correto a ser instalado para o módulo yaml é PyYAML:

./pyenv/bin/pip3 install PyYAML

4. O mistério do 403 forbidden e atributos imutáveis no Linux#

Com o painel restabelecido, os sites hospedados retornavam 403 Forbidden.

O atributo imutável (+i)#

Ao tentar ajustar as permissões do webroot, o sistema bloqueou:

chown -R www:www /home/user/public_html
# chown: changing ownership of '/home/user/public_html/.user.ini': Operation not permitted

O arquivo .user.ini estava com o bit de imutabilidade ativado (+i). Além disso, diretórios criados por ferramentas de IA continham links simbólicos que falhavam no chattr -R. A solução foi aplicar a remoção pontual no arquivo:

chattr -i /home/user/public_html/.user.ini
chown -R www:www /home/user/public_html
chmod 755 /home/user
chmod 755 /home/user/public_html

5. Cegueira de configuração e conflito de UID no Nginx#

Ao inspecionar a configuração carregada pelo Nginx em memória:

/usr/sbin/nginx -T | grep "server_name"
# server_name  _;

O Nginx do sistema estava lendo apenas /etc/nginx/nginx.conf, ignorando os Vhosts em /www/server/panel/vhost/nginx/. Além disso, o binário do sistema rodava sob o usuário nginx:nginx, enquanto os arquivos da aplicação pertenciam a www:www.

Correção de identidade e includes#

  1. Incluí o diretório de Vhosts no arquivo principal:
   sed -i '/include \/etc\/nginx\/conf.d\/\*\.conf;/a \    include /www/server/panel/vhost/nginx/*.conf;' /etc/nginx/nginx.conf
  1. Ajustei o usuário de execução para www:
   sed -i 's/^user  nginx;/user www www;/g' /etc/nginx/nginx.conf
  1. Testei a configuração:
   /usr/sbin/nginx -t

6. A causa raiz: o loop do multi-webserver hosting#

Ao subir o site, o arquivo /www/wwwlogs/domain.com.br.error.log registrou:

connect() failed (111: Connection refused) while connecting to upstream, client: IP, server: domain.com.br, request: "GET /index.html HTTP/1.1", upstream: "https://127.0.0.1:8290/index.html"

O servidor estava com a opção Multi-Webserver Hosting ativada. O Apache backend escutava na porta interna 8290, mas configurado em HTTP claro. O Vhost do Nginx estava configurado com proxy_pass https://127.0.0.1:8290; tentando negociar TLS com um socket HTTP comum, causando recusa imediata de conexão.

7. Protocolo de evacuação e reconstrução limpa#

Após mapear todas as camadas de falha (drift de binários do Python, desserialização de metadados, permissões corrompidas e loop de proxy reverso), manter o host com correções paliativas traria instabilidade futura.

Executei a evacuação completa de dados:

# 1. Backup de todos os bancos de dados
mkdir -p /backup_evacuation/db
for db in $(mysql -e "show databases;" -s --skip-column-names | grep -vE "performance_schema|information_schema|mysql|sys"); do
    mysqldump --opt --single-transaction "$db" > "/backup_evacuation/db/${db}.sql"
done

# 2. Empacotamento dos arquivos limpos
mkdir -p /backup_evacuation/www
tar -czvf /backup_evacuation/www/sites.tar.gz \
    --exclude='.user.ini' \
    --exclude='*.log' \
    --exclude='.agent' \
    --exclude='.claude' \
    --exclude='.cursor' \
    /www/wwwroot /home/user/public_html

# 3. Preservacao dos certificados SSL
cp -a /www/server/panel/vhost/cert /backup_evacuation/ssl_certs

Principais aprendizados do post-mortem#

  1. Prefira pilhas diretas: Manter uma arquitetura limpa com Nginx Standalone + PHP-FPM evita camadas desnecessárias de proxy loopback e falhas de negociação TLS em portas locais.
  2. Atenção aos UIDs: Binários de pacotes do sistema (nginx:nginx) costumam divergir dos usuários de painéis web (www:www). Alinhe a diretiva user no arquivo mestre do Nginx.
  3. Saiba a hora de reconstruir: Isolar a causa raiz é fundamental para o diagnóstico, mas reconhecer quando o ambiente sofreu desvios excessivos de configuração e reconstruir uma infraestrutura limpa é o que garante a estabilidade operacional.

Este artigo foi útil?

Deixe uma reação rápida para apoiar o conteúdo:

CC BY-NC

Este post está licenciado sob CC BY-NC.

Comentários

Participe da discussão abaixo.

0 comentários