Pasta Public do GLPI: Configuração Segura para Produção

Porque é que o DocumentRoot do GLPI 10 tem de apontar para a pasta public: o que fica exposto quando não aponta, um virtual host endurecido no Apache e Nginx, um script de verificação e o checklist de produção da manutenção NexTool.

Se o DocumentRoot do seu GLPI aponta para a raiz da instalação e não para a pasta public, o ficheiro com a palavra-passe da sua base de dados está a um GET de distância. Desde o GLPI 10 esta é a decisão de segurança mais importante do deploy - e a que mais encontramos mal configurada em ambientes que chegam para manutenção, precisamente porque o sistema funciona perfeitamente mesmo estando mal configurado.

Porque é que o webroot errado passa despercebido

O GLPI 10 continua a abrir e a operar normalmente se apontar o servidor web para a raiz (/var/www/glpi) em vez de /var/www/glpi/public. Nada quebra no ecrã, ninguém abre um pedido, e por isso a falha sobrevive durante anos. Na integração em manutenção de parques GLPI de clientes, este é o achado silencioso mais comum: o ambiente responde bem, mas https://glpi.cliente.com/files/_log/php-errors.log devolve o registo da aplicação como uma descarga normal.

O detalhe que só aparece na operação: no Apache, a instalação antiga era parcialmente salva pelos ficheiros .htaccess espalhados dentro de config/, files/ e afins, que negavam o acesso. Isso dá uma falsa sensação de proteção. No Nginx esses .htaccess simplesmente não existem - o Nginx nunca os lê. Assim, migrar de Apache para Nginx arrastando o root errado transforma uma exposição parcial em total: files/, config/ e todo o código-fonte passam a ser conteúdo estático servido a qualquer um. A troca de servidor web é o momento clássico em que esta bomba rebenta.

O que fica exposto quando o DocumentRoot está errado

A pasta public existe para publicar apenas o encaminhador (index.php), o .htaccess e os ficheiros estáticos (CSS, JS, imagens). Todo o resto deve ficar fora do alcance do navegador. A tabela abaixo é a matriz de verificação que usamos: com public como webroot, todos os URL abaixo devem devolver 404.

CaminhoConteúdoImpacto se expostoEstado esperado
/config/config_db.phpAnfitrião, utilizador e palavra-passe da base de dadosFuga total da base de dados404
/files/_log/php-errors.log, sql-errors.logReconhecimento da aplicação e fuga de PII404
/files/_dumps/Dumps SQL gerados pelo GLPICópia descarregável da base de dados404
/files/_sessions/Ficheiros de sessão ativosSequestro de sessão e roubo de token CSRF404
/src/Código-fonte PHP da aplicaçãoMapa da aplicação para exploração404
/vendor/Dependências ComposerExploração de CVE conhecidas de bibliotecas404
/install/install.phpAssistente de instalaçãoReconfiguração e tomada do ambiente404 (remover após instalar)

