Rodando fastpanel e aaPanel no Podman (Rocky Linux): o pesadelo do UID mapping e soluções reais
Voltar para blog

Rodando fastpanel e aaPanel no Podman (Rocky Linux): o pesadelo do UID mapping e soluções reais

07/06/2026 · 7 min · Infraestrutura

Este é o relato técnico de uma daquelas jornadas que todo analista de infraestrutura "raiz" enfrenta: a tentativa de rodar um painel de controle tradicional dentro de containers sem comprometer a estabilidade do host.

Se você está tentando subir o Fastpanel ou aaPanel via Podman/Docker no Rocky Linux, este artigo vai te poupar algumas horas de "cabeçada" contra permissões de sistema de arquivos e mapeamento de UIDs.

1. O problema inicial: o "unsupported os" e a armadilha do fastpanel#

Tudo começou com a necessidade de subir um painel de controle em uma VM com Rocky Linux, mas mantendo o host limpo através do uso de containers. Ao tentar rodar o script de instalação do Fastpanel, o primeiro "bloqueio" surgiu: o script checa o arquivo /etc/os-release e recusa a instalação se a versão não estiver na lista branca exata deles (geralmente focada em Debian/Ubuntu ou CentOS puro).

A solução técnica: o truque do os-release com systemd#

A solução não foi forçar o script no Rocky, mas sim criar um ambiente contido que o instalador aceitasse. Decidi usar uma imagem do Ubuntu 22.04 com suporte nativo a Systemd. Isso é inegociável, pois painéis como Fastpanel dependem do systemctl para gerenciar o stack (Nginx, MariaDB, PHP-FPM).

# Subindo o container com Ubuntu para "ludibriar" o instalador
podman run -d \
  --name control-panel \
  --privileged \
  --tmpfs /tmp --tmpfs /run --tmpfs /run/lock \
  -v /sys/fs/cgroup:/sys/fs/cgroup:ro \
  -p 80:80 -p 443:443 -p 8888:8888 \
  docker.io/jrei/systemd-ubuntu:22.04

A flag --privileged e os mounts de /sys/fs/cgroup são essenciais para que o Systemd interno funcione corretamente sob o Podman.

2. O pesadelo das permissões: erros de UID e o innodb (ibdata1)#

Após contornar a barreira do SO, o próximo desafio foi a persistência de dados. Mapear os volumes do host para o container (/home/user/mysql -> /var/lib/mysql) resultou imediatamente no temido erro OS errno 13: Permission denied.

Ao analisar os logs do MariaDB/MySQL, percebi que o processo interno não conseguia nem ler nem escrever no arquivo ibdata1.

2.1 rotina de backup preventivo obrigatório#

Antes de aplicar qualquer alteração de permissão ou de UID, faça uma cópia de segurança completa do diretório de dados no host para evitar corrupção:

# Criar diretório seguro de backup datado
BACKUP_DIR="/root/panel-backup-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$BACKUP_DIR"

# Fazer backup compactado dos dados existentes
tar czf "$BACKUP_DIR/data-backup.tar.gz" /home/percio/data/

echo "Backup salvo com sucesso em: $BACKUP_DIR"

2.2 a causa raiz: rootless Podman e UID namespaces#

Em um ambiente rootless Podman, o usuário que você vê como root dentro do container é, na verdade, o seu usuário comum no host. No entanto, serviços como o MySQL rodam com um usuário específico (geralmente UID 106 dentro do container Ubuntu).

Quando você mapeia um volume, o Rocky Linux vê que o arquivo pertence ao seu usuário (ex: UID 1000), mas o UID 106 do container não tem permissão de escrita nesse mapeamento automático de subUIDs.

2.3 o fix definitivo via podman unshare#

Dar chmod 777 é abdicar da segurança e muitas vezes nem resolve por causa do SELinux. A solução correta é usar o namespace de usuários para ajustar a posse no host de forma que o container entenda:

# 1. Garanta que o usuário do host é o dono físico inicialmente
sudo chown -R percio:percio /home/percio/data

# 2. Use o unshare para traduzir o UID 106 (mysql) para o host
podman unshare chown -R 106:106 /home/percio/data/mysql

# 3. Ajuste para o usuário principal do painel (ex: UID 1000 interno)
podman unshare chown -R 1000:1000 /home/percio/data/config

Além disso, o sufixo :Z no volume é obrigatório no Rocky Linux: -v /home/percio/data/mysql:/var/lib/mysql:Z

Isso instrui o Podman a rotular o arquivo com o contexto de segurança correto do SELinux, permitindo que o processo do container acesse o disco.

2.4 verificação pós-UID mapping#

