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.
| Ruta | Contenido | Impacto si se expone | Estado esperado |
|---|---|---|---|
/config/config_db.php | Host, usuario y contraseña de la base de datos | Fuga total de la base de datos | 404 |
/files/_log/ | php-errors.log, sql-errors.log | Reconocimiento de la app y fuga de PII | 404 |
/files/_dumps/ | Volcados SQL generados por GLPI | Copia descargable de la base de datos | 404 |
/files/_sessions/ | Archivos de sesión activos | Secuestro de sesión y robo de token CSRF | 404 |
/src/ | Código fuente PHP de la aplicación | Mapa de la aplicación para explotación | 404 |
/vendor/ | Dependencias de Composer | Explotación de CVE conocidos de librerías | 404 |
/install/install.php | Asistente de instalación | Reconfiguración y toma del entorno | 404 (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
- DocumentRoot / root apuntando a
.../public, nunca a la raíz de la instalación. - Eliminar
install/install.phpen cuanto termine la instalación/actualización (el propio GLPI muestra un aviso rojo mientras el archivo existe). - Forzar HTTPS con redirección 301 desde el puerto 80 y
Strict-Transport-Securityactivado. - Endurecimiento opcional: mover
config/yfiles/fuera del árbol web medianteGLPI_CONFIG_DIRyGLPI_VAR_DIR, dejando el webroot solo conpublic. - Ejecutar el script de verificación y exigir 404 en todas las rutas.
- Revisar el panel de seguridad del propio GLPI (Configurar) tras arrancar - señala pendientes como un
install.phppresente.
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.