Aller au contenu principal

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)​

AspectAs deployed
ToolGLPI 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)
DeployGAP 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
URLinternal https://itsm.10.0.0.200.nip.io (Tailscale) + public https://itsm.devandre.sbs (Cloudflare tunnel)
Databasededicated MariaDB 11.4 StatefulSet (GLPI requires MySQL/MariaDB β€” the one deviation from the CNPG/Postgres standard); GLPI auto-installs its schema on first boot
Authnative 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
SecretsVault secret/platform/ktayl-itsm (mariadb-root-password, db-password, oidc-client-id, oidc-client-secret) β†’ ESO / provisioning script
Securitynon-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)​

AspectAs deployed
SSO modelGLPI'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
Authentikprovider+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)
Provisioningthe 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 mappinguse_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 routeitsm.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 trustGLPI→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.

AspectAs deployed
Source of truthAuthentik, 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 rowprovisioned 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 mappinglogin_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
Profilesimported users get GLPI's default Self-Service (requester) profile automatically β€” "everyone = requester"
Role mappingan 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 syncdaily 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
Connectivitythe 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 carries cap_net_bind_service; with allowPrivilegeEscalation:false the kernel refuses to exec it (EPERM, exit 126). Fix: setcap -r the 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-singlesignon v1.4.0 release asset is glpi-singlesignon-v1.4.0.tar.bz2 (bzip2; the short singlesignon.tgz name is v2.x-only, which needs GLPI 11) and extracts to glpi-singlesignon/ β€” rename to singlesignon/ (GLPI only discovers a plugin whose dir = its key).
  • grant_types defaults to [] on a new Authentik 2026.x provider β†’ the authorize view rejects the browser flow ("Invalid grant_type for provider"). A web OIDC app needs authorization_code (+refresh_token); the shared oidc-provider.py now sets it (it previously left bookstack/glpi/plane login-broken β€” all three backfilled).
  • GLPI's url_base defaults to http://localhost/glpi and the plugin builds redirect_uri from it β†’ set url_base to 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_passwd with its instance key β†’ a raw SQL insert can't produce a bindable password; provision via GLPI's own AuthLDAP::add() in-pod. A pod that kubectl execs (the sync CronJob) from a default-deny-egress namespace needs a CiliumNetworkPolicy toEntities: [kube-apiserver] β€” a plain ipBlock can't match the apiserver identity under Cilium (the pod otherwise hangs on 10.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)​

StoryStatus
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).