Para validar se as permissões de namespace foram aplicadas corretamente, execute os seguintes testes:

# 1. Verificar ownership dos arquivos dentro do container
podman exec -it control-panel ls -la /var/lib/mysql/

# 2. Verificar o UID do processo MariaDB/MySQL em execução no container
podman exec -it control-panel ps aux | grep mysql

# 3. Validar se há erros de permissão nos logs de inicialização do container
podman logs control-panel 2>&1 | grep -i "permission\|error"

3. Terceira camada: verificação do SELinux#

No Rocky Linux, o SELinux bloqueia acessos de containers a diretórios do host por padrão. O sufixo :Z altera o contexto para container_file_t.

Comandos de diagnóstico do SELinux:#

# 1. Verificar contexto de segurança atual dos volumes mapeados no host
ls -Z /home/percio/data/mysql/

# 2. Pesquisar logs de auditoria do SELinux em busca de bloqueios (AVC denials) recentes
sudo ausearch -m avc -ts recent | grep mysql

# 3. Verificar status geral do SELinux
getenforce

# 4. Se os contextos estiverem incorretos ou quebrados, restaure-os recursivamente
sudo restorecon -Rv /home/percio/data/

4. Quarta camada: validação do systemd e MariaDB no container#

Como os painéis utilizam o Systemd interno, devemos garantir que a pilha de inicialização e o banco de dados estão respondendo.

Verificar systemd e serviços no container:#

# 1. Validar se o Systemd está rodando como PID 1 (deve retornar "systemd" ou "init")
podman exec -it control-panel ps -p 1 -o comm=

# 2. Verificar o status geral do systemctl dentro do container
podman exec -it control-panel systemctl status

# 3. Listar todos os serviços ativos no container
podman exec -it control-panel systemctl list-units --type=service --state=running

Verificar MariaDB/MySQL:#

# 1. Verificar status do serviço MySQL
podman exec -it control-panel systemctl status mysql

# 2. Testar queries internas e conexões
podman exec -it control-panel mysql -u root -p"sua_senha" -e "SHOW DATABASES;"

5. Quinta camada: alternativas ao --privileged e hardening#

A flag --privileged deve ser descontinuada em produção. Para rodar o container de forma restrita e segura, aplique as seguintes técnicas:

5.1 usar capabilities específicas#

Adicione apenas os privilégios necessários de administrador do sistema e de rede:

podman run -d \
  --name control-panel \
  --cap-add SYS_ADMIN \
  --cap-add NET_ADMIN \
  --cap-add SYS_PTRACE \
  --security-opt label=type:container_runtime_t \
  --tmpfs /tmp --tmpfs /run --tmpfs /run/lock \
  -v /sys/fs/cgroup:/sys/fs/cgroup:ro \
  -v /home/percio/data:/data:Z \
  -p 80:80 -p 443:443 -p 8888:8888 \
  docker.io/jrei/systemd-ubuntu:22.04

5.2 usar rootful Podman (sudo)#

Executar o container como root no host (sudo podman) mantém o isolamento nativo de capabilities e perfis seccomp, sendo mais seguro do que rodar rootless com privilégios totais (--privileged).

5.3 hardening adicional (limites de recursos e read-only)#

Limite o consumo de hardware do painel para evitar ataques de negação de serviço (DoS) ou vazamento de recursos no host:

# Limitar memória a 2GB e CPUs a 2 núcleos
podman run -d \
  --name control-panel \
  --memory=2g \
  --cpus=2 \
  --read-only \
  --tmpfs /tmp --tmpfs /run --tmpfs /run/lock \
  -v /home/percio/data:/data:Z \
  ...

6. Sexta camada: backups e atualizações do container#

A resiliência de um stack baseado em containers depende da facilidade de reinstalação e atualização dos dados persistidos.

6.1 backups dos dados#

Além do backup físico no host, realize dumps lógicos dos bancos de dados internos do container:

# Gerar dump do banco de dados de dentro do container para o host
podman exec -it control-panel mysqldump -u root -p"sua_senha" --all-databases > /root/db-backup-$(date +%Y%m%d).sql

# Salvar o estado completo do container como uma nova imagem
podman commit control-panel panel-snapshot:$(date +%Y%m%d)
podman save panel-snapshot:$(date +%Y%m%d) -o /root/panel-snapshot-$(date +%Y%m%d).tar

6.2 ciclo de atualização segura#

Para atualizar o painel ou o SO base do container sem perder configurações:

# 1. Parar e remover o container atual
podman stop control-panel
podman rm control-panel

# 2. Puxar a imagem atualizada do registro
podman pull docker.io/jrei/systemd-ubuntu:22.04

