diff --git a/server/health.go b/server/health.go new file mode 100644 index 00000000..bc758f00 --- /dev/null +++ b/server/health.go @@ -0,0 +1,35 @@ +package server + +import ( + "github.com/gofiber/fiber/v2" + log "github.com/sirupsen/logrus" +) + +// Healthz is an unauthenticated health check endpoint. It verifies the +// session storage backend and FreeIPA are reachable. Registered before the +// CSRF middleware so probes don't create sessions. +func (r *Router) Healthz(c *fiber.Ctx) error { + if _, err := r.storage.Get("healthz"); err != nil { + log.WithFields(log.Fields{ + "error": err, + }).Error("healthz: session storage check failed") + return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ + "status": "unavailable", + "storage": err.Error(), + }) + } + + if _, err := r.adminClient.Ping(); err != nil { + log.WithFields(log.Fields{ + "error": err, + }).Error("healthz: FreeIPA ping failed") + return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ + "status": "unavailable", + "freeipa": err.Error(), + }) + } + + return c.JSON(fiber.Map{ + "status": "ok", + }) +} diff --git a/server/router.go b/server/router.go index 6284177d..0087ca64 100644 --- a/server/router.go +++ b/server/router.go @@ -93,6 +93,9 @@ func RemoteIP(c *fiber.Ctx) string { } func (r *Router) SetupRoutes(app *fiber.App) { + // Health check (no auth, no CSRF/session - must be registered first) + app.Get("/healthz", r.Healthz) + // CSRF tokens stored in sessions app.Use(r.CSRF)