Cuidado com o falso "seguro": com o PHP ativo, /config/config_db.php pode responder 200 com corpo vazio (o ficheiro apenas define uma classe, não imprime nada). Um 200 vazio não significa que está tudo bem - significa que o ficheiro está dentro do webroot. O que realmente foge como descarga são os não-PHP: files/_log/*.log, files/_dumps/*.sql e qualquer cópia de segurança esquecida na árvore. Por isso a régua é 404, não "página em branco".

Apache: virtual host endurecido

O DocumentRoot aponta para public, a listagem de diretórios está desligada e o HTTP simples é redirecionado para HTTPS. O FallbackResource substitui o encaminhamento do .htaccess sem exigir AllowOverride All:

# /etc/apache2/sites-available/glpi.conf
<VirtualHost *:80>
    ServerName glpi.suaempresa.pt
    # Nada de GLPI em HTTP simples
    Redirect permanent / https://glpi.suaempresa.pt/
</VirtualHost>

<VirtualHost *:443>
    ServerName glpi.suaempresa.pt
    DocumentRoot /var/www/glpi/public

    <Directory /var/www/glpi/public>
        Require all granted
        Options -Indexes +FollowSymLinks
        # Encaminha sem depender de AllowOverride All
        FallbackResource /index.php
    </Directory>

    # Nega qualquer ficheiro oculto (.git, .env, .htaccess)
    <FilesMatch "^\.">
        Require all denied
    </FilesMatch>

    # Cabeçalhos de segurança
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "SAMEORIGIN"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    SSLEngine On
    SSLCertificateFile /etc/ssl/certs/glpi.crt
    SSLCertificateKeyFile /etc/ssl/private/glpi.key
</VirtualHost>

Nginx: sem a rede de segurança do .htaccess

No Nginx não há .htaccess que o salve, por isso o root tem de ser public desde o início. Repare em duas linhas que quase sempre faltam: o try_files $uri =404; dentro do bloco PHP e o bloqueio explícito de ficheiros ocultos.

# /etc/nginx/sites-available/glpi.conf
server {
    listen 80;
    server_name glpi.suaempresa.pt;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name glpi.suaempresa.pt;
    root /var/www/glpi/public;
    index index.php;

    ssl_certificate     /etc/ssl/certs/glpi.crt;
    ssl_certificate_key /etc/ssl/private/glpi.key;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ \.php$ {
        # =404 impede a execução via PATH_INFO forjado
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # O Nginx não lê .htaccess: negue ficheiros ocultos à mão
    location ~ /\. {
        deny all;
    }
}

O erro comum aqui é copiar um bloco PHP genérico da internet sem o try_files $uri =404;. Sem ele, um pedido como /uploads/foto.jpg/x.php pode ser passado ao FPM via PATH_INFO e executado, abrindo caminho a RCE se o atacante conseguir gravar um ficheiro na árvore. Com o =404, o Nginx recusa antes de chegar ao PHP.

Verificação pós-deploy: o teste de 30 segundos

Depois de recarregar o servidor web, execute este script contra o domínio real. Cada linha tem de dizer OK. Qualquer FALHA é um caminho servido que não deveria existir:

#!/usr/bin/env bash
BASE="https://glpi.suaempresa.pt"

# Todos os URL abaixo TÊM de devolver 404 num webroot correto
for path in \
  /config/config_db.php \
  /src/ \
  /vendor/ \
  /files/_log/php-errors.log \
  /install/install.php ; do
  code=$(curl -sk -o /dev/null -w '%{http_code}' "${BASE}${path}")
  if [ "$code" = "404" ]; then
    echo "OK    ${path} -> ${code}"
  else
    echo "FALHA ${path} -> ${code} (EXPOSTO)"
  fi
done

Na manutenção fixamos este script no checklist de integração de cada cliente novo e voltamos a executá-lo após cada troca de servidor web, atualização ou mudança de proxy inverso - porque um webroot correto não é um estado permanente, é algo que uma migração descuidada reverte em segundos.

Checklist de produção

  1. DocumentRoot / root a apontar para .../public, nunca para a raiz da instalação.
  2. Remover install/install.php assim que a instalação/atualização terminar (o próprio GLPI mostra um aviso vermelho enquanto o ficheiro existir).
  3. Forçar HTTPS com um redirecionamento 301 da porta 80 e Strict-Transport-Security ativo.
  4. Endurecimento opcional: mover config/ e files/ para fora da árvore web através de GLPI_CONFIG_DIR e GLPI_VAR_DIR, deixando no webroot apenas public.
  5. Executar o script de verificação e exigir 404 em todos os caminhos.
  6. Conferir o painel de segurança do próprio GLPI (Configurar) após arrancar - assinala pendências como um install.php presente.

Ajustar o webroot leva dez minutos; descobrir que a palavra-passe da base de dados esteve descarregável durante três anos custa muito mais. Quer esteja a levantar um GLPI novo, a migrar de Apache para Nginx ou a herdar um ambiente sem histórico de configuração, a equipa de manutenção GLPI da NexTool valida o webroot, endurece o virtual host e executa a bateria de verificação como parte da integração. Fale connosco sobre manutenção de GLPI.


Este conteúdo foi produzido com auxílio de inteligência artificial e revisto pela equipa da Nextool Solutions.

Perguntas Frequentes

Porque o encaminhamento do GLPI 10 não depende do webroot para carregar os ecrãs: resolve os caminhos internamente. A aplicação abre e opera normalmente com o DocumentRoot na raiz da instalação, por isso nada quebra visualmente. O problema é silencioso: config/, files/ e o código-fonte passam a ser alcançáveis pelo navegador. Como não há sintoma, a falha sobrevive anos até alguém testar os URL certos.

Não. O .htaccess é um recurso exclusivo do Apache. O Nginx ignora por completo esses ficheiros, incluindo os que o GLPI espalha dentro de config/ e files/ para negar acesso. Por isso uma migração de Apache para Nginx que herda o root errado é perigosa: a proteção parcial que o Apache dava desaparece e a exposição torna-se total. No Nginx, apontar o root para public é obrigatório, não opcional.

Sim. Enquanto o install.php existir, o próprio GLPI mostra um aviso de segurança a vermelho, e o ficheiro permite reconfigurar o ambiente. Remova-o (rm public/install/install.php) ou renomeie-o logo após instalar ou atualizar. Nota: se o webroot estiver corretamente definido para public, o caminho /install/install.php já devolve 404, mas remover o ficheiro é a defesa em profundidade recomendada.

Sim, e é o endurecimento recomendado para ambientes sensíveis. O GLPI suporta as constantes GLPI_CONFIG_DIR e GLPI_VAR_DIR para apontar config/ e files/ para diretórios fora da árvore web (por exemplo /etc/glpi e /var/lib/glpi). Assim, mesmo que alguém erre o webroot no futuro, não há credencial nem registo dentro do caminho servido pelo servidor web.

Faça um curl para URL que nunca deveriam existir num webroot correto, como /config/config_db.php, /vendor/ e /files/_log/php-errors.log. Todos têm de devolver 404. Atenção ao falso positivo: o config_db.php pode responder 200 com corpo vazio se o PHP o executar, e isso já sinaliza um webroot errado. A régua é 404, não uma página em branco.

Precisa de ajuda?