Documentação
Como o dPanel funciona por dentro e quais são exatamente as regras.
Arquitetura
O dPanel é um plugin WHM escrito em Perl, usando o interpretador que o próprio cPanel
distribui. Não tem daemon, não tem banco, não tem dependência para instalar: o estado vive em
arquivos sob /var/cpanel/dpanel/, todos pertencentes ao root.
O acesso é restrito a root pelo AppConfig do WHM, e cada requisição revalida essa permissão no próprio programa: quem não tiver privilégio total não passa.
index.cgi entrega a página, e index.cgi?action=... responde JSON.
Nenhum comando passa por shell. Toda execução usa lista de argumentos, o que elimina a classe inteira de falhas de injeção via nome de container, domínio ou caminho.
Ciclo de vida do stack
| Etapa | O que acontece |
|---|---|
| Inspecionar | lê o arquivo do cliente sem seguir symlink, conferindo dono e tamanho; mostra o original e a versão normalizada lado a lado |
| Publicar | valida, normaliza e grava a cópia própria em
/var/cpanel/dpanel/stacks/{conta}/; registra portas e caminhos |
| Subir | reconfere os caminhos e o firewall, checa disco e roda
docker compose up -d em segundo plano |
| Operar | iniciar, parar, reiniciar, kill, ver log, por container ou pelo stack inteiro |
| Remover | derruba os containers preservando volumes; com remoção completa, apaga também a configuração e o acesso remoto ao MySQL |
O arquivo publicado é conferido por hash antes de cada subida. Se alguém editar a cópia do root por fora do painel, o dPanel recusa e pede uma republicação.
Validação do compose
Esse é o limite de segurança do produto: um arquivo escrito por usuário não confiável vai ser executado pelo root. A regra é lista de permissões: o que não está previsto é recusado, então uma chave nova do Docker nunca passa por engano.
Leitura do arquivo
- Aberto sem seguir symlink, conferindo que é arquivo comum e que o dono é a conta.
- Limite de 256 KB para o compose e 32 KB para o
.env. - O YAML é interpretado por um processo separado, rodando como usuário sem privilégio e com teto de memória, CPU e tempo. Interpretar YAML hostil é a operação mais arriscada do fluxo, e ela acontece fora do root.
- Recusa mais de um documento no arquivo, tags customizadas, aninhamento acima de 20 níveis e mais de 10 mil nós.
Chaves recusadas, sempre
| Chave | Motivo |
|---|---|
privileged | equivale a root no host |
cap_add | capabilities extras em servidor compartilhado |
network_mode | host coloca o container fora do firewall |
pid, ipc, uts, cgroup |
namespaces do host expõem processos e memória |
devices, device_cgroup_rules | acesso a dispositivos do host |
sysctls | ajuste de parâmetros de kernel |
volumes_from, links, external_links |
alcançam containers de outras contas |
userns_mode, cgroup_parent, group_add |
burlam o isolamento e os limites |
secrets, configs, label_file |
apontam para caminhos do host |
include, extends | carregam outros arquivos do disco |
deploy | chaves de swarm; os limites são definidos pelo dPanel |
dns, dns_search | o Docker já resolve; evita exfiltração dirigida |
security_opt que enfraqueça |
seccomp:unconfined e similares; só
no-new-privileges é aceito |
Outras restrições
- Máximo de 8 serviços por stack e 4 portas publicadas por serviço.
- Imagens só de registries conhecidos: Docker Hub, GHCR, Quay, GitLab, ECR público, Microsoft, LinuxServer, Kubernetes e GCR. Registry em porta não padrão é recusado.
- Volumes nomeados não podem usar
driver_opts: é bind mount disfarçado. - Redes não podem ser externas nem usar
driver_opts; todas são substituídas por uma rede isolada por conta. .envaceita sóCHAVE=valor, sem quebra de linha no valor. A substituição de variáveis é feita pelo próprio dPanel, usando apenas esse arquivo, nunca o ambiente do root.
image: ${IMG} não serve para esconder um valor proibido.
O que é injetado
O arquivo que vai para o Docker é sempre o normalizado pelo dPanel. Além de resolver caminhos e portas, ele acrescenta em cada serviço:
| Item | Valor | Para quê |
|---|---|---|
mem_limit / memswap_limit | 512m (teto 1024m) | evita consumir a RAM do servidor |
cpus | 0.5 (teto 1.0) | evita monopolizar a CPU |
pids_limit | 128 (teto 512) | barra fork bomb |
ulimits | nofile, nproc, core | segunda camada de contenção |
shm_size | 64m (teto 128m) | memória compartilhada conta como RAM |
init | true | recolhe processos zumbis |
security_opt | no-new-privileges | impede escalar privilégio dentro do container |
cap_drop / cap_add | tudo, e devolve um conjunto mínimo | menos capabilities que o padrão do Docker |
logging | json-file, 10m × 3 | log de container não enche o disco |
extra_hosts | host.docker.internal | acesso ao MySQL do servidor |
networks | rede isolada da conta | containers de contas diferentes não se enxergam |
restart | unless-stopped | always brigaria com o painel |
container_name é removido: o nome passa a ser derivado do projeto, o que torna
colisão entre contas impossível.
Portas
Você não escolhe a porta do host: o dPanel escolhe. Escreva só a porta interna:
ports: - "80" # recomendado - "8080:80" # aceito: usa 8080 se estiver livre na faixa
- Faixa reservada padrão: 20000 a 29999.
- Publicação sempre em
127.0.0.1. Se o arquivo pedir0.0.0.0, o dPanel corrige e avisa. - A loopback também é filtrada. Publicar em
127.0.0.1barra só quem vem de fora; a loopback é compartilhada por todas as contas do servidor. A cadeiaDPANEL_LOdeixa a porta aceitar apenas o nginx, o root e a própria conta dona do stack; o PHP de outra conta leva connection refused. - Uma porta pedida fora da faixa, ocupada no sistema ou pertencente a outra conta é trocada por uma livre, com aviso.
- Só TCP, sem intervalos de porta.
- As portas são estáveis: republicar um stack mantém a porta de cada serviço, justamente para não quebrar os redirecionamentos já configurados. Se ainda assim uma porta sumir, o painel avisa qual regra ficou órfã.
Volumes e caminhos
Bind mounts só podem apontar para dentro de /home/{conta}/. Caminhos relativos
são resolvidos a partir da pasta docker da conta.
- Recusa
..em qualquer posição, til, e caracteres de controle. - Resolve o caminho real e compara componente a componente:
/home/bobnunca passa como se fosse da contabo. - Cada componente do caminho precisa pertencer à conta ou ao root.
- Diretórios reservados do cPanel são bloqueados:
.ssh,.cpanel,mail,etc,logs,ssl, entre outros. - Montar o home inteiro não é permitido: amplo demais.
- O caminho gravado no arquivo publicado é o caminho real resolvido, não o que o cliente escreveu.
Volumes nomeados são permitidos, mas não chegam ao Docker como volume. O dPanel
materializa cada um como uma pasta em /home/{conta}/docker/volumes/{nome} e monta
como bind. O compose do cliente continua sendo o padrão que ele copiou da internet; o que muda
é onde o dado fica.
O motivo é que volume nomeado de verdade mora em /var/lib/docker/volumes/ —
fora do /home, ou seja fora do backup do cPanel, fora da transferência da conta, e
sobrevivendo à remoção dela como dado órfão. Dentro do home, some junto com a conta e entra no
backup.
- A pasta é criada com o uid da conta, nunca pelo root: se algum componente do caminho for um symlink para fora do home, a criação esbarra em permissão em vez de ser seguida.
- Depois de criada, ela passa pela mesma resolução de caminho de qualquer bind, e o dev/inode é reconferido no deploy.
- A seção
volumes:some do arquivo publicado — não sobra volume vazio. - Quota: a quota do cPanel é contada por dono. Sem
user:no serviço, o container grava como root e os arquivos não entram na quota da conta, mesmo estando no home. O painel avisa quando isso acontece.
Build com Dockerfile
Imagem construída na hora é permitida, com o contexto obrigatoriamente dentro do home da conta.
| Regra | Limite |
|---|---|
| Contexto | dentro de /home/{conta}/, resolvido pelo caminho real |
| Tamanho | até 200 MB e 20.000 arquivos |
| Dockerfile | relativo ao contexto; sem .. |
| Imagens base | seguem a mesma lista de registries permitidos |
| Recusado | # syntax= (baixa um interpretador externo),
--mount=type=ssh, --mount=type=secret,
--network=host |
| Chaves de build recusadas | network, secrets,
ssh, extra_hosts, privileged,
cache_from, additional_contexts |
Se preferir desligar build por completo no seu servidor, mude
allow_build na configuração.
Redirecionamentos
As regras são publicadas no nginx, que o ea-nginx coloca na frente do
Apache. Cada conta e domínio recebe um arquivo no ponto de extensão oficial do ea-nginx,
já incluído dentro do server{} daquele domínio e preservado por
ea-nginx config:
/etc/nginx/conf.d/users/{conta}/{domínio}/dpanel.conf as regras
/etc/nginx/conf.d/dpanel-maps.conf os maps (nível http)
/etc/nginx/conf.d/dpanel/proxy-params.conf os cabeçalhos
O {conta}.conf gerado pelo cPanel nunca é tocado.
MaxRequestWorkers e derruba todos os sites
do servidor. No nginx a mesma conexão custa dois descritores num event loop. Foi essa
mudança que motivou a v0.2.0, e por isso o ea-nginx passou a ser obrigatório.
Escopo: domínio inteiro ou só um caminho
- Domínio inteiro: vira um
locationpor expressão regular, porque um segundolocation /no mesmoserver{}faria o nginx recusar a configuração. Como regex vence prefixo simples, a lista de exclusão abaixo é obrigatória. - Só um caminho: vira
location ^~ /caminho/, que vence qualquer regex. O prefixo é recortado antes de chegar ao container:/app/xchega como/x. Acessar/appsem barra devolve um 301 para/app/.
O que nunca é encaminhado
Mesmo com o domínio inteiro apontado para o container, estes caminhos continuam no site do
cliente: /.well-known (senão o AutoSSL para de renovar certificado),
/cpanel, /webmail, /webdisk, /autodiscover,
/autoconfig, /cpcalendars, /cpcontacts,
/mail/config e /Microsoft-Server-ActiveSync.
O que a sua aplicação recebe
Para o container, toda requisição parece vir do gateway do Docker
(172.17.0.1 ou 172.18.0.1). Nunca use o endereço da conexão como IP
do visitante; ele vem nos cabeçalhos:
| Cabeçalho | Para que serve |
|---|---|
X-Real-IP | IP do visitante. É este que substitui o
REMOTE_ADDR. |
X-Forwarded-For | Cadeia de IPs; o primeiro é o visitante. |
X-Forwarded-Proto | http ou https. |
X-Forwarded-Host | Domínio que o visitante digitou. |
X-Forwarded-Port | Porta pública original, 80 ou 443. |
X-Forwarded-Server | Igual ao Host; mantido por compatibilidade com o formato do cPanel. |
Host | Preservado: a aplicação vê o domínio real. |
CF-Connecting-IPCF-Visitor | Só existem quando o domínio está atrás da Cloudflare. |
UpgradeConnection | Fazem o WebSocket funcionar. Não precisa tratar. |
Em Express: app.set('trust proxy', true). Em PHP dentro do container:
$_SERVER['HTTP_X_REAL_IP']. Para resposta em streaming (SSE), responda com
X-Accel-Buffering: no; o nginx desliga o buffering só naquele endpoint.
mail., autodiscover., cpanel.. Encaminhar tudo
quebraria webmail e a autoconfiguração de e-mail. O dPanel casa só o domínio escolhido mais
a variante www; qualquer outro Host volta para o Apache.
Domínio adicional e estacionado
O vhost correto é descoberto a partir dos dados do próprio cPanel: domínio principal e subdomínio têm vhost próprio, adicional usa o subdomínio companheiro, estacionado usa o domínio em que está estacionado. Você escolhe o domínio na lista e o dPanel resolve o resto.
Aplicação segura
Antes de recarregar, o dPanel roda nginx -t. Se a configuração ficar inválida
ele restaura os arquivos anteriores, testa de novo e informa o erro: nada entra em vigor
quebrado. O reload do nginx é gracioso: nenhuma conexão em andamento cai. Includes de
VirtualHost deixados por versões 0.1.x são removidos na primeira aplicação.
Isolamento de rede
Containers ficam presos por cadeias próprias de iptables. O ponto que a maioria das receitas
erra: tráfego de container para o próprio host chega em INPUT, não em
FORWARD. Regra em DOCKER-USER sozinha não protege nada disso.
| Fluxo | Política |
|---|---|
| container → MySQL 3306 no gateway | permitido |
| container → qualquer outra porta do host | recusado |
| container → metadata da instância (169.254.0.0/16) | recusado |
| container → 10/8, 172.16/12, 192.168/16 | recusado |
| container → internet | permitido |
| container → container da mesma conta | permitido |
| host → container | permitido |
| internet → porta publicada | recusado |
As regras são reaplicadas em quatro momentos: quando o Docker inicia, depois de cada reconstrução do CSF, a cada 60 segundos por um timer do systemd, e antes de cada subida de stack: se não estiverem ativas, o deploy é recusado.
Para inspecionar ou reaplicar manualmente:
/usr/local/cpanel/whostmgr/docroot/cgi/dpanel/bin/dpanel-firewall status /usr/local/cpanel/whostmgr/docroot/cgi/dpanel/bin/dpanel-firewall apply
As redes do Docker ficam fixas em 172.17.0.0/16 e 172.18.0.0/16, o que mantém previsível tanto o firewall quanto a liberação do MySQL.
Limites de recurso
Containers rodam como processos do root, fora do controle de recursos por conta do CloudLinux. O plano de hospedagem do cliente não limita o container dele. Por isso o controle é feito em três camadas:
- Por serviço: memória, CPU e PIDs injetados no arquivo publicado.
- Por stack e global: o dPanel soma o que já está publicado e recusa uma publicação que estoure o teto do servidor.
- No kernel: um
dpanel.slicedo systemd limita todos os containers juntos. Mesmo com um erro de contabilidade no painel, o servidor não cai.
Vale dizer ao cliente: containers consomem de um pool administrado por você, não da conta dele.
Banco de dados
Dentro do container: host.docker.internal, porta 3306. É a única porta do
servidor acessível.
Como o cliente vem de um IP de rede Docker e não de localhost, o dPanel libera
o acesso remoto MySQL da conta para as faixas do Docker no primeiro deploy, usando a API do
próprio cPanel, o que mantém o painel do cliente coerente. Ao remover o stack por completo,
a liberação é desfeita.
Arquivos no servidor
| Caminho | Conteúdo |
|---|---|
/usr/local/cpanel/whostmgr/docroot/cgi/dpanel/ | o plugin |
/var/cpanel/dpanel/stacks/{conta}/ |
compose normalizado, .env, original, metadados e log do deploy |
/var/cpanel/dpanel/redirects.json | regras de domínio |
/var/cpanel/dpanel/ports.json | portas atribuídas |
/var/cpanel/dpanel/config.json | ajustes do servidor |
/var/cpanel/dpanel/logs/actions.log | auditoria em JSONL |
/etc/docker/daemon.json | configuração do daemon |
/etc/systemd/system/dpanel.slice | teto global de recursos |
/usr/local/csf/bin/csfpost.sh | reposição das regras após o CSF |
Toda a árvore de estado é do root, sem permissão para mais ninguém.
Configuração
Ajustes do servidor ficam em /var/cpanel/dpanel/config.json. Só as chaves que
você quiser mudar precisam estar lá; o resto usa o padrão.
{
"port_min": 20000,
"port_max": 29999,
"mem_default": "512m",
"mem_max": "1024m",
"cpus_default": 0.5,
"cpus_max": 1.0,
"stack_mem_max": "1536m",
"global_mem_max": "2048m",
"max_services": 8,
"allow_build": 1,
"build_context_max_mb": 200,
"min_free_disk_gb": 5,
"registries": ["docker.io", "ghcr.io", "quay.io"]
}
Mudanças valem na próxima ação do painel, sem reiniciar nada. Se mexer no teto
global, ajuste também o dpanel.slice, que é o limite real aplicado pelo kernel.
Auditoria
Cada ação vira uma linha JSON em /var/cpanel/dpanel/logs/actions.log, com
data, usuário do WHM, IP de origem, conta afetada, resultado e detalhe. A aba
Auditoria do painel mostra as últimas cem.
Eventos de segurança (recusa de validação, arquivo publicado alterado por fora, caminho montado que mudou) também vão para o syslog, para serem vistos por quem já monitora o servidor.
tail -f /var/cpanel/dpanel/logs/actions.log
A rotação é semanal, com 26 semanas de retenção.
Riscos conhecidos
Nenhum sistema é perfeito; estes são os pontos que ficaram em aberto e por quê.
| Risco | Situação |
|---|---|
| Corrida de symlink | O caminho real é resolvido na validação e reconferido, por identificador de arquivo, imediatamente antes de cada subida. Trocar o diretório por um link simbólico nessa janela de microssegundos é teoricamente possível. A troca é detectada e registrada; fechar de vez exige prender o arquivo por descritor, o que está planejado. |
| MySQL entre contas | Qualquer container pode tentar autenticar contra o banco de qualquer conta. Permissões e senhas são a barreira. |
| Conteúdo da imagem | O que a imagem faz por dentro é contido pelo isolamento, pelos limites e pelo firewall, mas o conteúdo dela não é auditado. |
| Espaço em disco | Não há teto por conta para imagens. Há guarda de espaço livre antes do deploy, aviso no painel e limpeza manual. |