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:
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wild-cloud/wild-central/internal/authelia"
|
||||
"github.com/wild-cloud/wild-central/internal/certbot"
|
||||
"github.com/wild-cloud/wild-central/internal/config"
|
||||
"github.com/wild-cloud/wild-central/internal/dnsmasq"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
"github.com/wild-cloud/wild-central/internal/haproxy"
|
||||
"github.com/wild-cloud/wild-central/internal/network"
|
||||
"github.com/wild-cloud/wild-central/internal/sse"
|
||||
"github.com/wild-cloud/wild-central/internal/storage"
|
||||
)
|
||||
|
||||
// EnsureCentralDomain registers (or updates) Central's own domain as a domain
|
||||
@@ -96,13 +98,18 @@ func (api *API) reconcileNetworking() {
|
||||
case domains.BackendHTTP:
|
||||
var routeBackends []haproxy.HTTPRouteBackend
|
||||
for _, r := range dom.EffectiveRoutes() {
|
||||
routeBackends = append(routeBackends, haproxy.HTTPRouteBackend{
|
||||
rb := haproxy.HTTPRouteBackend{
|
||||
Paths: r.Paths,
|
||||
Backend: r.Backend.Address,
|
||||
HealthPath: r.Backend.Health,
|
||||
Headers: r.Headers,
|
||||
IPAllow: r.IPAllow,
|
||||
})
|
||||
}
|
||||
if dom.Auth != nil && dom.Auth.Enabled {
|
||||
rb.AuthEnabled = true
|
||||
rb.AuthPolicy = dom.Auth.Policy
|
||||
}
|
||||
routeBackends = append(routeBackends, rb)
|
||||
}
|
||||
httpRoutes = append(httpRoutes, haproxy.HTTPRoute{
|
||||
Name: dom.DomainName,
|
||||
@@ -143,6 +150,22 @@ func (api *API) reconcileNetworking() {
|
||||
HTTPRoutes: activeHTTPRoutes,
|
||||
CertsDir: certsDir,
|
||||
}
|
||||
|
||||
// Propagate Authelia forward-auth settings if enabled
|
||||
if globalCfg.Cloud.Authelia.Enabled && globalCfg.Cloud.Authelia.Domain != "" {
|
||||
genOpts.AuthEnabled = true
|
||||
genOpts.AuthBackend = "127.0.0.1:9091"
|
||||
genOpts.AuthDomain = globalCfg.Cloud.Authelia.Domain
|
||||
genOpts.AuthSessionDomain = authelia.SessionDomainFromAuthDomain(globalCfg.Cloud.Authelia.Domain)
|
||||
|
||||
// Regenerate Authelia config so session cookie domains stay in sync
|
||||
// with the set of auth-protected domains
|
||||
if err := api.generateAutheliaConfig(globalCfg); err != nil {
|
||||
slog.Warn("reconcile: failed to regenerate authelia config", "error", err)
|
||||
} else if api.authelia.UserCount() > 0 {
|
||||
_ = api.authelia.RestartService()
|
||||
}
|
||||
}
|
||||
haproxyCfg := api.haproxy.GenerateWithOpts(l4Routes, nil, genOpts)
|
||||
|
||||
if err := api.haproxy.WriteConfig(haproxyCfg); err != nil {
|
||||
@@ -224,6 +247,16 @@ func (api *API) reconcileNetworking() {
|
||||
})
|
||||
}
|
||||
|
||||
// Set addn-hosts path for DNS filtering
|
||||
if globalCfg.Cloud.DNSFilter.Enabled {
|
||||
hostsPath := api.dnsFilter.HostsFilePath()
|
||||
if storage.FileExists(hostsPath) {
|
||||
api.dnsmasq.SetFilterConfPath(hostsPath)
|
||||
}
|
||||
} else {
|
||||
api.dnsmasq.SetFilterConfPath("")
|
||||
}
|
||||
|
||||
if err := api.dnsmasq.UpdateConfig(globalCfg, dnsEntries, true); err != nil {
|
||||
slog.Error("reconcile: failed to update dnsmasq", "error", err)
|
||||
} else {
|
||||
@@ -359,6 +392,25 @@ func (api *API) broadcastHaproxyEvent(eventType string, message string) {
|
||||
api.sseManager.Broadcast(event)
|
||||
}
|
||||
|
||||
// broadcastAutheliaEvent broadcasts SSE events for Authelia status changes
|
||||
func (api *API) broadcastAutheliaEvent(eventType string, message string) {
|
||||
if api.sseManager == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &sse.Event{
|
||||
ID: fmt.Sprintf("authelia-%d", time.Now().UnixNano()),
|
||||
Type: eventType,
|
||||
InstanceName: "global",
|
||||
Timestamp: time.Now(),
|
||||
Data: map[string]any{
|
||||
"message": message,
|
||||
},
|
||||
}
|
||||
|
||||
api.sseManager.Broadcast(event)
|
||||
}
|
||||
|
||||
// broadcastCentralStatusEvent broadcasts SSE events for central status changes
|
||||
func (api *API) broadcastCentralStatusEvent(startTime time.Time) {
|
||||
if api.sseManager == nil {
|
||||
@@ -382,3 +434,22 @@ func (api *API) broadcastCentralStatusEvent(startTime time.Time) {
|
||||
|
||||
api.sseManager.Broadcast(event)
|
||||
}
|
||||
|
||||
// broadcastDNSFilterEvent broadcasts SSE events for DNS filter changes
|
||||
func (api *API) broadcastDNSFilterEvent(eventType string, message string) {
|
||||
if api.sseManager == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &sse.Event{
|
||||
ID: fmt.Sprintf("dns-filter-%d", time.Now().UnixNano()),
|
||||
Type: eventType,
|
||||
InstanceName: "global",
|
||||
Timestamp: time.Now(),
|
||||
Data: map[string]any{
|
||||
"message": message,
|
||||
},
|
||||
}
|
||||
|
||||
api.sseManager.Broadcast(event)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user