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:
@@ -17,10 +17,12 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"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/crowdsec"
|
||||
"github.com/wild-cloud/wild-central/internal/ddns"
|
||||
"github.com/wild-cloud/wild-central/internal/dnsfilter"
|
||||
"github.com/wild-cloud/wild-central/internal/dnsmasq"
|
||||
"github.com/wild-cloud/wild-central/internal/haproxy"
|
||||
"github.com/wild-cloud/wild-central/internal/network"
|
||||
@@ -45,9 +47,12 @@ type API struct {
|
||||
crowdsec *crowdsec.Manager
|
||||
vpn *wireguard.Manager
|
||||
certbot *certbot.Manager
|
||||
domains *domains.Manager // Domain registration manager
|
||||
sseManager *sse.Manager // SSE manager for real-time events
|
||||
port int // Running API port (for config-driven routes)
|
||||
authelia *authelia.Manager // Authelia authentication service manager
|
||||
dnsFilter *dnsfilter.Manager // DNS filtering manager
|
||||
dnsFilterRunner *dnsfilter.Runner // DNS filter background update runner
|
||||
domains *domains.Manager // Domain registration manager
|
||||
sseManager *sse.Manager // SSE manager for real-time events
|
||||
port int // Running API port (for config-driven routes)
|
||||
}
|
||||
|
||||
// NewAPI creates a new Central API handler with all dependencies
|
||||
@@ -72,10 +77,18 @@ func NewAPI(dataDir, version string, allowedOrigins []string) (*API, error) {
|
||||
crowdsec: crowdsec.NewManager(),
|
||||
vpn: wireguard.NewManager(dataDir, vpnConfigPath),
|
||||
certbot: certbot.NewManager(""),
|
||||
authelia: authelia.NewManager(filepath.Join(dataDir, "authelia")),
|
||||
dnsFilter: dnsfilter.NewManager(filepath.Join(dataDir, "dns-filter")),
|
||||
domains: domains.NewManager(dataDir),
|
||||
sseManager: sseManager,
|
||||
}
|
||||
|
||||
// The compiled blocklist must be readable by dnsmasq which runs as its
|
||||
// own user. Use /var/lib/wild-central/dns-filter/ (world-readable) rather
|
||||
// than the data dir which may be under a user home with 0750 permissions.
|
||||
filterDir := envOrDefault("WILD_CENTRAL_FILTER_DIR", "/var/lib/wild-central/dns-filter")
|
||||
api.dnsFilter.SetHostsFilePath(filepath.Join(filterDir, "blocked.conf"))
|
||||
|
||||
// Wire up domain registration reconciliation: when domains change,
|
||||
// regenerate dnsmasq DNS entries and HAProxy routes.
|
||||
api.domains.SetReconcileFn(api.reconcileNetworking)
|
||||
@@ -110,6 +123,30 @@ func (api *API) StartDDNS(ctx gocontext.Context) {
|
||||
api.ddns.Start(ctx, api.ddnsParams)
|
||||
}
|
||||
|
||||
// StartDNSFilter launches the DNS filter background goroutine if enabled.
|
||||
func (api *API) StartDNSFilter(ctx gocontext.Context) {
|
||||
api.dnsFilterRunner = dnsfilter.NewRunner(api.dnsFilter, func() {
|
||||
if err := api.dnsmasq.ReloadService(); err != nil {
|
||||
slog.Warn("dns-filter: failed to reload dnsmasq after update", "error", err)
|
||||
}
|
||||
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter lists updated")
|
||||
})
|
||||
|
||||
state, err := config.LoadState(api.statePath())
|
||||
if err != nil || !state.Cloud.DNSFilter.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
_ = api.dnsFilter.EnsureDataDir()
|
||||
|
||||
interval := state.Cloud.DNSFilter.IntervalHours
|
||||
if interval <= 0 {
|
||||
interval = 24
|
||||
}
|
||||
|
||||
api.dnsFilterRunner.Start(ctx, interval)
|
||||
}
|
||||
|
||||
// ddnsParams returns the current DDNS parameters derived from runtime state.
|
||||
// Called by the DDNS runner on each tick — always returns fresh values.
|
||||
func (api *API) ddnsParams() ddns.Params {
|
||||
@@ -257,6 +294,35 @@ func (api *API) RegisterRoutes(r *mux.Router) {
|
||||
r.HandleFunc("/api/v1/cloudflare/zone", api.CloudflareGetZoneID).Methods("GET")
|
||||
r.HandleFunc("/api/v1/cloudflare/zone", api.CloudflareSetZoneID).Methods("POST")
|
||||
|
||||
// Authelia authentication management
|
||||
r.HandleFunc("/api/v1/authelia/status", api.AutheliaStatus).Methods("GET")
|
||||
r.HandleFunc("/api/v1/authelia/config", api.AutheliaGetConfig).Methods("GET")
|
||||
r.HandleFunc("/api/v1/authelia/config", api.AutheliaUpdateConfig).Methods("PUT")
|
||||
r.HandleFunc("/api/v1/authelia/restart", api.AutheliaRestart).Methods("POST")
|
||||
r.HandleFunc("/api/v1/authelia/users", api.AutheliaListUsers).Methods("GET")
|
||||
r.HandleFunc("/api/v1/authelia/users", api.AutheliaCreateUser).Methods("POST")
|
||||
r.HandleFunc("/api/v1/authelia/users/{username}", api.AutheliaUpdateUser).Methods("PUT")
|
||||
r.HandleFunc("/api/v1/authelia/users/{username}", api.AutheliaDeleteUser).Methods("DELETE")
|
||||
r.HandleFunc("/api/v1/authelia/oidc/clients", api.AutheliaListOIDCClients).Methods("GET")
|
||||
r.HandleFunc("/api/v1/authelia/oidc/clients", api.AutheliaCreateOIDCClient).Methods("POST")
|
||||
r.HandleFunc("/api/v1/authelia/oidc/clients/{clientId}", api.AutheliaUpdateOIDCClient).Methods("PUT")
|
||||
r.HandleFunc("/api/v1/authelia/oidc/clients/{clientId}", api.AutheliaDeleteOIDCClient).Methods("DELETE")
|
||||
|
||||
// DNS Filtering
|
||||
r.HandleFunc("/api/v1/dns-filter/status", api.DNSFilterStatus).Methods("GET")
|
||||
r.HandleFunc("/api/v1/dns-filter/config", api.DNSFilterGetConfig).Methods("GET")
|
||||
r.HandleFunc("/api/v1/dns-filter/config", api.DNSFilterUpdateConfig).Methods("PUT")
|
||||
r.HandleFunc("/api/v1/dns-filter/lists", api.DNSFilterGetLists).Methods("GET")
|
||||
r.HandleFunc("/api/v1/dns-filter/lists", api.DNSFilterAddList).Methods("POST")
|
||||
r.HandleFunc("/api/v1/dns-filter/lists/upload", api.DNSFilterUploadList).Methods("POST")
|
||||
r.HandleFunc("/api/v1/dns-filter/lists/suggested", api.DNSFilterGetSuggested).Methods("GET")
|
||||
r.HandleFunc("/api/v1/dns-filter/lists/{id}", api.DNSFilterRemoveList).Methods("DELETE")
|
||||
r.HandleFunc("/api/v1/dns-filter/lists/{id}/toggle", api.DNSFilterToggleList).Methods("PUT")
|
||||
r.HandleFunc("/api/v1/dns-filter/custom", api.DNSFilterGetCustomEntries).Methods("GET")
|
||||
r.HandleFunc("/api/v1/dns-filter/custom", api.DNSFilterSetCustomEntry).Methods("POST")
|
||||
r.HandleFunc("/api/v1/dns-filter/custom/{domain:.+}", api.DNSFilterRemoveCustomEntry).Methods("DELETE")
|
||||
r.HandleFunc("/api/v1/dns-filter/update", api.DNSFilterTriggerUpdate).Methods("POST")
|
||||
|
||||
// Domain registration — domain is the unique key
|
||||
r.HandleFunc("/api/v1/domains", api.DomainsListAll).Methods("GET")
|
||||
r.HandleFunc("/api/v1/domains", api.DomainsRegister).Methods("POST")
|
||||
@@ -386,6 +452,17 @@ func getDaemonStatus() map[string]map[string]any {
|
||||
return firstWord(out)
|
||||
},
|
||||
},
|
||||
"authelia": {
|
||||
unit: "authelia.service",
|
||||
verCmd: []string{"authelia", "--version"},
|
||||
verParse: func(out string) string {
|
||||
// "authelia version 4.x.x" or just "4.x.x"
|
||||
if after, found := strings.CutPrefix(out, "authelia version "); found {
|
||||
return firstWord(after)
|
||||
}
|
||||
return firstWord(out)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := make(map[string]map[string]any, len(daemons)+1)
|
||||
|
||||
Reference in New Issue
Block a user