GLPI hardening rarely fails because of a zero-day. It fails because of the forgotten installer, the web root pointed at the wrong directory, and the session cookie missing the Secure flag behind the proxy. This guide is the checklist we apply while maintaining client environments: the commands, the virtual host, and the diagnostic queries we use to lock down the attack surface of a GLPI in production.
How a GLPI actually gets compromised
Across dozens of maintained environments, the pattern repeats: compromise almost never comes from an exotic core flaw. It comes from configuration. The four vectors we find most, in order, are: (1) install/install.php never removed after deployment; (2) the DocumentRoot pointing at the installation root instead of public/, which leaves files/_log/ and config backups servable as text; (3) default credentials (glpi/glpi) still active; and (4) TLS terminating at the reverse proxy with the backend on plain HTTP and the session cookie without Secure. None of these needs an exploit, just a curl. Start the audit by testing exactly this:
# Installer still live? (expected: 404)
curl -s -o /dev/null -w "%{http_code}\n" https://glpi.client.com/install/install.php
# Error log exposed? (expected: 403/404, NEVER 200 with content)
curl -s https://glpi.client.com/files/_log/php-errors.log | head
# Config backup leaking the database credentials? (expected: nothing)
curl -s https://glpi.client.com/config/config_db.php.bak | head
If any of these returns content, stop everything: the environment is exposed right now, and fixing it is priority zero.
1. Close the surface: web root at public/ and no installation leftovers
- Point the
DocumentRoot(orrooton Nginx) at/var/www/glpi/public, never at/var/www/glpi. Since GLPI 10, only thepublic/folder should be the web root. - Remove
install/install.phpafter finishing the install or the upgrade. The upgrade recreates that file: the common mistake is to update GLPI and forget to remove it again. - Make sure
config/,files/and any.bakor.gitstay out of the web server's reach.
2. Virtual host: correct root, headers, and only index.php running PHP
The GLPI 10 standard is to route everything through public/index.php and let no other .php execute directly. This is the server block we use as a baseline on Nginx:
# /etc/nginx/sites-available/glpi.conf
server {
listen 443 ssl;
http2 on;
server_name glpi.client.com;
root /var/www/glpi/public; # never /var/www/glpi
index index.php;
ssl_certificate /etc/letsencrypt/live/glpi.client.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/glpi.client.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
try_files $uri /index.php$is_args$args;
}
# Only index.php routes PHP
location ~ ^/index\.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
# Any other .php: deny
location ~ \.php$ {
return 404;
}
}
On Apache the equivalent is DocumentRoot /var/www/glpi/public with a restricted <Directory> block and the same headers via Header set. A field note: only enable Strict-Transport-Security (HSTS) after the certificate is fully working. With HSTS on and a broken TLS you lock the entire domain in your users' browsers for the whole max-age.
3. Session and cookies: the silent leak behind the proxy
This is the finding that most often goes unnoticed. When TLS terminates at the proxy (Nginx/HAProxy) and GLPI runs over HTTP on the backend, the session cookie must still be marked Secure and HttpOnly. Under Setup > General, on the security tab, enforce secure cookies and a password policy (minimum length, complexity, expiration). Without it, the session cookie travels on the internal HTTP hop without Secure, and capturing traffic on that segment is enough to hijack a logged-in technician's session. Also set the session lifetime to something matching the environment's risk instead of the long default.
4. Accounts and authentication
Immediately change the passwords for glpi, tech, normal and post-only, or disable the ones you do not use. Run this diagnostic straight against the database to map what still depends on a local password (authtype = 1) and is a candidate for MFA/SSO or deactivation:
-- Default GLPI accounts still active
SELECT name, is_active, last_login
FROM glpi_users
WHERE name IN ('glpi','tech','normal','post-only');
-- All accounts with an active LOCAL password (authtype = 1)
-- authtype: 1=local 2=mail 3=LDAP 4=external 5=CAS
SELECT id, name, is_active, last_login
FROM glpi_users
WHERE authtype = 1 AND is_active = 1
ORDER BY last_login;
The recommendation for production: SSO (Azure AD/Entra ID or another provider) with MFA, and local login restricted to a single break-glass emergency account with a strong password kept in a vault. GLPI core has no native MFA nor lockout by attempts: for MFA use a suitable plugin or the SSO itself; for brute force, put GLPI behind fail2ban reading the proxy access log. See also our guide to SSO with Azure AD/Entra ID.
5. Permissions: read-only code, write only where needed
The principle is simple: the PHP-FPM user (www-data) must read the code but never write it. The only writable tree is GLPI_VAR_DIR. That way, even an application flaw cannot persist malicious code in the served directory.
| Directory | Role | Owner:group | www-data write |
|---|---|---|---|
| /var/www/glpi | Code + public/ web root | root:www-data | No (750 / 640) |
| /etc/glpi | Config (config_db.php) | root:www-data | No (750 / 640) |
| /var/lib/glpi | GLPI_VAR_DIR (files, cache, sessions) | www-data:www-data | Yes (750) |
| /var/log/glpi | Application logs | www-data:www-data | Yes (750) |
Splitting config/ and files/ out of the web root (via GLPI_CONFIG_DIR and GLPI_VAR_DIR) is the layout we adopt by default: if the web root is ever exposed by mistake, config and data do not go with it.
6. Updates and continuous monitoring
Keep the version current. Most GLPI CVEs already have a patch by the time they make the news; the real risk is the environment stuck three versions behind. Monitor authentication attempts (spikes in failed logins suggest brute force) in Zabbix/Grafana and track the CVEs for your installed version. For that we use the CVE Scan module, which cross-references the GLPI version against known CVEs and audits environment points. And do not treat hardening without a recovery plan: see the backup and disaster recovery guide.
Common mistakes we see in maintenance
- Config backup in the web root: someone saves
config_db.php.bakbefore editing; since.bakis not executed by PHP, the server hands the database credentials over as plain text. - Legacy alias surviving: the new vhost points at
public/, but an oldAlias /glpistill exposes the root. - HSTS before the certificate: enabling HSTS with an incomplete TLS locks the domain in users' browsers.
- Local login disabled without a break-glass: local login is cut relying 100% on SSO and, when SSO goes down, nobody can log in as admin.
Doing this once is easy; keeping it across dozens of client environments, with recurring upgrades recreating install/, is maintenance work. That is exactly what NexTool does: NexTool's GLPI maintenance validates the web root, hardens the virtual host, reviews permissions, and runs this verification battery as part of the maintenance cycle.
Este conteúdo foi produzido com auxílio de inteligência artificial e revisado pela equipe Nextool Solutions.