ITSM β GLPI (ITIL v4)
:::note Status
π’ S001 + S002 live on dev (2026-10-04). GLPI is deployed, reachable internally at
https://itsm.10.0.0.200.nip.io (Tailscale) and publicly at https://itsm.devandre.sbs (Cloudflare
tunnel), gated by native Authentik OIDC (the glpi-singlesignon plugin β not forward-auth).
Detailed design (PRD, solution architecture, NFR register, threat model, ADRs, sprint plan) lives in the
ktayl-itsm repo (docs/); this page is the org-site map.
:::
The ktayl-solution IS runs its IT service management on GLPI (ITIL v4 β CMDB, incident/request
management, SLAs, helpdesk), board #16 (IS Foundations). It is the service-management system of
record; it is not project management (Plane) or metrics (Prometheus/Grafana). Delivered via BMAD
(Path B β adopt + integrate): the planning set + readiness gate (PASS) are in the ktayl-itsm repo.
As-built β what is live (S001)β
| Aspect | As deployed |
|---|---|
| Tool | GLPI 10.0.28 β a custom image built from ktayl-itsm (FROM glpi/glpi + minicloud CA, apache on 8080, non-root), pushed to Harbor (dev) + ghcr (prod, cosign-signed + SBOM) |
| Deploy | GAP wrapper chart minicloud-gitops/services/ktayl-itsm/helm/ + ArgoCD app apps/workloads/ktayl-itsm-dev.yaml, namespace itsm. GLPI + a dedicated MariaDB are both stateful workloads in the chart's templates/ (each with its own PVC) β the shared stateless minicloud-app-deployment library does not fit a PVC-backed app |
| URL | internal https://itsm.10.0.0.200.nip.io (Tailscale) + public https://itsm.devandre.sbs (Cloudflare tunnel) |
| Database | dedicated MariaDB 11.4 StatefulSet (GLPI requires MySQL/MariaDB β the one deviation from the CNPG/Postgres standard); GLPI auto-installs its schema on first boot |
| Auth | native Authentik OIDC via the glpi-singlesignon plugin (baked into the image) β GLPI's own login page offers Log in with Authentik; users auto-provision on first sign-in, keyed by email |
| Secrets | Vault secret/platform/ktayl-itsm (mariadb-root-password, db-password, oidc-client-id, oidc-client-secret) β ESO / provisioning script |
| Security | non-root, allowPrivilegeEscalation:false, drop:[ALL], seccomp RuntimeDefault, no SA-token automount; default-deny netpol (GLPI β MariaDB only); Trivy flags image CVEs to the GitHub Security tab (SARIF, nothing suppressed) |
As-built β SSO + public route (S002)β
| Aspect | As deployed |
|---|---|
| SSO model | GLPI's native OIDC (glpi-singlesignon v1.4.0, baked into the custom image) β Authentik. Not forward-auth: GLPI's own login page is the gate, so the public ingress carries no auth annotations |
| Authentik | provider+app glpi (confidential, authorization_code+refresh_token), scopes openid email profile (+ groups); redirect is a REGEX β¦/callback.php.* (the plugin appends a /provider/<id> path segment) |
| Provisioning | the provider row (endpoints + client creds) is upserted by the reusable minicloud-ops/scripts/glpi/configure-sso-provider.sh (creds read from Vault, never echoed) β GLPI stores SSO providers in its DB, not config |
| User mapping | use_email_for_login=1 β GLPI account keyed off the OIDC email; first sign-in auto-provisions the user. The owner is pre-provisioned Super-Admin |
| Public route | itsm.devandre.sbs via the k8s Cloudflare tunnel (manifests/cloudflare-tunnel/02-configmap.yaml, originServerName: itsm.10.0.0.200.nip.io) + ingress rule/TLS SAN for both hosts |
| Back-channel trust | GLPIβAuthentik token/userinfo over split-horizon DNS (auth.devandre.sbsβinternal ingress), trusted via the S001 runtime CA-trust initContainer (minicloud CA appended to the bundle) |
:::note group β GLPI profile
The glpi-singlesignon v1.4.0 plugin does not map the OIDC groups claim to GLPI profiles β it
auto-provisions every new user with GLPI's default profile. Profile elevation / role mapping is done
in GLPI itself (manual profile assignment, or GLPI authorization rules by email domain), not from
the claim. Claim-driven groupβprofile is a later story (plugin v2.x / a rules layer). The owner's account
is pre-provisioned Super-Admin so the ITSM is administrable over SSO from first login.
:::
As-built β employee ingestion via Authentik LDAP (S008)β
So IT can track who requested what, every employee exists in GLPI as a selectable requester β not only after their first SSO login. GLPI's native LDAP sync is pointed at the same Authentik LDAP outpost used for mail federation.
| Aspect | As deployed |
|---|---|
| Source of truth | Authentik, via the shared ktayl-ldap-outpost (ns authentik, ldap://β¦:3389, base DN dc=devandre,dc=sbs) β the same outpost the mail federation uses |
| Import filter | (&(objectClass=user)(memberOf=cn=Workplace Users,ou=groups,dc=devandre,dc=sbs)) β only real employees; cleanly excludes service accounts, the it shared mailbox and akadmin |
| Directory row | provisioned by the reusable minicloud-ops/scripts/glpi/configure-ldap-directory.sh β runs GLPI's AuthLDAP::add() in-pod so rootdn_passwd is encrypted with the instance key (a raw SQL insert can't); bind creds from Vault secret/platform/authentik-ldap-outpost, piped via stdin (never on an argv) |
| Identity mapping | login_field=cn (β glpi_users.name = matricule/username), email1_field=mail. Because the OIDC singlesignon plugin keys on email, an SSO login links to the LDAP-imported account by email β no duplicate |
| Profiles | imported users get GLPI's default Self-Service (requester) profile automatically β "everyone = requester" |
| Role mapping | an authorization rule (RuleRight, criterion memberof CONTAINS the group DN) maps the Direction IT / SI group β Technician profile, so IT staff can be assigned tickets (not just raise them). Provisioned by minicloud-ops/scripts/glpi/configure-ldap-profile-rule.sh, applied on sync. Additive β non-IT users keep Self-Service via GLPI's default-profile fallback |
| Ongoing sync | daily CronJob glpi-ldap-sync (30 2 * * *) runs glpi:ldap:synchronize_users inside the live pod via a least-privilege pods/exec SA β joiners imported, movers refreshed, leavers handled. Mirrors the nextcloud-mail-provisioner pattern |
| Connectivity | the outpost ingress netpol admits ns itsm; GLPI egress β authentik:3389/6636; the sync pod reaches the kube-apiserver via a CiliumNetworkPolicy toEntities: [kube-apiserver] (a plain ipBlock can't match the apiserver identity under Cilium) |
:::note Owner account reconciliation
The owner's GLPI account was historically seeded by OIDC as kanmegnea@gmail.com (Authentik's akadmin
carries that address). It was reconciled in place to login 100001 + primary email
kanmegnea@devandre.sbs, keeping Super-Admin β so LDAP matches it by cn=100001 and OIDC by the
org email, both resolving to the one account. The gmail address remains only on Authentik's break-glass
akadmin user.
:::
Operate / verifyβ
# employee ingestion (S008): provision/refresh the LDAP directory row (idempotent):
ssh controller "cd ~/minicloud-ops && bash scripts/glpi/configure-ldap-directory.sh itsm 'Authentik (ktayl employees)' platform/authentik-ldap-outpost"
# one-off import of new employees (the daily CronJob does this automatically at 02:30 UTC):
ssh controller "kubectl exec -n itsm \$(kubectl get pod -n itsm -l app=glpi -o jsonpath='{.items[0].metadata.name}') -c glpi -- php /var/www/glpi/bin/console glpi:ldap:synchronize_users -c -s 1 -n"
# run the sync CronJob on demand + watch it:
ssh controller "kubectl create job -n itsm glpi-ldap-sync-now --from=cronjob/glpi-ldap-sync && kubectl logs -n itsm -f job/glpi-ldap-sync-now"
# (re)provision the Direction IT / SI -> Technician authorization rule (idempotent), then sync to apply:
ssh controller "cd ~/minicloud-ops && bash scripts/glpi/configure-ldap-profile-rule.sh itsm 'LDAP: Direction IT / SI -> Technician' 'cn=Direction IT / SI,ou=groups,dc=devandre,dc=sbs' Technician"
# internal health (Tailscale + minicloud CA):
/usr/bin/curl --cacert ~/minicloud-ca.crt -sI https://itsm.10.0.0.200.nip.io/ # -> 200, "Authentication - GLPI"
# public edge (Cloudflare) + SSO redirect chain:
/usr/bin/curl -sI https://itsm.devandre.sbs/ # -> 200 (valid public TLS)
# the GLPI login page carries a "Log in with Authentik" link β
# /plugins/singlesignon/front/callback.php/provider/1 302s to auth.devandre.sbs/application/o/authorize/
# β Authentik renders "Log in to continue to GLPI ITSM"
ssh controller "kubectl get pods -n itsm" # glpi + glpi-mariadb
ssh controller "kubectl get application ktayl-itsm-dev -n argocd" # Synced / Healthy
# (re)provision the SSO provider row idempotently:
ssh controller "cd ~/minicloud-ops && bash scripts/glpi/configure-sso-provider.sh itsm Authentik https://auth.devandre.sbs platform/ktayl-itsm"
Deploy gotchas (captured β reusable for any apache/php vendor image on this hardened cluster)β
- File-capability binaries fail to exec under
no_new_privs. apache2 carriescap_net_bind_service; withallowPrivilegeEscalation:falsethe kernel refuses to exec it (EPERM, exit 126). Fix:setcap -rthe binary in the custom image and bind an unprivileged port (8080). - GLPI data lives at
/var/glpi/{config,files}, not/var/www/glpi/*β mount the PVCs there (wrong paths β config unpersisted β re-install crashloop). First-boot install needs a startupProbe. - SSO (S002): the
glpi-singlesignonv1.4.0 release asset isglpi-singlesignon-v1.4.0.tar.bz2(bzip2; the shortsinglesignon.tgzname is v2.x-only, which needs GLPI 11) and extracts toglpi-singlesignon/β rename tosinglesignon/(GLPI only discovers a plugin whose dir = its key). grant_typesdefaults to[]on a new Authentik 2026.x provider β the authorize view rejects the browser flow ("Invalid grant_type for provider"). A web OIDC app needsauthorization_code(+refresh_token); the sharedoidc-provider.pynow sets it (it previously left bookstack/glpi/plane login-broken β all three backfilled).- GLPI's
url_basedefaults tohttp://localhost/glpiand the plugin buildsredirect_urifrom it β seturl_baseto the public host (https://itsm.devandre.sbs) or OIDC redirects point at localhost. - The plugin appends
/provider/<id>to the callback β the Authentik redirect must be REGEX (β¦/callback.php.*), not STRICT, or Authentik returns invalid_request. - LDAP ingestion (S008): GLPI encrypts
rootdn_passwdwith its instance key β a raw SQL insert can't produce a bindable password; provision via GLPI's ownAuthLDAP::add()in-pod. A pod thatkubectl execs (the sync CronJob) from a default-deny-egress namespace needs aCiliumNetworkPolicytoEntities: [kube-apiserver]β a plainipBlockcan't match the apiserver identity under Cilium (the pod otherwise hangs on10.43.0.1:443).
Full detail + rationale: ktayl-itsm repo docs/ (deployment-architecture, ADRs) and the platform
memory project_ktayl_itsm_glpi.
Roadmap (board #16)β
| Story | Status |
|---|---|
| S001 β Deploy GLPI (wrapper chart + custom image) | β Done (dev) |
| S002 β Authentik OIDC SSO + public route | β Done (dev) β native OIDC, auto-provision, public route; claimβprofile deferred |
| S003 β Incident + request + SLA | β¬ |
| S004 β Minimal CMDB (BYOD-scoped) + seed | β¬ |
| S005 β Alertmanager β auto-ticket | β¬ |
| S006 β KPI dashboard (GLPI β Grafana) | β¬ |
| S007 β Self-service portal + KB | β¬ |
| S008 β Employee ingestion via Authentik LDAP (requesters + daily sync) | β
Done (dev) β 20 employees imported as requesters; daily glpi-ldap-sync CronJob |
Compliance mappingβ
ITIL v4 service management Β· DORA (ICT incident management + change governance β the ITSM record is evidence) Β· ACPR / Solvency II operational-risk + incident logging Β· GDPR (ticket-PII retention, handled in S003). Certification: BC03 (dΓ©ployer & sΓ©curiser).