Carpeta Public de GLPI: Configuración Segura para Producción

Por qué el DocumentRoot de GLPI 10 debe apuntar a la carpeta public: qué queda expuesto cuando no lo hace, un virtual host endurecido en Apache y Nginx, un script de verificación y el checklist de producción del mantenimiento de NexTool.

Si el DocumentRoot de tu GLPI apunta a la raíz de la instalación y no a la carpeta public, el archivo con la contraseña de tu base de datos está a un GET de distancia. Desde GLPI 10 esta es la decisión de seguridad más importante del despliegue - y la que más encontramos mal configurada en entornos que llegan para mantenimiento, justamente porque el sistema funciona perfectamente aun estando mal configurado.

Por qué el webroot equivocado pasa desapercibido

GLPI 10 sigue abriendo y operando con normalidad si apuntas el servidor web a la raíz (/var/www/glpi) en lugar de /var/www/glpi/public. Nada se rompe en pantalla, nadie abre un ticket, y por eso el fallo sobrevive durante años. Al incorporar parques GLPI de clientes a mantenimiento, este es el hallazgo silencioso más común: el entorno responde bien, pero https://glpi.cliente.com/files/_log/php-errors.log devuelve el log de la aplicación como una descarga cualquiera.

El detalle que solo aparece en la operación: en Apache, la instalación antigua se salvaba en parte por los archivos .htaccess repartidos dentro de config/, files/ y similares, que denegaban el acceso. Eso da una falsa sensación de protección. En Nginx esos .htaccess simplemente no existen - Nginx nunca los lee. Así que migrar de Apache a Nginx arrastrando el root equivocado convierte una exposición parcial en total: files/, config/ y todo el código fuente pasan a ser contenido estático servido a cualquiera. El cambio de servidor web es el momento clásico en que esta bomba estalla.

Qué queda expuesto cuando el DocumentRoot está mal

La carpeta public existe para publicar solo el enrutador (index.php), el .htaccess y los archivos estáticos (CSS, JS, imágenes). Todo lo demás debe quedar fuera del alcance del navegador. La tabla siguiente es la matriz de verificación que usamos: con public como webroot, todas las URL de abajo deben devolver 404.

RutaContenidoImpacto si se exponeEstado esperado
/config/config_db.phpHost, usuario y contraseña de la base de datosFuga total de la base de datos404
/files/_log/php-errors.log, sql-errors.logReconocimiento de la app y fuga de PII404
/files/_dumps/Volcados SQL generados por GLPICopia descargable de la base de datos404
/files/_sessions/Archivos de sesión activosSecuestro de sesión y robo de token CSRF404
/src/Código fuente PHP de la aplicaciónMapa de la aplicación para explotación404
/vendor/Dependencias de ComposerExplotación de CVE conocidos de librerías404
/install/install.phpAsistente de instalaciónReconfiguración y toma del entorno404 (eliminar tras instalar)