# 3. Recriar o container mapeando novamente os volumes persistidos
podman run -d --name control-panel ... (volumes mapeados)

7. Sétima camada: auditoria de rede e firewall#

Validar portas expostas no container:#

# Listar mapeamento de portas ativas no Podman
podman port control-panel

# Testar conectividade local nas portas 80 e 8888
curl -I http://localhost:80
curl -k https://localhost:8888

Liberar e verificar firewall do host (Rocky Linux):#

# Verificar se as portas estão expostas no Firewalld do host
sudo firewall-cmd --list-all

# Adicionar regras permanentes para o painel
sudo firewall-cmd --zone=public --add-port=80/tcp --permanent
sudo firewall-cmd --zone=public --add-port=443/tcp --permanent
sudo firewall-cmd --zone=public --add-port=8888/tcp --permanent
sudo firewall-cmd --reload

8. Oitava camada: depuração de logs do container#

Quando ocorrerem travamentos ou falhas na interface web:

# Acompanhar logs em tempo real
podman logs -f control-panel

# Exibir as últimas 100 linhas com carimbo de data/hora (timestamps)
podman logs --tail 100 --timestamps control-panel

# Filtrar erros ou falhas específicas
podman logs control-panel 2>&1 | grep -iE "error|fail|warning"

9. O mistério da autenticação: CLI vs. web no fastpanel#

Neste ponto, o MySQL rodava, mas surgiu um paradoxo bizarro: eu resetava a senha do banco ou do painel via linha de comando (usando scripts internos como mogwai), recebia um retorno de "sucesso", mas ao tentar logar na interface web, recebia "Senha Incorreta".

Após auditoria forense nos arquivos do container, identifiquei três causas prováveis:

  1. SQLite Interno: O Fastpanel mantém as configurações de acesso em um SQLite (/etc/fastpanel/fastpanel.sqlite) que pode entrar em descompasso com o banco MariaDB se houve erro de escrita durante o bootstrap.
  2. Corrupção de Hash: O instalador, ao falhar inicialmente por causa das permissões de UID, populou as tabelas com seeds corrompidos.
  3. Locks de Sessão: O container não conseguia persistir os arquivos de sessão PHP devido ao mesmo problema de UID mapping, invalidando qualquer tentativa de login bem-sucedida.

10. Pivotando para o aaPanel: a alternativa modular#

Minha conclusão após 10 anos de infraestrutura: o Fastpanel é excelente em bare metal, mas suas dependências profundas do Systemd e do stack monolítico o tornam hostil para um isolamento limpo em containers.

Migrei o lab para o aaPanel. O aaPanel é modular e possui scripts de instalação que lidam muito melhor com ambientes Docker/Podman por não forçarem tanto a mão no controle do PID 1.

O cuidado crítico com a porta 22 (SSH)#

Cuidado ao seguir tutoriais genéricos. Muitos sugerem mapear -p 22:21 por erro de digitação. Nunca faça isso. Mapear a porta de FTP (21) do container para a porta 22 do seu Host vai derrubar seu acesso SSH remoto. Use mapeamentos seguros como: -p 2121:21 (FTP) e -p 8888:8888 (Painel).

O erro do 404 (security entrance)#

Ao acessar o IP pela primeira vez, o aaPanel costuma dar 404 Not Found. Isso não é um erro, é uma feature de segurança. Ele gera um token aleatório (ex: /8f2a1b) que deve ser concatenado à URL.

Como recuperar essa URL se você perder o log:

# Consultando o log de bootstrap
podman logs aapanel

# Ou chamando a CLI interna do aaPanel
podman exec -it aapanel bt 14

11. Conclusão e checklist de SRE#

Rodar painéis em containers no Rocky Linux exige entender a conversa entre o Host e o Guest através das camadas de User Namespaces e SELinux.

Checklist de sobrevivência para containers Podman:#


Tabela de correlação: sintomas vs. causas#

Causa RaizSintoma ComumDiagnóstico RápidoCorreção Recomendada
UID Mapping IncorretoMySQL OS errno 13 (Permission denied)podman logs indicando falhas de escritapodman unshare chown -R 106:106
Bloqueio SELinuxContainer não acessa volumes locaissudo ausearch -m avc -ts recentAdicionar :Z nos volumes / restorecon
Privilégios ExcessivosContainer rodando com --privilegedpodman inspect confirmando flag ativaSubstituir por --cap-add ou rootful
Falta de Sticky BitServiços temporários quebram no bootstrapls -ld /tmp (permissão diferente de 1777)chmod 1777 /tmp no container
Porta 22 ConflituosaPerda de acesso SSH ao host físicoSSH aponta para banner de login do FTPAjustar mapeamento de portas na criação

