GLPI Public Folder: Secure Configuration for Production

Why the GLPI 10 DocumentRoot must point at the public folder: what gets exposed when it does not, a hardened Apache and Nginx virtual host, a verification script and the NexTool maintenance production checklist.

If your GLPI DocumentRoot points at the install root instead of the public folder, the file holding your database password is one GET request away. Since GLPI 10 this is the single most important security decision of the deploy - and the one we most often find wrong in environments that reach us for maintenance, precisely because the system works perfectly even when it is misconfigured.

Why the wrong webroot goes unnoticed

GLPI 10 still opens and runs normally if you point the web server at the root (/var/www/glpi) instead of /var/www/glpi/public. Nothing breaks on screen, nobody files a ticket, and that is why the flaw survives for years. When onboarding client GLPI fleets for maintenance, this is the most common silent finding: the environment responds fine, but https://glpi.client.com/files/_log/php-errors.log hands back the application log as an ordinary download.

The detail that only shows up in operations: on Apache, the old install was partially saved by the .htaccess files scattered inside config/, files/ and similar folders, which denied access. That gives a false sense of protection. On Nginx those .htaccess files simply do not exist - Nginx never reads them. So migrating from Apache to Nginx while carrying the wrong root turns a partial exposure into a total one: files/, config/ and the entire source tree become static content served to anyone. Switching web servers is the classic moment when this bomb goes off.

What gets exposed when the DocumentRoot is wrong

The public folder exists to publish only the router (index.php), the .htaccess and the static files (CSS, JS, images). Everything else must stay out of the browser's reach. The table below is the verification matrix we use: with public as the webroot, every URL below must return 404.

PathContentsImpact if exposedExpected status
/config/config_db.phpDatabase host, user and passwordFull database leak404
/files/_log/php-errors.log, sql-errors.logApplication recon and PII leak404
/files/_dumps/SQL dumps produced by GLPIDownloadable copy of the database404
/files/_sessions/Active session filesSession hijacking and CSRF token theft404
/src/Application PHP source codeApplication map for exploitation404
/vendor/Composer dependenciesExploiting known library CVEs404
/install/install.phpInstallation wizardReconfiguration and environment takeover404 (remove after install)

Beware the false "safe": with PHP active, /config/config_db.php may answer 200 with an empty body (the file only defines a class, it prints nothing). An empty 200 does not mean you are fine - it means the file sits inside the webroot. What actually leaks as a download are the non-PHP files: files/_log/*.log, files/_dumps/*.sql and any backup forgotten in the tree. That is why the ruler is 404, not "blank page".

Apache: hardened virtual host

The DocumentRoot points at public, directory listing is off, and plain HTTP is redirected to HTTPS. FallbackResource replaces the .htaccess routing without requiring AllowOverride All:

# /etc/apache2/sites-available/glpi.conf
<VirtualHost *:80>
    ServerName glpi.yourcompany.com
    # No GLPI over plain HTTP
    Redirect permanent / https://glpi.yourcompany.com/
</VirtualHost>

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

    <Directory /var/www/glpi/public>
        Require all granted
        Options -Indexes +FollowSymLinks
        # Routes without depending on AllowOverride All
        FallbackResource /index.php
    </Directory>

    # Deny any hidden file (.git, .env, .htaccess)
    <FilesMatch "^\.">
        Require all denied
    </FilesMatch>

    # Security headers
    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: without the .htaccess safety net

On Nginx there is no .htaccess to save you, so the root has to be public from the start. Note two lines that are almost always missing: the try_files $uri =404; inside the PHP block and the explicit hidden-file block.

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

server {
    listen 443 ssl;
    http2 on;
    server_name glpi.yourcompany.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 blocks execution via forged PATH_INFO
        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 ignores .htaccess: deny hidden files by hand
    location ~ /\. {
        deny all;
    }
}

The common mistake here is copying a generic PHP block from the internet without try_files $uri =404;. Without it, a request like /uploads/photo.jpg/x.php can be passed to FPM via PATH_INFO and executed, opening the door to RCE if the attacker manages to write a file into the tree. With =404, Nginx refuses before reaching PHP.

Post-deploy verification: the 30-second test

After reloading the web server, run this script against the real domain. Every line must say OK. Any FAIL is a served path that should not exist:

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

# Every URL below MUST return 404 on a correct webroot
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 "FAIL ${path} -> ${code} (EXPOSED)"
  fi
done

In maintenance we pin this script to the onboarding checklist of every new client and run it again after each web-server swap, upgrade or reverse-proxy change - because a correct webroot is not a permanent state, it is something a careless migration reverts in seconds.

Production checklist

  1. DocumentRoot / root pointing at .../public, never at the install root.
  2. Remove install/install.php as soon as the install/upgrade finishes (GLPI itself shows a red warning while the file exists).
  3. Force HTTPS with a 301 redirect from port 80 and Strict-Transport-Security enabled.
  4. Optional hardening: move config/ and files/ out of the web tree via GLPI_CONFIG_DIR and GLPI_VAR_DIR, leaving the webroot with only public.
  5. Run the verification script and require 404 on every path.
  6. Check GLPI's own security panel (Setup) after startup - it flags pending items such as a present install.php.

Fixing the webroot takes ten minutes; finding out that the database password was downloadable for three years costs far more. Whether you are standing up a fresh GLPI, migrating from Apache to Nginx, or inheriting an environment with no config history, the NexTool GLPI maintenance team validates the webroot, hardens the virtual host and runs the verification battery as part of onboarding. Talk to us about GLPI maintenance.


This content was produced with the help of artificial intelligence and reviewed by the Nextool Solutions team.

Frequently Asked Questions

Because GLPI 10 routing does not depend on the webroot to load its screens: it resolves paths internally. The application opens and runs normally with the DocumentRoot at the install root, so nothing breaks visually. The problem is silent: config/, files/ and the source code become reachable from the browser. Since there is no symptom, the flaw survives for years until someone tests the right URLs.

No. .htaccess is an Apache-only feature. Nginx completely ignores those files, including the ones GLPI scatters inside config/ and files/ to deny access. That is why an Apache-to-Nginx migration inheriting the wrong root is dangerous: the partial protection Apache gave disappears and the exposure becomes total. On Nginx, pointing the root at public is mandatory, not optional.

Yes. As long as install.php exists, GLPI itself shows a red security warning, and the file lets someone reconfigure the environment. Remove it (rm public/install/install.php) or rename it right after installing or upgrading. Note: if the webroot is correctly set to public, the path /install/install.php already returns 404 - but removing the file is the recommended defense in depth.

Yes, and it is the recommended hardening for sensitive environments. GLPI supports the GLPI_CONFIG_DIR and GLPI_VAR_DIR constants to point config/ and files/ at directories outside the web tree (for example /etc/glpi and /var/lib/glpi). That way, even if someone gets the webroot wrong in the future, there is no credential or log inside the path served by the web server.

curl the URLs that should never exist on a correct webroot, such as /config/config_db.php, /vendor/ and /files/_log/php-errors.log. All must return 404. Watch out for the false positive: config_db.php may answer 200 with an empty body if PHP executes it - that alone signals a wrong webroot. The ruler is 404, not a blank page.

Need help?