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.
160 lines
5.9 KiB
Go
160 lines
5.9 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ExtraPort is a TCP or UDP port that the firewall should allow on the WAN interface.
|
|
type ExtraPort struct {
|
|
Port int `yaml:"port" json:"port"`
|
|
Protocol string `yaml:"protocol,omitempty" json:"protocol,omitempty"` // "tcp" (default) or "udp"
|
|
Label string `yaml:"label,omitempty" json:"label,omitempty"`
|
|
}
|
|
|
|
// UnmarshalYAML handles both plain integers (legacy: extraPorts: [22]) and structured entries
|
|
// (extraPorts: [{port: 22, protocol: tcp, label: SSH}]).
|
|
func (p *ExtraPort) UnmarshalYAML(value *yaml.Node) error {
|
|
if value.Kind == yaml.ScalarNode {
|
|
var port int
|
|
if err := value.Decode(&port); err != nil {
|
|
return fmt.Errorf("extraPort: expected integer, got %q", value.Value)
|
|
}
|
|
p.Port = port
|
|
return nil
|
|
}
|
|
type alias ExtraPort
|
|
return value.Decode((*alias)(p))
|
|
}
|
|
|
|
// UnmarshalJSON handles both plain integers (legacy: extraPorts: [22]) and structured entries.
|
|
func (p *ExtraPort) UnmarshalJSON(data []byte) error {
|
|
if len(data) > 0 && data[0] != '{' {
|
|
var port int
|
|
if err := json.Unmarshal(data, &port); err != nil {
|
|
return fmt.Errorf("extraPort: expected integer or object, got: %s", data)
|
|
}
|
|
p.Port = port
|
|
return nil
|
|
}
|
|
type alias ExtraPort
|
|
var a alias
|
|
if err := json.Unmarshal(data, &a); err != nil {
|
|
return err
|
|
}
|
|
*p = ExtraPort(a)
|
|
return nil
|
|
}
|
|
|
|
// SplitExtraPorts separates ExtraPorts into TCP and UDP slices.
|
|
// Ports with no Protocol or Protocol "tcp" are TCP; Protocol "udp" are UDP.
|
|
func SplitExtraPorts(ports []ExtraPort) (tcp []int, udp []int) {
|
|
for _, p := range ports {
|
|
if strings.EqualFold(p.Protocol, "udp") {
|
|
udp = append(udp, p.Port)
|
|
} else {
|
|
tcp = append(tcp, p.Port)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// HAProxyCustomRoute defines a custom TCP proxy rule for non-Wild Cloud services
|
|
type HAProxyCustomRoute struct {
|
|
Name string `yaml:"name" json:"name"`
|
|
Port int `yaml:"port" json:"port"`
|
|
Backend string `yaml:"backend" json:"backend"`
|
|
}
|
|
|
|
// DHCPStaticLease defines a static DHCP address assignment
|
|
type DHCPStaticLease struct {
|
|
MAC string `yaml:"mac" json:"mac"`
|
|
IP string `yaml:"ip" json:"ip"`
|
|
Hostname string `yaml:"hostname,omitempty" json:"hostname,omitempty"`
|
|
}
|
|
|
|
// State represents Wild Central's runtime state — operator settings, networking
|
|
// configuration, DDNS, DHCP, etc. Persisted in {dataDir}/state.yaml and mutated
|
|
// via the API. This is NOT boot-time config (which comes from env vars).
|
|
type State struct {
|
|
Operator struct {
|
|
Email string `yaml:"email,omitempty" json:"email,omitempty"`
|
|
} `yaml:"operator,omitempty" json:"operator,omitempty"`
|
|
Cloud struct {
|
|
Central struct {
|
|
Domain string `yaml:"domain,omitempty" json:"domain,omitempty"` // e.g. "central.payne.io"
|
|
} `yaml:"central,omitempty" json:"central,omitempty"`
|
|
Dnsmasq struct {
|
|
IP string `yaml:"ip,omitempty" json:"ip,omitempty"`
|
|
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
|
|
DHCP struct {
|
|
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
|
RangeStart string `yaml:"rangeStart,omitempty" json:"rangeStart,omitempty"`
|
|
RangeEnd string `yaml:"rangeEnd,omitempty" json:"rangeEnd,omitempty"`
|
|
LeaseTime string `yaml:"leaseTime,omitempty" json:"leaseTime,omitempty"`
|
|
Gateway string `yaml:"gateway,omitempty" json:"gateway,omitempty"`
|
|
StaticLeases []DHCPStaticLease `yaml:"staticLeases,omitempty" json:"staticLeases,omitempty"`
|
|
} `yaml:"dhcp,omitempty" json:"dhcp,omitempty"`
|
|
} `yaml:"dnsmasq,omitempty" json:"dnsmasq,omitempty"`
|
|
HAProxy struct {
|
|
CustomRoutes []HAProxyCustomRoute `yaml:"customRoutes,omitempty" json:"customRoutes,omitempty"`
|
|
} `yaml:"haproxy,omitempty" json:"haproxy,omitempty"`
|
|
Nftables struct {
|
|
Enabled *bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
|
WANInterface string `yaml:"wanInterface,omitempty" json:"wanInterface,omitempty"`
|
|
ExtraPorts []ExtraPort `yaml:"extraPorts,omitempty" json:"extraPorts,omitempty"`
|
|
} `yaml:"nftables,omitempty" json:"nftables,omitempty"`
|
|
DDNS struct {
|
|
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
|
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
|
|
IntervalMinutes int `yaml:"intervalMinutes,omitempty" json:"intervalMinutes,omitempty"`
|
|
} `yaml:"ddns,omitempty" json:"ddns,omitempty"`
|
|
Authelia struct {
|
|
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
|
Domain string `yaml:"domain,omitempty" json:"domain,omitempty"` // login portal domain (e.g. auth.payne.io)
|
|
DefaultPolicy string `yaml:"defaultPolicy,omitempty" json:"defaultPolicy,omitempty"` // one_factor or two_factor
|
|
} `yaml:"authelia,omitempty" json:"authelia,omitempty"`
|
|
DNSFilter struct {
|
|
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
|
IntervalHours int `yaml:"intervalHours,omitempty" json:"intervalHours,omitempty"` // update interval, default 24
|
|
} `yaml:"dnsFilter,omitempty" json:"dnsFilter,omitempty"`
|
|
} `yaml:"cloud,omitempty" json:"cloud,omitempty"`
|
|
}
|
|
|
|
// LoadState loads state from the specified path
|
|
func LoadState(configPath string) (*State, error) {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading config file %s: %w", configPath, err)
|
|
}
|
|
|
|
config := &State{}
|
|
if err := yaml.Unmarshal(data, config); err != nil {
|
|
return nil, fmt.Errorf("parsing config file: %w", err)
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// SaveState saves the state to the specified path
|
|
func SaveState(config *State, configPath string) error {
|
|
// Ensure the directory exists
|
|
if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil {
|
|
return fmt.Errorf("creating config directory: %w", err)
|
|
}
|
|
|
|
data, err := yaml.Marshal(config)
|
|
if err != nil {
|
|
return fmt.Errorf("marshaling config: %w", err)
|
|
}
|
|
|
|
return os.WriteFile(configPath, data, 0644)
|
|
}
|
|
|
|
|