12. Script de diagnóstico automatizado (diagnose-podman-panel.sh)#

Utilize este script para auditar de forma automatizada a saúde das permissões, SELinux, portas, e status dos serviços do container no host:

#!/bin/bash
# diagnose-podman-panel.sh
# Script de diagnóstico para painéis de controle rodando no Podman (Rocky Linux).
# Deve ser executado como root ou usuário com acesso ao Podman.

set -euo pipefail

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0;37m'

log_info() { echo -e "[${GREEN}INFO${NC}] $1"; }
log_warn() { echo -e "[${YELLOW}WARN${NC}] $1"; }
log_error() { echo -e "[${RED}ERROR${NC}] $1"; }

CONTAINER_NAME="control-panel"
if [ $# -ge 1 ]; then
    CONTAINER_NAME="$1"
fi

log_info "Iniciando diagnóstico do container: $CONTAINER_NAME..."

# 1. Verificar se o container existe e está rodando
if ! podman ps -a --format "{{.Names}}" | grep -qw "$CONTAINER_NAME"; then
    log_error "Container '$CONTAINER_NAME' não encontrado."
    exit 1
fi

CONTAINER_STATUS=$(podman inspect --format "{{.State.Status}}" "$CONTAINER_NAME")
log_info "Status do Container: $CONTAINER_STATUS"

# 2. Verificar Privilégios (Privileged check)
IS_PRIVILEGED=$(podman inspect --format "{{.HostConfig.Privileged}}" "$CONTAINER_NAME")
if [ "$IS_PRIVILEGED" = "true" ]; then
    log_warn "O container está rodando com privilégios totais (--privileged=true). Recomenda-se migrar para capabilities específicas."
else
    log_info "Container rodando sem privilégios totais (Seguro)."
fi

# 3. Auditoria de Volumes e SELinux
log_info "--- Auditoria de Volumes e SELinux ---"
VOLUMES=$(podman inspect --format "{{range .Mounts}}{{.Source}}:{{.Destination}} {{end}}" "$CONTAINER_NAME")
for vol in $VOLUMES; do
    src=$(echo "$vol" | cut -d':' -f1)
    dest=$(echo "$vol" | cut -d':' -f2)
    if [ -d "$src" ] || [ -f "$src" ]; then
        context=$(ls -Zd "$src" | awk '{print $4}' 2>/dev/null || ls -Zd "$src" | awk '{print $1}')
        log_info "Volume: $src -> $dest | Contexto SELinux: $context"
        if [[ "$context" != *"container_file_t"* ]]; then
            log_warn "O volume '$src' não está rotulado como container_file_t. Adicione o sufixo :Z ou rode restorecon."
        fi
    else
        log_error "Diretório de origem do volume não encontrado no host: $src"
    fi
done

# 4. Status do SELinux no Host
if command -v getenforce >/dev/null 2>&1; then
    log_info "Status do SELinux no host: $(getenforce)"
fi

# 5. Diagnóstico de Portas e Firewall
log_info "--- Conectividade e Portas ---"
podman port "$CONTAINER_NAME" || log_warn "Nenhuma porta exposta encontrada."

if command -v firewall-cmd >/dev/null 2>&1; then
    log_info "Portas abertas no Firewalld:"
    firewall-cmd --list-ports || true
fi

# 6. Diagnóstico do Systemd e MariaDB no Container
if [ "$CONTAINER_STATUS" = "running" ]; then
    log_info "--- Validação Interna do Container ---"
    
    # PID 1 Check
    PID1=$(podman exec "$CONTAINER_NAME" ps -p 1 -o comm= 2>/dev/null || echo "Erro")
    log_info "PID 1 do Container: $PID1"
    
    # Systemd Status
    if podman exec "$CONTAINER_NAME" systemctl is-system-running >/dev/null 2>&1 || true; then
        systemd_status=$(podman exec "$CONTAINER_NAME" systemctl is-system-running 2>/dev/null || echo "Desconhecido")
        log_info "Status do Systemd no container: $systemd_status"
    fi
    
    # MariaDB Status
    if podman exec "$CONTAINER_NAME" systemctl status mysql >/dev/null 2>&1; then
        log_info "MariaDB/MySQL: Rodando (OK)"
    elif podman exec "$CONTAINER_NAME" systemctl status mariadb >/dev/null 2>&1; then
        log_info "MariaDB/MySQL: Rodando (OK)"
    else
        log_error "Serviço MariaDB/MySQL inativo ou falhando dentro do container."
    fi
else
    log_warn "Container inativo. Não é possível rodar diagnósticos internos."
fi

log_info "Diagnóstico concluído."

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