diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index 88e12cee46..0b3afcba9f 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -32,12 +32,18 @@ postgresql: # host: replica1.example.com listen: - http: 0.0.0.0:9000 - https: 0.0.0.0:9443 - ldap: 0.0.0.0:3389 - ldaps: 0.0.0.0:6636 - radius: 0.0.0.0:1812 - metrics: 0.0.0.0:9300 + http: + - "[::]:9000" + https: + - "[::]:9443" + ldap: + - "[::]:3389" + ldaps: + - "[::]:6636" + radius: + - "[::]:1812" + metrics: + - "[::]:9300" debug: 0.0.0.0:9900 debug_py: 0.0.0.0:9901 trusted_proxy_cidrs: diff --git a/authentik/tasks/middleware.py b/authentik/tasks/middleware.py index 97cb77c748..2197164c97 100644 --- a/authentik/tasks/middleware.py +++ b/authentik/tasks/middleware.py @@ -224,7 +224,10 @@ class WorkerHealthcheckMiddleware(Middleware): thread: HTTPServerThread | None def __init__(self): - host, _, port = CONFIG.get("listen.http").rpartition(":") + listen = CONFIG.get("listen.http", ["[::]:9000"]) + if isinstance(listen, str): + listen = listen.split(",") + host, _, port = listen[0].rpartition(":") try: port = int(port) @@ -323,7 +326,10 @@ class MetricsMiddleware(BaseMetricsMiddleware): return [] def after_worker_boot(self, broker: Broker, worker: Worker): - addr, _, port = CONFIG.get("listen.metrics").rpartition(":") + listen = CONFIG.get("listen.metrics", ["[::]:9300"]) + if isinstance(listen, str): + listen = listen.split(",") + addr, _, port = listen[0].rpartition(":") try: port = int(port) diff --git a/cmd/server/healthcheck.go b/cmd/server/healthcheck.go index 8651aff503..2022197f61 100644 --- a/cmd/server/healthcheck.go +++ b/cmd/server/healthcheck.go @@ -1,7 +1,9 @@ package main import ( + "context" "fmt" + "net" "net/http" "os" "path" @@ -12,7 +14,8 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "goauthentik.io/internal/config" - "goauthentik.io/internal/utils/web" + utils "goauthentik.io/internal/utils/web" + "goauthentik.io/internal/web" ) var workerPidFile = path.Join(os.TempDir(), "authentik-worker.pid") @@ -44,9 +47,15 @@ func init() { func checkServer() int { h := &http.Client{ - Transport: web.NewUserAgentTransport("goauthentik.io/healthcheck", http.DefaultTransport), + Transport: utils.NewUserAgentTransport("goauthentik.io/healthcheck", + &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", path.Join(os.TempDir(), web.SocketName)) + }, + }, + ), } - url := fmt.Sprintf("http://%s%s-/health/live/", config.Get().Listen.HTTP, config.Get().Web.Path) + url := fmt.Sprintf("http://localhost%s-/health/live/", config.Get().Web.Path) res, err := h.Head(url) if err != nil { log.WithError(err).Warning("failed to send healthcheck request") diff --git a/cmd/server/server.go b/cmd/server/server.go index ea5ce45ecd..042d55951d 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/url" + "os" "time" "github.com/getsentry/sentry-go" @@ -51,9 +52,10 @@ var rootCmd = &cobra.Command{ ex := common.Init() defer common.Defer() - u, err := url.Parse(fmt.Sprintf("http://%s%s", config.Get().Listen.HTTP, config.Get().Web.Path)) - if err != nil { - panic(err) + u := url.URL{ + Scheme: "unix", + Host: fmt.Sprintf("%s/%s", os.TempDir(), web.SocketName), + Path: config.Get().Web.Path, } ws := web.NewWebServer() @@ -70,13 +72,13 @@ var rootCmd = &cobra.Command{ }, } -func attemptProxyStart(ws *web.WebServer, u *url.URL) { +func attemptProxyStart(ws *web.WebServer, u url.URL) { maxTries := 100 attempt := 0 l := log.WithField("logger", "authentik.server") for { l.Debug("attempting to init outpost") - ac := ak.NewAPIController(*u, config.Get().SecretKey) + ac := ak.NewAPIController(u, config.Get().SecretKey) if ac == nil { attempt += 1 time.Sleep(1 * time.Second) diff --git a/internal/config/struct.go b/internal/config/struct.go index 8ab9fc55d6..f373ff2225 100644 --- a/internal/config/struct.go +++ b/internal/config/struct.go @@ -50,12 +50,12 @@ type PostgreSQLConfig struct { } type ListenConfig struct { - HTTP string `yaml:"http" env:"HTTP, overwrite"` - HTTPS string `yaml:"https" env:"HTTPS, overwrite"` - LDAP string `yaml:"ldap" env:"LDAP, overwrite"` - LDAPS string `yaml:"ldaps" env:"LDAPS, overwrite"` - Radius string `yaml:"radius" env:"RADIUS, overwrite"` - Metrics string `yaml:"metrics" env:"METRICS, overwrite"` + HTTP []string `yaml:"http" env:"HTTP, overwrite"` + HTTPS []string `yaml:"https" env:"HTTPS, overwrite"` + LDAP []string `yaml:"ldap" env:"LDAP, overwrite"` + LDAPS []string `yaml:"ldaps" env:"LDAPS, overwrite"` + Radius []string `yaml:"radius" env:"RADIUS, overwrite"` + Metrics []string `yaml:"metrics" env:"METRICS, overwrite"` Debug string `yaml:"debug" env:"DEBUG, overwrite"` TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"TRUSTED_PROXY_CIDRS, overwrite"` } diff --git a/internal/outpost/ak/api.go b/internal/outpost/ak/api.go index 6d0491ff19..5af60bc437 100644 --- a/internal/outpost/ak/api.go +++ b/internal/outpost/ak/api.go @@ -5,6 +5,7 @@ import ( "crypto/fips140" "fmt" "math/rand" + "net" "net/http" "net/url" "os" @@ -54,19 +55,44 @@ type APIController struct { // NewAPIController initialise new API Controller instance from URL and API token func NewAPIController(akURL url.URL, token string) *APIController { rsp := sentry.StartSpan(context.Background(), "authentik.outposts.init") + log := log.WithField("logger", "authentik.outpost.ak-api-controller") + + originalAkURL := akURL + var client http.Client + if akURL.Scheme == "unix" { + log.WithField("host", akURL.Host).WithField("path", akURL.Path).Debug("using unix socket") + socketPath := akURL.Host + client = http.Client{ + Transport: web.NewUserAgentTransport( + constants.UserAgentOutpost(), + web.NewTracingTransport( + rsp.Context(), + &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", socketPath) + }, + }, + ), + ), + } + akURL.Scheme = "http" + akURL.Host = "localhost" + } else { + client = http.Client{ + Transport: web.NewUserAgentTransport( + constants.UserAgentOutpost(), + web.NewTracingTransport( + rsp.Context(), + GetTLSTransport(), + ), + ), + } + } apiConfig := api.NewConfiguration() apiConfig.Host = akURL.Host apiConfig.Scheme = akURL.Scheme - apiConfig.HTTPClient = &http.Client{ - Transport: web.NewUserAgentTransport( - constants.UserAgentOutpost(), - web.NewTracingTransport( - rsp.Context(), - GetTLSTransport(), - ), - ), - } + apiConfig.HTTPClient = &client apiConfig.Servers = api.ServerConfigurations{ { URL: fmt.Sprintf("%sapi/v3", akURL.Path), @@ -77,8 +103,6 @@ func NewAPIController(akURL url.URL, token string) *APIController { // create the API client, with the transport apiClient := api.NewAPIClient(apiConfig) - log := log.WithField("logger", "authentik.outpost.ak-api-controller") - // Because we don't know the outpost UUID, we simply do a list and pick the first // The service account this token belongs to should only have access to a single outpost outposts, _ := retry.DoWithData[*api.PaginatedOutpostList]( @@ -124,7 +148,7 @@ func NewAPIController(akURL url.URL, token string) *APIController { } ac.logger.WithField("embedded", ac.IsEmbedded()).Info("Outpost mode") ac.logger.WithField("offset", ac.reloadOffset.String()).Debug("HA Reload offset") - err = ac.initEvent(akURL, outpost.Pk) + err = ac.initEvent(originalAkURL, outpost.Pk) if err != nil { go ac.recentEvents() } diff --git a/internal/outpost/ak/api_event.go b/internal/outpost/ak/api_event.go index bdf2c22ae9..291fefde0a 100644 --- a/internal/outpost/ak/api_event.go +++ b/internal/outpost/ak/api_event.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "fmt" "maps" + "net" "net/http" "net/url" "strconv" @@ -45,9 +46,19 @@ func (ac *APIController) initEvent(akURL url.URL, outpostUUID string) error { dialer := websocket.Dialer{ Proxy: http.ProxyFromEnvironment, HandshakeTimeout: 10 * time.Second, - TLSClientConfig: &tls.Config{ + } + if akURL.Scheme == "unix" { + ac.logger.WithField("host", akURL.Host).WithField("path", akURL.Path).Debug("websocket is using unix connection") + socketPath := akURL.Host + dialer.NetDialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, "unix", socketPath) + } + akURL.Scheme = "http" + akURL.Host = "localhost" + } else { + dialer.TLSClientConfig = &tls.Config{ InsecureSkipVerify: config.Get().AuthentikInsecure, - }, + } } wsu := ac.getWebsocketURL(akURL, outpostUUID, query).String() diff --git a/internal/outpost/ak/healthcheck/cmd.go b/internal/outpost/ak/healthcheck/cmd.go index 3b12a5f235..c1eadb772e 100644 --- a/internal/outpost/ak/healthcheck/cmd.go +++ b/internal/outpost/ak/healthcheck/cmd.go @@ -1,13 +1,16 @@ package healthcheck import ( - "fmt" + "context" + "net" "net/http" "os" + "path" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "goauthentik.io/internal/config" + "goauthentik.io/internal/outpost/ak" "goauthentik.io/internal/utils/web" ) @@ -21,9 +24,15 @@ var Command = &cobra.Command{ func check() int { h := &http.Client{ - Transport: web.NewUserAgentTransport("goauthentik.io/healthcheck", http.DefaultTransport), + Transport: web.NewUserAgentTransport("goauthentik.io/healthcheck", + &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", path.Join(os.TempDir(), ak.MetricsSocketName)) + }, + }, + ), } - url := fmt.Sprintf("http://%s/outpost.goauthentik.io/ping", config.Get().Listen.Metrics) + url := "http://localhost/outpost.goauthentik.io/ping" res, err := h.Head(url) if err != nil { log.WithError(err).Warning("failed to send healthcheck request") diff --git a/internal/outpost/ak/metrics.go b/internal/outpost/ak/metrics.go index 3556367667..c980072af8 100644 --- a/internal/outpost/ak/metrics.go +++ b/internal/outpost/ak/metrics.go @@ -1,12 +1,22 @@ package ak import ( + "net/http" + "os" + "path" + + "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" + log "github.com/sirupsen/logrus" + "goauthentik.io/internal/utils/sentry" + "goauthentik.io/internal/utils/unix" ) var ( - OutpostInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{ + MetricsSocketName = "authentik-metrics.sock" + OutpostInfo = promauto.NewGaugeVec(prometheus.GaugeOpts{ Name: "authentik_outpost_info", Help: "Outpost info", }, []string{"outpost_name", "outpost_type", "uuid", "version", "build"}) @@ -19,3 +29,43 @@ var ( Help: "Connection status", }, []string{"outpost_name", "outpost_type", "uuid"}) ) + +func MetricsRouter() *mux.Router { + m := mux.NewRouter() + m.Use(sentry.SentryNoSampleMiddleware) + m.HandleFunc("/outpost.goauthentik.io/ping", func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(204) + }) + m.Path("/metrics").Handler(promhttp.Handler()) + return m +} + +func RunMetricsServer(listen string, router *mux.Router) { + l := log.WithField("logger", "authentik.outpost.metrics").WithField("listen", listen) + l.Info("Starting Metrics server") + err := http.ListenAndServe(listen, router) + if err != nil { + l.WithError(err).Warning("Failed to start metrics listener") + } +} + +func RunMetricsUnix(router *mux.Router) { + socketPath := path.Join(os.TempDir(), MetricsSocketName) + l := log.WithField("logger", "authentik.outpost.metrics").WithField("listen", socketPath) + ln, err := unix.Listen(socketPath) + if err != nil { + l.WithError(err).Warning("failed to listen") + return + } + defer func() { + err := ln.Close() + if err != nil { + l.WithError(err).Warning("failed to close listener") + } + }() + l.WithField("listen", socketPath).Info("Starting Metrics server") + err = http.Serve(ln, router) + if err != nil { + l.WithError(err).Warning("Failed to start metrics listener") + } +} diff --git a/internal/outpost/ldap/ldap.go b/internal/outpost/ldap/ldap.go index 4cf6611aa8..1454a2bf41 100644 --- a/internal/outpost/ldap/ldap.go +++ b/internal/outpost/ldap/ldap.go @@ -11,7 +11,6 @@ import ( "goauthentik.io/internal/config" "goauthentik.io/internal/crypto" "goauthentik.io/internal/outpost/ak" - "goauthentik.io/internal/outpost/ldap/metrics" "goauthentik.io/internal/utils" "beryju.io/ldap" @@ -63,9 +62,7 @@ func (ls *LDAPServer) Type() string { return "ldap" } -func (ls *LDAPServer) StartLDAPServer() error { - listen := config.Get().Listen.LDAP - +func (ls *LDAPServer) StartLDAPServer(listen string) error { ln, err := net.Listen("tcp", listen) if err != nil { ls.log.WithField("listen", listen).WithError(err).Warning("Failed to listen (SSL)") @@ -89,26 +86,40 @@ func (ls *LDAPServer) StartLDAPServer() error { } func (ls *LDAPServer) Start() error { + listenLdap := config.Get().Listen.LDAP + listenLdaps := config.Get().Listen.LDAPS + listenMetrics := config.Get().Listen.Metrics + metricsRouter := ak.MetricsRouter() wg := sync.WaitGroup{} - wg.Add(3) + wg.Add(len(listenLdap) + len(listenLdaps) + 1 + len(listenMetrics)) + for _, listen := range listenLdap { + go func() { + defer wg.Done() + err := ls.StartLDAPServer(listen) + if err != nil { + panic(err) + } + }() + } + for _, listen := range listenLdaps { + go func() { + defer wg.Done() + err := ls.StartLDAPTLSServer(listen) + if err != nil { + panic(err) + } + }() + } go func() { defer wg.Done() - metrics.RunServer() - }() - go func() { - defer wg.Done() - err := ls.StartLDAPServer() - if err != nil { - panic(err) - } - }() - go func() { - defer wg.Done() - err := ls.StartLDAPTLSServer() - if err != nil { - panic(err) - } + ak.RunMetricsUnix(metricsRouter) }() + for _, listen := range listenMetrics { + go func() { + defer wg.Done() + ak.RunMetricsServer(listen, metricsRouter) + }() + } wg.Wait() return nil } diff --git a/internal/outpost/ldap/ldap_tls.go b/internal/outpost/ldap/ldap_tls.go index 40dfc25d9b..55d81c8a99 100644 --- a/internal/outpost/ldap/ldap_tls.go +++ b/internal/outpost/ldap/ldap_tls.go @@ -5,7 +5,6 @@ import ( "net" "github.com/pires/go-proxyproto" - "goauthentik.io/internal/config" "goauthentik.io/internal/utils" ) @@ -37,8 +36,7 @@ func (ls *LDAPServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certifica return ls.defaultCert, nil } -func (ls *LDAPServer) StartLDAPTLSServer() error { - listen := config.Get().Listen.LDAPS +func (ls *LDAPServer) StartLDAPTLSServer(listen string) error { tlsConfig := utils.GetTLSConfig() tlsConfig.GetCertificate = ls.getCertificates diff --git a/internal/outpost/ldap/metrics/metrics.go b/internal/outpost/ldap/metrics/metrics.go index 8c36b2f883..df618fff3b 100644 --- a/internal/outpost/ldap/metrics/metrics.go +++ b/internal/outpost/ldap/metrics/metrics.go @@ -1,16 +1,8 @@ package metrics import ( - "net/http" - - log "github.com/sirupsen/logrus" - "goauthentik.io/internal/config" - "goauthentik.io/internal/utils/sentry" - - "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( @@ -23,19 +15,3 @@ var ( Help: "Total number of rejected requests", }, []string{"outpost_name", "type", "reason", "app"}) ) - -func RunServer() { - m := mux.NewRouter() - l := log.WithField("logger", "authentik.outpost.metrics") - m.Use(sentry.SentryNoSampleMiddleware) - m.HandleFunc("/outpost.goauthentik.io/ping", func(rw http.ResponseWriter, r *http.Request) { - rw.WriteHeader(204) - }) - m.Path("/metrics").Handler(promhttp.Handler()) - listen := config.Get().Listen.Metrics - l.WithField("listen", listen).Info("Starting Metrics server") - err := http.ListenAndServe(listen, m) - if err != nil { - l.WithError(err).Warning("Failed to start metrics listener") - } -} diff --git a/internal/outpost/proxyv2/metrics/metrics.go b/internal/outpost/proxyv2/metrics/metrics.go index 6f589025f8..f3fe104785 100644 --- a/internal/outpost/proxyv2/metrics/metrics.go +++ b/internal/outpost/proxyv2/metrics/metrics.go @@ -1,16 +1,8 @@ package metrics import ( - "net/http" - - log "github.com/sirupsen/logrus" - "goauthentik.io/internal/config" - "goauthentik.io/internal/utils/sentry" - - "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( @@ -23,19 +15,3 @@ var ( Help: "Proxy upstream response latencies in seconds", }, []string{"outpost_name", "method", "scheme", "host", "upstream_host"}) ) - -func RunServer() { - m := mux.NewRouter() - l := log.WithField("logger", "authentik.outpost.metrics") - m.Use(sentry.SentryNoSampleMiddleware) - m.HandleFunc("/outpost.goauthentik.io/ping", func(rw http.ResponseWriter, r *http.Request) { - rw.WriteHeader(204) - }) - m.Path("/metrics").Handler(promhttp.Handler()) - listen := config.Get().Listen.Metrics - l.WithField("listen", listen).Info("Starting Metrics server") - err := http.ListenAndServe(listen, m) - if err != nil { - l.WithError(err).Warning("Failed to start metrics listener") - } -} diff --git a/internal/outpost/proxyv2/proxyv2.go b/internal/outpost/proxyv2/proxyv2.go index 598325c631..ead5e56483 100644 --- a/internal/outpost/proxyv2/proxyv2.go +++ b/internal/outpost/proxyv2/proxyv2.go @@ -18,7 +18,6 @@ import ( "goauthentik.io/internal/crypto" "goauthentik.io/internal/outpost/ak" "goauthentik.io/internal/outpost/proxyv2/application" - "goauthentik.io/internal/outpost/proxyv2/metrics" "goauthentik.io/internal/utils" sentryutils "goauthentik.io/internal/utils/sentry" "goauthentik.io/internal/utils/web" @@ -127,11 +126,10 @@ func (ps *ProxyServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certific } // ServeHTTP constructs a net.Listener and starts handling HTTP requests -func (ps *ProxyServer) ServeHTTP() { - listenAddress := config.Get().Listen.HTTP - listener, err := net.Listen("tcp", listenAddress) +func (ps *ProxyServer) ServeHTTP(listen string) { + listener, err := net.Listen("tcp", listen) if err != nil { - ps.log.WithField("listen", listenAddress).WithError(err).Warning("Failed to listen") + ps.log.WithField("listen", listen).WithError(err).Warning("Failed to listen") return } proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()} @@ -142,18 +140,17 @@ func (ps *ProxyServer) ServeHTTP() { } }() - ps.log.WithField("listen", listenAddress).Info("Starting HTTP server") + ps.log.WithField("listen", listen).Info("Starting HTTP server") ps.serve(proxyListener) - ps.log.WithField("listen", listenAddress).Info("Stopping HTTP server") + ps.log.WithField("listen", listen).Info("Stopping HTTP server") } // ServeHTTPS constructs a net.Listener and starts handling HTTPS requests -func (ps *ProxyServer) ServeHTTPS() { - listenAddress := config.Get().Listen.HTTPS +func (ps *ProxyServer) ServeHTTPS(listen string) { tlsConfig := utils.GetTLSConfig() tlsConfig.GetCertificate = ps.getCertificates - ln, err := net.Listen("tcp", listenAddress) + ln, err := net.Listen("tcp", listen) if err != nil { ps.log.WithError(err).Warning("Failed to listen (TLS)") return @@ -167,26 +164,40 @@ func (ps *ProxyServer) ServeHTTPS() { }() tlsListener := tls.NewListener(proxyListener, tlsConfig) - ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server") + ps.log.WithField("listen", listen).Info("Starting HTTPS server") ps.serve(tlsListener) - ps.log.WithField("listen", listenAddress).Info("Stopping HTTPS server") + ps.log.WithField("listen", listen).Info("Stopping HTTPS server") } func (ps *ProxyServer) Start() error { + listenHttp := config.Get().Listen.HTTP + listenHttps := config.Get().Listen.HTTPS + listenMetrics := config.Get().Listen.Metrics + metricsRouter := ak.MetricsRouter() wg := sync.WaitGroup{} - wg.Add(3) + wg.Add(len(listenHttp) + len(listenHttps) + 1 + len(listenMetrics)) + for _, listen := range listenHttp { + go func() { + defer wg.Done() + ps.ServeHTTP(listen) + }() + } + for _, listen := range listenHttps { + go func() { + defer wg.Done() + ps.ServeHTTPS(listen) + }() + } go func() { defer wg.Done() - ps.ServeHTTP() - }() - go func() { - defer wg.Done() - ps.ServeHTTPS() - }() - go func() { - defer wg.Done() - metrics.RunServer() + ak.RunMetricsUnix(metricsRouter) }() + for _, listen := range listenMetrics { + go func() { + defer wg.Done() + ak.RunMetricsServer(listen, metricsRouter) + }() + } return nil } diff --git a/internal/outpost/rac/metrics/metrics.go b/internal/outpost/rac/metrics/metrics.go deleted file mode 100644 index 0a3e6b45d4..0000000000 --- a/internal/outpost/rac/metrics/metrics.go +++ /dev/null @@ -1,28 +0,0 @@ -package metrics - -import ( - "net/http" - - log "github.com/sirupsen/logrus" - "goauthentik.io/internal/config" - "goauthentik.io/internal/utils/sentry" - - "github.com/gorilla/mux" - "github.com/prometheus/client_golang/prometheus/promhttp" -) - -func RunServer() { - m := mux.NewRouter() - l := log.WithField("logger", "authentik.outpost.metrics") - m.Use(sentry.SentryNoSampleMiddleware) - m.HandleFunc("/outpost.goauthentik.io/ping", func(rw http.ResponseWriter, r *http.Request) { - rw.WriteHeader(204) - }) - m.Path("/metrics").Handler(promhttp.Handler()) - listen := config.Get().Listen.Metrics - l.WithField("listen", listen).Info("Starting Metrics server") - err := http.ListenAndServe(listen, m) - if err != nil { - l.WithError(err).Warning("Failed to start metrics listener") - } -} diff --git a/internal/outpost/rac/rac.go b/internal/outpost/rac/rac.go index c2cb65f8bb..b78029c77c 100644 --- a/internal/outpost/rac/rac.go +++ b/internal/outpost/rac/rac.go @@ -9,9 +9,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/wwt/guac" + "goauthentik.io/internal/config" "goauthentik.io/internal/outpost/ak" "goauthentik.io/internal/outpost/rac/connection" - "goauthentik.io/internal/outpost/rac/metrics" ) type RACServer struct { @@ -92,12 +92,10 @@ func (rs *RACServer) wsHandler(ctx context.Context, msg ak.Event) error { } func (rs *RACServer) Start() error { + listenMetrics := config.Get().Listen.Metrics + metricsRouter := ak.MetricsRouter() wg := sync.WaitGroup{} - wg.Add(2) - go func() { - defer wg.Done() - metrics.RunServer() - }() + wg.Add(1 + 1 + len(listenMetrics)) go func() { defer wg.Done() err := rs.startGuac() @@ -105,6 +103,16 @@ func (rs *RACServer) Start() error { panic(err) } }() + go func() { + defer wg.Done() + ak.RunMetricsUnix(metricsRouter) + }() + for _, listen := range listenMetrics { + go func() { + defer wg.Done() + ak.RunMetricsServer(listen, metricsRouter) + }() + } wg.Wait() return nil } diff --git a/internal/outpost/radius/metrics/metrics.go b/internal/outpost/radius/metrics/metrics.go index c741c04ed1..2aa6269c88 100644 --- a/internal/outpost/radius/metrics/metrics.go +++ b/internal/outpost/radius/metrics/metrics.go @@ -1,16 +1,8 @@ package metrics import ( - "net/http" - - log "github.com/sirupsen/logrus" - "goauthentik.io/internal/config" - "goauthentik.io/internal/utils/sentry" - - "github.com/gorilla/mux" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/prometheus/client_golang/prometheus/promhttp" ) var ( @@ -23,19 +15,3 @@ var ( Help: "Total number of rejected requests", }, []string{"outpost_name", "reason", "app"}) ) - -func RunServer() { - m := mux.NewRouter() - l := log.WithField("logger", "authentik.outpost.metrics") - m.Use(sentry.SentryNoSampleMiddleware) - m.HandleFunc("/outpost.goauthentik.io/ping", func(rw http.ResponseWriter, r *http.Request) { - rw.WriteHeader(204) - }) - m.Path("/metrics").Handler(promhttp.Handler()) - listen := config.Get().Listen.Metrics - l.WithField("listen", listen).Info("Starting Metrics server") - err := http.ListenAndServe(listen, m) - if err != nil { - l.WithError(err).Warning("Failed to start metrics listener") - } -} diff --git a/internal/outpost/radius/radius.go b/internal/outpost/radius/radius.go index fa25c9177d..66c2c1ab51 100644 --- a/internal/outpost/radius/radius.go +++ b/internal/outpost/radius/radius.go @@ -10,7 +10,7 @@ import ( log "github.com/sirupsen/logrus" "goauthentik.io/internal/config" "goauthentik.io/internal/outpost/ak" - "goauthentik.io/internal/outpost/radius/metrics" + "golang.org/x/sync/errgroup" "layeh.com/radius" ) @@ -30,7 +30,7 @@ type ProviderInstance struct { } type RadiusServer struct { - s radius.PacketServer + s []*radius.PacketServer log *log.Entry ac *ak.APIController cryptoStore *ak.CryptoStore @@ -45,10 +45,13 @@ func NewServer(ac *ak.APIController) ak.Outpost { providers: map[int32]*ProviderInstance{}, cryptoStore: ak.NewCryptoStore(ac.Client.CryptoAPI), } - rs.s = radius.PacketServer{ - Handler: rs, - SecretSource: rs, - Addr: config.Get().Listen.Radius, + listenRadius := config.Get().Listen.Radius + for _, listen := range listenRadius { + rs.s = append(rs.s, &radius.PacketServer{ + Handler: rs, + SecretSource: rs, + Addr: listen, + }) } return rs } @@ -95,29 +98,44 @@ func (rs *RadiusServer) RADIUSSecret(ctx context.Context, remoteAddr net.Addr) ( } func (rs *RadiusServer) Start() error { + listenMetrics := config.Get().Listen.Metrics + metricsRouter := ak.MetricsRouter() wg := sync.WaitGroup{} - wg.Add(2) + wg.Add(len(rs.s) + 1 + len(listenMetrics)) + for _, s := range rs.s { + go func() { + defer wg.Done() + rs.log.WithField("listen", s.Addr).Info("Starting radius server") + err := s.ListenAndServe() + if err != nil { + panic(err) + } + }() + } go func() { defer wg.Done() - metrics.RunServer() - }() - go func() { - defer wg.Done() - rs.log.WithField("listen", rs.s.Addr).Info("Starting radius server") - err := rs.s.ListenAndServe() - if err != nil { - panic(err) - } + ak.RunMetricsUnix(metricsRouter) }() + for _, listen := range listenMetrics { + go func() { + defer wg.Done() + ak.RunMetricsServer(listen, metricsRouter) + }() + } wg.Wait() return nil } func (rs *RadiusServer) Stop() error { ctx, cancel := context.WithCancel(context.Background()) - err := rs.s.Shutdown(ctx) + errs := new(errgroup.Group) + for _, s := range rs.s { + errs.Go(func() error { + return s.Shutdown(ctx) + }) + } cancel() - return err + return errs.Wait() } func (rs *RadiusServer) TimerFlowCacheExpiry(context.Context) {} diff --git a/internal/utils/unix/unix.go b/internal/utils/unix/unix.go new file mode 100644 index 0000000000..89816aae8f --- /dev/null +++ b/internal/utils/unix/unix.go @@ -0,0 +1,43 @@ +package unix + +import ( + "net" +) + +type Listener struct { + *net.UnixListener +} + +type Conn struct { + net.Conn +} + +func Listen(path string) (*Listener, error) { + addr, err := net.ResolveUnixAddr("unix", path) + if err != nil { + return nil, err + } + ln, err := net.ListenUnix("unix", addr) + if err != nil { + return nil, err + } + return &Listener{ + ln, + }, nil +} + +func (l *Listener) Accept() (net.Conn, error) { + c, err := l.UnixListener.Accept() + if err != nil { + return nil, err + } + return &Conn{c}, nil +} + +func (c *Conn) LocalAddr() net.Addr { + return &net.TCPAddr{IP: net.IPv6loopback, Port: 0} +} + +func (c *Conn) RemoteAddr() net.Addr { + return &net.TCPAddr{IP: net.IPv6loopback, Port: 0} +} diff --git a/internal/web/metrics.go b/internal/web/metrics.go index fff462f5ea..b1ff6dc949 100644 --- a/internal/web/metrics.go +++ b/internal/web/metrics.go @@ -19,7 +19,7 @@ var Requests = promauto.NewHistogramVec(prometheus.HistogramOpts{ Help: "API request latencies in seconds", }, []string{"dest"}) -func (ws *WebServer) runMetricsServer() { +func (ws *WebServer) runMetricsServer(listen string) { l := log.WithField("logger", "authentik.router.metrics") m := mux.NewRouter() @@ -49,10 +49,10 @@ func (ws *WebServer) runMetricsServer() { return } }) - l.WithField("listen", config.Get().Listen.Metrics).Info("Starting Metrics server") - err := http.ListenAndServe(config.Get().Listen.Metrics, m) + l.WithField("listen", listen).Info("Starting Metrics server") + err := http.ListenAndServe(listen, m) if err != nil { l.WithError(err).Warning("Failed to start metrics server") } - l.WithField("listen", config.Get().Listen.Metrics).Info("Stopping Metrics server") + l.WithField("listen", listen).Info("Stopping Metrics server") } diff --git a/internal/web/web.go b/internal/web/web.go index 3dbcc688b3..e1d9a4d326 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -21,17 +21,18 @@ import ( "goauthentik.io/internal/config" "goauthentik.io/internal/constants" "goauthentik.io/internal/gounicorn" - "goauthentik.io/internal/outpost/ak" "goauthentik.io/internal/outpost/proxyv2" "goauthentik.io/internal/utils" + "goauthentik.io/internal/utils/unix" "goauthentik.io/internal/utils/web" "goauthentik.io/internal/web/brand_tls" ) const ( + SocketName = "authentik.sock" IPCKeyFile = "authentik-core-ipc.key" MetricsKeyFile = "authentik-core-metrics.key" - UnixSocketName = "authentik-core.sock" + CoreSocketName = "authentik-core.sock" ) type WebServer struct { @@ -64,7 +65,7 @@ func NewWebServer() *WebServer { loggingHandler.Use(web.NewLoggingHandler(l, nil)) tmp := os.TempDir() - socketPath := path.Join(tmp, UnixSocketName) + socketPath := path.Join(tmp, CoreSocketName) // create http client to talk to backend, normal client if we're in debug more // and a client that connects to our socket when in non debug mode @@ -140,7 +141,8 @@ func (ws *WebServer) prepareKeys() { func (ws *WebServer) Start() { ws.prepareKeys() - u, err := url.Parse(fmt.Sprintf("http://%s%s", config.Get().Listen.HTTP, config.Get().Web.Path)) + socketPath := path.Join(os.TempDir(), SocketName) + u, err := url.Parse(fmt.Sprintf("http://localhost%s", config.Get().Web.Path)) if err != nil { panic(err) } @@ -150,7 +152,11 @@ func (ws *WebServer) Start() { apiConfig.HTTPClient = &http.Client{ Transport: web.NewUserAgentTransport( constants.UserAgentIPC(), - ak.GetTLSTransport(), + &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", socketPath) + }, + }, ), } apiConfig.Servers = api.ServerConfigurations{ @@ -171,10 +177,18 @@ func (ws *WebServer) Start() { go tw.Start() }) - go ws.runMetricsServer() + for _, listen := range config.Get().Listen.Metrics { + go ws.runMetricsServer(listen) + } go ws.attemptStartBackend() - go ws.listenPlain() - go ws.listenTLS() + _ = os.Remove(socketPath) + go ws.listenUnix(socketPath) + for _, listen := range config.Get().Listen.HTTP { + go ws.listenPlain(listen) + } + for _, listen := range config.Get().Listen.HTTPS { + go ws.listenTLS(listen) + } } func (ws *WebServer) attemptStartBackend() { @@ -225,23 +239,41 @@ func (ws *WebServer) Shutdown() { ws.stop <- struct{}{} } -func (ws *WebServer) listenPlain() { - ln, err := net.Listen("tcp", config.Get().Listen.HTTP) +func (ws *WebServer) listenUnix(listen string) { + ln, err := unix.Listen(listen) if err != nil { - ws.log.WithError(err).Warning("failed to listen") + ws.log.WithField("listen", listen).WithError(err).Warning("failed to listen") + return + } + defer func() { + err := ln.Close() + if err != nil { + ws.log.WithField("listen", listen).WithError(err).Warning("failed to close listener") + } + }() + + ws.log.WithField("listen", listen).Info("Starting HTTP server") + ws.serve(ln) + ws.log.WithField("listen", listen).Info("Stopping HTTP server") +} + +func (ws *WebServer) listenPlain(listen string) { + ln, err := net.Listen("tcp", listen) + if err != nil { + ws.log.WithField("listen", listen).WithError(err).Warning("failed to listen") return } proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()} defer func() { err := proxyListener.Close() if err != nil { - ws.log.WithError(err).Warning("failed to close proxy listener") + ws.log.WithField("listen", listen).WithError(err).Warning("failed to close proxy listener") } }() - ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server") + ws.log.WithField("listen", listen).Info("Starting HTTP server") ws.serve(proxyListener) - ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Stopping HTTP server") + ws.log.WithField("listen", listen).Info("Stopping HTTP server") } func (ws *WebServer) serve(listener net.Listener) { diff --git a/internal/web/web_tls.go b/internal/web/web_tls.go index 7c4818ea1e..9aca90a5c8 100644 --- a/internal/web/web_tls.go +++ b/internal/web/web_tls.go @@ -6,7 +6,6 @@ import ( "github.com/pires/go-proxyproto" - "goauthentik.io/internal/config" "goauthentik.io/internal/crypto" "goauthentik.io/internal/utils" "goauthentik.io/internal/utils/web" @@ -48,13 +47,13 @@ func (ws *WebServer) GetCertificate() func(ch *tls.ClientHelloInfo) (*tls.Config } // ServeHTTPS constructs a net.Listener and starts handling HTTPS requests -func (ws *WebServer) listenTLS() { +func (ws *WebServer) listenTLS(listen string) { tlsConfig := utils.GetTLSConfig() tlsConfig.GetConfigForClient = ws.GetCertificate() - ln, err := net.Listen("tcp", config.Get().Listen.HTTPS) + ln, err := net.Listen("tcp", listen) if err != nil { - ws.log.WithError(err).Warning("failed to listen (TLS)") + ws.log.WithField("listen", listen).WithError(err).Warning("failed to listen (TLS)") return } proxyListener := &proxyproto.Listener{ @@ -71,7 +70,7 @@ func (ws *WebServer) listenTLS() { }() tlsListener := tls.NewListener(proxyListener, tlsConfig) - ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Starting HTTPS server") + ws.log.WithField("listen", listen).Info("Starting HTTPS server") ws.serve(tlsListener) - ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Stopping HTTPS server") + ws.log.WithField("listen", listen).Info("Stopping HTTPS server") } diff --git a/website/docs/install-config/configuration/configuration.mdx b/website/docs/install-config/configuration/configuration.mdx index f84ac43d00..c24b541727 100644 --- a/website/docs/install-config/configuration/configuration.mdx +++ b/website/docs/install-config/configuration/configuration.mdx @@ -237,43 +237,43 @@ Defaults to `seconds=60`. ##### `AUTHENTIK_LISTEN__HTTP` -Listening address:port for HTTP. +List of comma-separated `address:port` values for HTTP. Applies to the Server, the Worker, and Proxy outposts. -Defaults to `0.0.0.0:9000`. +Defaults to `[::]:9000`. ##### `AUTHENTIK_LISTEN__HTTPS` -Listening address:port for HTTPS. +List of comma-separated `address:port` values for HTTPS. Applies to the Server and Proxy outposts. -Defaults to `0.0.0.0:9443`. +Defaults to `[::]:9443`. ##### `AUTHENTIK_LISTEN__LDAP` -Listening address:port for LDAP. +List of comma-separated `address:port` values for LDAP. Applies to LDAP outposts. -Defaults to `0.0.0.0:3389`. +Defaults to `[::]:3389`. ##### `AUTHENTIK_LISTEN__LDAPS` -Listening address:port for LDAPS. +List of comma-separated `address:port` values for LDAPS. Applies to LDAP outposts. -Defaults to `0.0.0.0:6636`. +Defaults to `[::]:6636`. ##### `AUTHENTIK_LISTEN__METRICS` -Listening address:port for Prometheus metrics. +List of comma-separated `address:port` values for Prometheus metrics. Applies to all. -Defaults to `0.0.0.0:9300`. +Defaults to `[::]:9300`. ##### `AUTHENTIK_LISTEN__DEBUG` diff --git a/website/docs/releases/2026/v2026.5.md b/website/docs/releases/2026/v2026.5.md index c69bd82a6e..550cfaafb8 100644 --- a/website/docs/releases/2026/v2026.5.md +++ b/website/docs/releases/2026/v2026.5.md @@ -6,7 +6,11 @@ draft: true ## Highlights - +## Breaking changes + +### Listening on multiple IPs + +For advanced use cases, authentik now supports setting listening settings to a comma-separated list of IPs. With this change, the default IP we listen on changed from `0.0.0.0` to `[::]` to better match ecosystem standards. Some IPv4-only environments might need to adapt those settings. ## New features and improvements