Add Authelia as centralized authentication and OIDC provider

Authelia runs as a managed native service on Wild Central, providing
two integration patterns for network services:

- Forward-auth via HAProxy for apps without native SSO (Lua auth-request
  script intercepts requests, redirects unauthenticated users to login portal)
- OIDC provider for apps with native support (Gitea, Grafana, etc.)

Backend: new internal/authelia/ package with service manager, config
generation, file-based user management (argon2id), and OIDC client
management. API endpoints for status, config, users CRUD, and OIDC
clients CRUD.

HAProxy: config generator extended with lua-load, auth-request
directives, and Authelia backend. Auth directives scoped to session
cookie domain — only subdomains of the auth portal's parent are
eligible for forward-auth.

Frontend: Authentication page with enable/disable, user management,
OIDC client management (add/edit/delete), and protected domains
toggles. Advanced subsystem page for raw config view. Dashboard
service card and sidebar entries.
This commit is contained in:
2026-07-12 11:47:37 +00:00
parent 24bb976652
commit d796a79f79
27 changed files with 3097 additions and 11 deletions

View File

@@ -0,0 +1,156 @@
// Package authelia manages the Authelia authentication service.
// Authelia provides centralized authentication for network services via
// HAProxy forward-auth and acts as an OIDC provider for apps with native support.
package authelia
import (
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)
// Status represents the current state of the Authelia service
type Status struct {
Active bool `json:"active"`
Version string `json:"version,omitempty"`
PID int `json:"pid,omitempty"`
LastRestart time.Time `json:"lastRestart,omitempty"`
}
// Manager handles Authelia configuration and service lifecycle
type Manager struct {
dataDir string // e.g. /var/lib/wild-central/authelia
}
// NewManager creates a new Authelia manager
func NewManager(dataDir string) *Manager {
return &Manager{dataDir: dataDir}
}
// EnsureDataDir creates the Authelia data directory if it doesn't exist
func (m *Manager) EnsureDataDir() error {
if err := os.MkdirAll(m.dataDir, 0700); err != nil {
return fmt.Errorf("creating authelia data dir: %w", err)
}
return nil
}
// IsInstalled checks if the authelia binary exists on PATH
func (m *Manager) IsInstalled() bool {
_, err := exec.LookPath("authelia")
return err == nil
}
const luaScriptPath = "/etc/haproxy/lua/haproxy-auth-request.lua"
// HasLuaScript checks if the HAProxy auth-request Lua script is installed
func (m *Manager) HasLuaScript() bool {
info, err := os.Stat(luaScriptPath)
return err == nil && info.Size() > 0
}
// GetStatus returns the current Authelia service status
func (m *Manager) GetStatus() (*Status, error) {
status := &Status{}
cmd := exec.Command("systemctl", "is-active", "--quiet", "authelia")
status.Active = cmd.Run() == nil
if status.Active {
if out, err := exec.Command("systemctl", "show", "authelia.service", "--property=MainPID").Output(); err == nil {
parts := strings.Split(strings.TrimSpace(string(out)), "=")
if len(parts) == 2 {
if pid, err := strconv.Atoi(parts[1]); err == nil {
status.PID = pid
}
}
}
if out, err := exec.Command("systemctl", "show", "authelia.service", "--property=ActiveEnterTimestamp").Output(); err == nil {
parts := strings.Split(strings.TrimSpace(string(out)), "=")
if len(parts) == 2 {
if t, err := time.Parse("Mon 2006-01-02 15:04:05 MST", parts[1]); err == nil {
status.LastRestart = t
}
}
}
}
if out, err := exec.Command("authelia", "--version").Output(); err == nil {
s := string(out)
if i := strings.Index(s, "version "); i >= 0 {
v := s[i+8:]
if j := strings.IndexByte(v, ' '); j > 0 {
v = v[:j]
}
status.Version = strings.TrimSpace(v)
}
}
return status, nil
}
// RestartService restarts the Authelia systemd service
func (m *Manager) RestartService() error {
cmd := exec.Command("systemctl", "restart", "authelia.service")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to restart authelia: %w (output: %s)", err, string(output))
}
slog.Info("authelia service restarted", "component", "authelia")
return nil
}
// StopService stops the Authelia systemd service
func (m *Manager) StopService() error {
cmd := exec.Command("systemctl", "stop", "authelia.service")
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to stop authelia: %w (output: %s)", err, string(output))
}
slog.Info("authelia service stopped", "component", "authelia")
return nil
}
// ReadConfig reads the current Authelia configuration file
func (m *Manager) ReadConfig() (string, error) {
data, err := os.ReadFile(m.configPath())
if err != nil {
return "", fmt.Errorf("reading authelia config: %w", err)
}
return string(data), nil
}
// DataDir returns the Authelia data directory path
func (m *Manager) DataDir() string {
return m.dataDir
}
func (m *Manager) configPath() string {
return filepath.Join(m.dataDir, "configuration.yml")
}
func (m *Manager) usersDBPath() string {
return filepath.Join(m.dataDir, "users_database.yml")
}
func (m *Manager) sqlitePath() string {
return filepath.Join(m.dataDir, "db.sqlite3")
}
func (m *Manager) jwksPath() string {
return filepath.Join(m.dataDir, "jwks.pem")
}
func (m *Manager) notificationPath() string {
return filepath.Join(m.dataDir, "notification.txt")
}
func (m *Manager) oidcClientsPath() string {
return filepath.Join(m.dataDir, "oidc_clients.yml")
}