Cuidado con el falso "seguro": con PHP activo, /config/config_db.php puede responder 200 con cuerpo vacío (el archivo solo define una clase, no imprime nada). Un 200 vacío no significa que esté todo bien - significa que el archivo está dentro del webroot. Lo que de verdad se filtra como descarga son los archivos no PHP: files/_log/*.log, files/_dumps/*.sql y cualquier copia de seguridad olvidada en el árbol. Por eso la regla es 404, no "página en blanco".

Apache: virtual host endurecido

El DocumentRoot apunta a public, el listado de directorios está apagado y el HTTP plano se redirige a HTTPS. FallbackResource sustituye el enrutamiento del .htaccess sin exigir AllowOverride All:

# /etc/apache2/sites-available/glpi.conf
<VirtualHost *:80>
    ServerName glpi.tuempresa.com
    # Nada de GLPI sobre HTTP plano
    Redirect permanent / https://glpi.tuempresa.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName glpi.tuempresa.com
    DocumentRoot /var/www/glpi/public

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

    # Deniega cualquier archivo oculto (.git, .env, .htaccess)
    <FilesMatch "^\.">
        Require all denied
    </FilesMatch>

    # Cabeceras de seguridad
    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: sin la red de seguridad del .htaccess

En Nginx no hay .htaccess que te salve, así que el root tiene que ser public desde el principio. Fíjate en dos líneas que casi siempre faltan: el try_files $uri =404; dentro del bloque PHP y el bloqueo explícito de archivos ocultos.

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

server {
    listen 443 ssl;
    http2 on;
    server_name glpi.tuempresa.com;
    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 impide la ejecución vía PATH_INFO falsificado
        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;
    }

    # Nginx ignora .htaccess: deniega archivos ocultos a mano
    location ~ /\. {
        deny all;
    }
}

El error común aquí es copiar un bloque PHP genérico de internet sin el try_files $uri =404;. Sin él, una petición como /uploads/foto.jpg/x.php puede pasarse a FPM vía PATH_INFO y ejecutarse, abriendo la puerta a RCE si el atacante logra escribir un archivo en el árbol. Con =404, Nginx rechaza antes de llegar a PHP.

Verificación tras el despliegue: la prueba de 30 segundos

Tras recargar el servidor web, ejecuta este script contra el dominio real. Cada línea debe decir OK. Cualquier FALLO es una ruta servida que no debería existir:

#!/usr/bin/env bash
BASE="https://glpi.tuempresa.com"

# Todas las URL de abajo DEBEN devolver 404 en un webroot correcto
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 "FALLO ${path} -> ${code} (EXPUESTO)"
  fi
done

En mantenimiento fijamos este script en el checklist de incorporación de cada cliente nuevo y lo volvemos a ejecutar tras cada cambio de servidor web, actualización o cambio de proxy inverso - porque un webroot correcto no es un estado permanente, es algo que una migración descuidada revierte en segundos.

Checklist de producción

  1. DocumentRoot / root apuntando a .../public, nunca a la raíz de la instalación.
  2. Eliminar install/install.php en cuanto termine la instalación/actualización (el propio GLPI muestra un aviso rojo mientras el archivo existe).
  3. Forzar HTTPS con redirección 301 desde el puerto 80 y Strict-Transport-Security activado.
  4. Endurecimiento opcional: mover config/ y files/ fuera del árbol web mediante GLPI_CONFIG_DIR y GLPI_VAR_DIR, dejando el webroot solo con public.
  5. Ejecutar el script de verificación y exigir 404 en todas las rutas.
  6. Revisar el panel de seguridad del propio GLPI (Configurar) tras arrancar - señala pendientes como un install.php presente.

Ajustar el webroot lleva diez minutos; descubrir que la contraseña de la base de datos estuvo descargable durante tres años cuesta mucho más. Tanto si estás levantando un GLPI nuevo, migrando de Apache a Nginx o heredando un entorno sin historial de configuración, el equipo de mantenimiento GLPI de NexTool valida el webroot, endurece el virtual host y ejecuta la batería de verificación como parte de la incorporación. Habla con nosotros sobre mantenimiento de GLPI.


Este contenido se produjo con ayuda de inteligencia artificial y fue revisado por el equipo de Nextool Solutions.

Preguntas Frecuentes

Porque el enrutamiento de GLPI 10 no depende del webroot para cargar sus pantallas: resuelve las rutas internamente. La aplicación abre y opera con normalidad con el DocumentRoot en la raíz de la instalación, así que nada se rompe visualmente. El problema es silencioso: config/, files/ y el código fuente quedan alcanzables desde el navegador. Como no hay síntoma, el fallo sobrevive años hasta que alguien prueba las URL correctas.

No. .htaccess es una función exclusiva de Apache. Nginx ignora por completo esos archivos, incluidos los que GLPI reparte dentro de config/ y files/ para denegar acceso. Por eso una migración de Apache a Nginx que hereda el root equivocado es peligrosa: la protección parcial que daba Apache desaparece y la exposición se vuelve total. En Nginx, apuntar el root a public es obligatorio, no opcional.

Sí. Mientras exista install.php, el propio GLPI muestra un aviso de seguridad en rojo, y el archivo permite reconfigurar el entorno. Elimínalo (rm public/install/install.php) o renómbralo justo tras instalar o actualizar. Nota: si el webroot está correctamente en public, la ruta /install/install.php ya devuelve 404, pero eliminar el archivo es la defensa en profundidad recomendada.

Sí, y es el endurecimiento recomendado para entornos sensibles. GLPI admite las constantes GLPI_CONFIG_DIR y GLPI_VAR_DIR para apuntar config/ y files/ a directorios fuera del árbol web (por ejemplo /etc/glpi y /var/lib/glpi). Así, aunque alguien equivoque el webroot en el futuro, no hay credencial ni log dentro de la ruta servida por el servidor web.

Haz curl a URL que nunca deberían existir en un webroot correcto, como /config/config_db.php, /vendor/ y /files/_log/php-errors.log. Todas deben devolver 404. Ojo con el falso positivo: config_db.php puede responder 200 con cuerpo vacío si PHP lo ejecuta, y eso ya indica un webroot equivocado. La regla es 404, no página en blanco.

?Necesitas ayuda?