diff --git a/outpost/cmd/server.go b/outpost/cmd/proxy/server.go similarity index 63% rename from outpost/cmd/server.go rename to outpost/cmd/proxy/server.go index 02c6d3140..6ddb57882 100644 --- a/outpost/cmd/server.go +++ b/outpost/cmd/proxy/server.go @@ -1,4 +1,4 @@ -package cmd +package main import ( "fmt" @@ -8,18 +8,21 @@ import ( "os/signal" "time" - "github.com/BeryJu/authentik/outpost/pkg/server" + log "github.com/sirupsen/logrus" + + "github.com/BeryJu/authentik/outpost/pkg/ak" + "github.com/BeryJu/authentik/outpost/pkg/proxy" ) const helpMessage = `authentik proxy Required environment variables: - - AUTHENTIK_HOST: URL to connect to (format "http://authentik.company") - - AUTHENTIK_TOKEN: Token to authenticate with - - AUTHENTIK_INSECURE: Skip SSL Certificate verification` +- AUTHENTIK_HOST: URL to connect to (format "http://authentik.company") +- AUTHENTIK_TOKEN: Token to authenticate with +- AUTHENTIK_INSECURE: Skip SSL Certificate verification` -// RunServer main entrypoint, runs the full server -func RunServer() { +func main() { + log.SetLevel(log.DebugLevel) pbURL, found := os.LookupEnv("AUTHENTIK_HOST") if !found { fmt.Println("env AUTHENTIK_HOST not set!") @@ -42,11 +45,13 @@ func RunServer() { rand.Seed(time.Now().UnixNano()) - ac := server.NewAPIController(*pbURLActual, pbToken) + ac := ak.NewAPIController(*pbURLActual, pbToken) interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) + ac.Server = proxy.NewServer(ac) + ac.Start() for { diff --git a/outpost/go.mod b/outpost/go.mod index 2d8dd72c6..2e6b951b1 100644 --- a/outpost/go.mod +++ b/outpost/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( cloud.google.com/go v0.64.0 // indirect + github.com/BeryJu/authentik/proxy v0.0.0-20210116180903-8acb9dde5f2f github.com/coreos/go-oidc v2.2.1+incompatible github.com/getsentry/sentry-go v0.9.0 github.com/go-openapi/errors v0.19.9 @@ -19,8 +20,10 @@ require ( github.com/justinas/alice v1.2.0 github.com/kr/pretty v0.2.1 // indirect github.com/magiconair/properties v1.8.4 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc github.com/pelletier/go-toml v1.8.1 // indirect + github.com/pkg/errors v0.9.1 github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect github.com/recws-org/recws v1.2.2 github.com/sirupsen/logrus v1.7.0 @@ -30,11 +33,11 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.7.1 // indirect golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de // indirect - golang.org/x/mod v0.4.0 // indirect + golang.org/x/mod v0.4.1 // indirect golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect - golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect + golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 // indirect golang.org/x/text v0.3.5 // indirect - golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e // indirect + golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 // indirect gopkg.in/ini.v1 v1.62.0 // indirect gopkg.in/square/go-jose.v2 v2.5.1 // indirect ) diff --git a/outpost/go.sum b/outpost/go.sum index 4b13d2222..07b37d14a 100644 --- a/outpost/go.sum +++ b/outpost/go.sum @@ -36,8 +36,11 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/BeryJu/authentik v0.0.0-20210108085217-fd6d99f4f999 h1:ymxzvnxKNUomJIRG1VP3I6ls5mWn8r1xWD82bHASEk0= +github.com/BeryJu/authentik v0.0.0-20210116180903-8acb9dde5f2f h1:pLOJgn8bIzavtn0h874lys3gs7uk1RnMqMWIttOWY8Y= github.com/BeryJu/authentik/outpost v0.0.0-20210108085217-fd6d99f4f999 h1:XYHeaZx7fm4JNx77MHMO6ek/Gdp+sZa2jIJyjC294Gw= github.com/BeryJu/authentik/proxy v0.0.0-20210108085217-fd6d99f4f999 h1:XYHeaZx7fm4JNx77MHMO6ek/Gdp+sZa2jIJyjC294Gw= +github.com/BeryJu/authentik/proxy v0.0.0-20210116180903-8acb9dde5f2f h1:bp617AbteaVcZBXMtr4/A+FSSVGKqRWlTo5chcirq8k= +github.com/BeryJu/authentik/proxy v0.0.0-20210116180903-8acb9dde5f2f/go.mod h1:6/VeRMuLHUE3Ywr1uIpjxnmOJJsAfld7OOOi+uocxQw= github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw= github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= @@ -508,6 +511,8 @@ github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8 github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.0 h1:7ks8ZkOP5/ujthUsT07rNv+nkLXCQWKNHuwzOAesEks= github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -752,6 +757,8 @@ golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -880,6 +887,8 @@ golang.org/x/sys v0.0.0-20201223074533-0d417f636930 h1:vRgIt+nup/B/BwIS0g2oC0haq golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 h1:wHn06sgWHMO1VsQ8F+KzDJx/JzqfsNLnc+oEi07qD7s= golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78 h1:nVuTkr9L6Bq62qpUqKo/RnZCFfzDBL0bYo6w9OJUqZY= +golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -955,6 +964,8 @@ golang.org/x/tools v0.0.0-20201226215659-b1c90890d22a h1:pdfjQ7VswBeGam3EpuEJ4e8 golang.org/x/tools v0.0.0-20201226215659-b1c90890d22a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e h1:Z2uDrs8MyXUWJbwGc4V+nGjV4Ygo+oubBbWSVQw21/I= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 h1:BTs2GMGSMWpgtCpv1CE7vkJTv7XcHdcLLnAMu7UbgTY= +golang.org/x/tools v0.0.0-20210115202250-e0d201561e39/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/outpost/main.go b/outpost/main.go index 4f77b281f..790580777 100644 --- a/outpost/main.go +++ b/outpost/main.go @@ -1,11 +1,5 @@ package main -import ( - "github.com/BeryJu/authentik/outpost/cmd" - log "github.com/sirupsen/logrus" -) - func main() { - log.SetLevel(log.DebugLevel) - cmd.RunServer() + } diff --git a/outpost/pkg/ak/api.go b/outpost/pkg/ak/api.go new file mode 100644 index 000000000..83b6c54c0 --- /dev/null +++ b/outpost/pkg/ak/api.go @@ -0,0 +1,100 @@ +package ak + +import ( + "fmt" + "math/rand" + "net/url" + "time" + + "github.com/BeryJu/authentik/outpost/pkg" + "github.com/BeryJu/authentik/outpost/pkg/client" + "github.com/BeryJu/authentik/outpost/pkg/client/outposts" + "github.com/go-openapi/runtime" + "github.com/pkg/errors" + "github.com/recws-org/recws" + + httptransport "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + log "github.com/sirupsen/logrus" +) + +const ConfigLogLevel = "log_level" +const ConfigErrorReportingEnabled = "error_reporting_enabled" +const ConfigErrorReportingEnvironment = "error_reporting_environment" + +// APIController main controller which connects to the authentik api via http and ws +type APIController struct { + Client *client.Authentik + Auth runtime.ClientAuthInfoWriter + token string + + Server Outpost + + lastBundleHash string + logger *log.Entry + + reloadOffset time.Duration + + wsConn *recws.RecConn +} + +// NewAPIController initialise new API Controller instance from URL and API token +func NewAPIController(pbURL url.URL, token string) *APIController { + transport := httptransport.New(pbURL.Host, client.DefaultBasePath, []string{pbURL.Scheme}) + transport.Transport = SetUserAgent(getTLSTransport(), fmt.Sprintf("authentik-proxy@%s", pkg.VERSION)) + + // create the transport + auth := httptransport.BasicAuth("", token) + + // create the API client, with the transport + apiClient := client.New(transport, strfmt.Default) + + // 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, err := apiClient.Outposts.OutpostsOutpostsList(outposts.NewOutpostsOutpostsListParams(), auth) + + if err != nil { + panic(err) + } + outpost := outposts.Payload.Results[0] + doGlobalSetup(outpost.Config.(map[string]interface{})) + + ac := &APIController{ + Client: apiClient, + Auth: auth, + token: token, + + logger: log.WithField("component", "ak-api-controller"), + + reloadOffset: time.Duration(rand.Intn(10)) * time.Second, + + lastBundleHash: "", + } + ac.logger.Debugf("HA Reload offset: %s", ac.reloadOffset) + ac.initWS(pbURL, outpost.Pk) + return ac +} + +func (a *APIController) GetLastBundleHash() string { + return a.lastBundleHash +} + +// Start Starts all handlers, non-blocking +func (a *APIController) Start() error { + err := a.Server.Refresh() + if err != nil { + return errors.Wrap(err, "failed to run initial refresh") + } + go func() { + a.logger.Debug("Starting WS Handler...") + a.startWSHandler() + }() + go func() { + a.logger.Debug("Starting WS Health notifier...") + a.startWSHealth() + }() + go func() { + a.Server.Start() + }() + return nil +} diff --git a/outpost/pkg/server/api_uag.go b/outpost/pkg/ak/api_uag.go similarity index 96% rename from outpost/pkg/server/api_uag.go rename to outpost/pkg/ak/api_uag.go index b34896750..d600abc0f 100644 --- a/outpost/pkg/server/api_uag.go +++ b/outpost/pkg/ak/api_uag.go @@ -1,4 +1,4 @@ -package server +package ak import "net/http" diff --git a/outpost/pkg/ak/api_update.go b/outpost/pkg/ak/api_update.go new file mode 100644 index 000000000..ebdd7d9b7 --- /dev/null +++ b/outpost/pkg/ak/api_update.go @@ -0,0 +1,26 @@ +package ak + +import ( + "crypto/sha512" + "encoding/hex" + + "github.com/BeryJu/authentik/outpost/pkg/client/outposts" + "github.com/BeryJu/authentik/outpost/pkg/models" +) + +func (a *APIController) Update() ([]*models.ProxyOutpostConfig, error) { + providers, err := a.Client.Outposts.OutpostsProxyList(outposts.NewOutpostsProxyListParams(), a.Auth) + if err != nil { + a.logger.WithError(err).Error("Failed to fetch providers") + return nil, err + } + // Check provider hash to see if anything is changed + hasher := sha512.New() + bin, _ := providers.Payload.MarshalBinary() + hash := hex.EncodeToString(hasher.Sum(bin)) + if hash == a.lastBundleHash { + return nil, nil + } + a.lastBundleHash = hash + return providers.Payload.Results, nil +} diff --git a/outpost/pkg/server/api_ws.go b/outpost/pkg/ak/api_ws.go similarity index 91% rename from outpost/pkg/server/api_ws.go rename to outpost/pkg/ak/api_ws.go index 3d845af01..a483a94d7 100644 --- a/outpost/pkg/server/api_ws.go +++ b/outpost/pkg/ak/api_ws.go @@ -1,4 +1,4 @@ -package server +package ak import ( "crypto/tls" @@ -40,7 +40,7 @@ func (ac *APIController) initWS(pbURL url.URL, outpostUUID strfmt.UUID) { } ws.Dial(fmt.Sprintf(pathTemplate, scheme, pbURL.Host, outpostUUID.String()), header) - ac.logger.WithField("component", "ws").WithField("outpost", outpostUUID.String()).Debug("connecting to authentik") + ac.logger.WithField("component", "ak-ws").WithField("outpost", outpostUUID.String()).Debug("connecting to authentik") ac.wsConn = ws // Send hello message with our version @@ -52,7 +52,7 @@ func (ac *APIController) initWS(pbURL url.URL, outpostUUID strfmt.UUID) { } err := ws.WriteJSON(msg) if err != nil { - ac.logger.WithField("component", "ws").WithError(err).Warning("Failed to hello to authentik") + ac.logger.WithField("component", "ak-ws").WithError(err).Warning("Failed to hello to authentik") } } @@ -87,7 +87,7 @@ func (ac *APIController) startWSHandler() { } if wsMsg.Instruction == WebsocketInstructionTriggerUpdate { time.Sleep(ac.reloadOffset) - err := ac.UpdateIfRequired() + err := ac.Server.Refresh() if err != nil { ac.logger.WithField("loop", "ws-handler").WithError(err).Debug("Failed to update") } diff --git a/outpost/pkg/server/api_ws_msg.go b/outpost/pkg/ak/api_ws_msg.go similarity index 97% rename from outpost/pkg/server/api_ws_msg.go rename to outpost/pkg/ak/api_ws_msg.go index a8367549a..f1f2e3aa8 100644 --- a/outpost/pkg/server/api_ws_msg.go +++ b/outpost/pkg/ak/api_ws_msg.go @@ -1,4 +1,4 @@ -package server +package ak type websocketInstruction int diff --git a/outpost/pkg/server/cert.go b/outpost/pkg/ak/cert.go similarity index 90% rename from outpost/pkg/server/cert.go rename to outpost/pkg/ak/cert.go index 08e1cf55d..69c39f6bf 100644 --- a/outpost/pkg/server/cert.go +++ b/outpost/pkg/ak/cert.go @@ -1,4 +1,4 @@ -package server +package ak import ( "crypto/rand" @@ -13,8 +13,8 @@ import ( log "github.com/sirupsen/logrus" ) -func generateSelfSignedCert() (tls.Certificate, error) { - +// GenerateSelfSignedCert Generate a self-signed TLS Certificate, to be used as fallback +func GenerateSelfSignedCert() (tls.Certificate, error) { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { log.Fatalf("Failed to generate private key: %v", err) diff --git a/outpost/pkg/ak/global.go b/outpost/pkg/ak/global.go new file mode 100644 index 000000000..c7d07fc3f --- /dev/null +++ b/outpost/pkg/ak/global.go @@ -0,0 +1,60 @@ +package ak + +import ( + "net/http" + "os" + "strings" + "time" + + "github.com/BeryJu/authentik/outpost/pkg" + "github.com/getsentry/sentry-go" + httptransport "github.com/go-openapi/runtime/client" + log "github.com/sirupsen/logrus" +) + +func doGlobalSetup(config map[string]interface{}) { + log.SetFormatter(&log.JSONFormatter{}) + switch config[ConfigLogLevel].(string) { + case "debug": + log.SetLevel(log.DebugLevel) + case "info": + log.SetLevel(log.InfoLevel) + case "warning": + log.SetLevel(log.WarnLevel) + case "error": + log.SetLevel(log.ErrorLevel) + default: + log.SetLevel(log.DebugLevel) + } + log.WithField("version", pkg.VERSION).Info("Starting authentik proxy") + + var dsn string + if config[ConfigErrorReportingEnabled].(bool) { + dsn = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8" + log.Debug("Error reporting enabled") + } + + err := sentry.Init(sentry.ClientOptions{ + Dsn: dsn, + Environment: config[ConfigErrorReportingEnvironment].(string), + }) + if err != nil { + log.Fatalf("sentry.Init: %s", err) + } + + defer sentry.Flush(2 * time.Second) +} + +func getTLSTransport() http.RoundTripper { + value, set := os.LookupEnv("AUTHENTIK_INSECURE") + if !set { + value = "false" + } + tlsTransport, err := httptransport.TLSTransport(httptransport.TLSClientOptions{ + InsecureSkipVerify: strings.ToLower(value) == "true", + }) + if err != nil { + panic(err) + } + return tlsTransport +} diff --git a/outpost/pkg/ak/outpost.go b/outpost/pkg/ak/outpost.go new file mode 100644 index 000000000..c60023757 --- /dev/null +++ b/outpost/pkg/ak/outpost.go @@ -0,0 +1,6 @@ +package ak + +type Outpost interface { + Start() error + Refresh() error +} diff --git a/outpost/pkg/proxy/api.go b/outpost/pkg/proxy/api.go new file mode 100644 index 000000000..bcdb84fe3 --- /dev/null +++ b/outpost/pkg/proxy/api.go @@ -0,0 +1,48 @@ +package proxy + +import ( + "net/url" + + "github.com/BeryJu/authentik/outpost/pkg/models" + log "github.com/sirupsen/logrus" +) + +func (s *Server) Refresh() error { + providers, err := s.ak.Update() + if err != nil { + return err + } + if providers == nil { + s.logger.Debug("Providers have not changed, not updating") + return nil + } + bundles := s.bundleProviders(providers) + s.updateHTTPServer(bundles) + return nil +} + +func (s *Server) bundleProviders(providers []*models.ProxyOutpostConfig) []*providerBundle { + bundles := make([]*providerBundle, len(providers)) + for idx, provider := range providers { + externalHost, err := url.Parse(*provider.ExternalHost) + if err != nil { + log.WithError(err).Warning("Failed to parse URL, skipping provider") + } + bundles[idx] = &providerBundle{ + s: s, + Host: externalHost.Host, + log: log.WithField("component", "proxy-bundle").WithField("provider", provider.Name), + } + bundles[idx].Build(provider) + } + return bundles +} + +func (s *Server) updateHTTPServer(bundles []*providerBundle) { + newMap := make(map[string]*providerBundle) + for _, bundle := range bundles { + newMap[bundle.Host] = bundle + } + s.logger.Debug("Swapped maps") + s.Handlers = newMap +} diff --git a/outpost/pkg/server/api_bundle.go b/outpost/pkg/proxy/api_bundle.go similarity index 82% rename from outpost/pkg/server/api_bundle.go rename to outpost/pkg/proxy/api_bundle.go index 531e11dbd..eab01119c 100644 --- a/outpost/pkg/server/api_bundle.go +++ b/outpost/pkg/proxy/api_bundle.go @@ -1,4 +1,4 @@ -package server +package proxy import ( "context" @@ -11,7 +11,6 @@ import ( "github.com/BeryJu/authentik/outpost/pkg/client/crypto" "github.com/BeryJu/authentik/outpost/pkg/models" - "github.com/BeryJu/authentik/outpost/pkg/proxy" "github.com/jinzhu/copier" "github.com/justinas/alice" "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" @@ -23,11 +22,13 @@ import ( type providerBundle struct { http.Handler - a *APIController - proxy *proxy.OAuthProxy + s *Server + proxy *OAuthProxy Host string cert *tls.Certificate + + log *log.Entry } func (pb *providerBundle) prepareOpts(provider *models.ProxyOutpostConfig) *options.Options { @@ -37,7 +38,7 @@ func (pb *providerBundle) prepareOpts(provider *models.ProxyOutpostConfig) *opti return nil } providerOpts := &options.Options{} - copier.Copy(&providerOpts, &pb.a.commonOpts) + copier.Copy(&providerOpts, getCommonOptions()) providerOpts.ClientID = provider.ClientID providerOpts.ClientSecret = provider.ClientSecret @@ -66,22 +67,22 @@ func (pb *providerBundle) prepareOpts(provider *models.ProxyOutpostConfig) *opti } if provider.Certificate != nil { - pb.a.logger.WithField("provider", provider.ClientID).Debug("Enabling TLS") - cert, err := pb.a.client.Crypto.CryptoCertificatekeypairsRead(&crypto.CryptoCertificatekeypairsReadParams{ + pb.log.WithField("provider", provider.ClientID).Debug("Enabling TLS") + cert, err := pb.s.ak.Client.Crypto.CryptoCertificatekeypairsRead(&crypto.CryptoCertificatekeypairsReadParams{ Context: context.Background(), KpUUID: *provider.Certificate, - }, pb.a.auth) + }, pb.s.ak.Auth) if err != nil { - pb.a.logger.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to fetch certificate") + pb.log.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to fetch certificate") return providerOpts } x509cert, err := tls.X509KeyPair([]byte(*cert.Payload.CertificateData), []byte(cert.Payload.KeyData)) if err != nil { - pb.a.logger.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to parse certificate") + pb.log.WithField("provider", provider.ClientID).WithError(err).Warning("Failed to parse certificate") return providerOpts } pb.cert = &x509cert - pb.a.logger.WithField("provider", provider.ClientID).WithField("certificate-key-pair", *cert.Payload.Name).Debug("Loaded certificates") + pb.log.WithField("provider", provider.ClientID).WithField("certificate-key-pair", *cert.Payload.Name).Debug("Loaded certificates") } return providerOpts } @@ -119,7 +120,7 @@ func (pb *providerBundle) Build(provider *models.ProxyOutpostConfig) { log.Printf("%s", err) os.Exit(1) } - oauthproxy, err := proxy.NewOAuthProxy(opts) + oauthproxy, err := NewOAuthProxy(opts) if err != nil { log.Errorf("ERROR: Failed to initialise OAuth2 Proxy: %v", err) os.Exit(1) diff --git a/outpost/pkg/proxy/common.go b/outpost/pkg/proxy/common.go new file mode 100644 index 000000000..eab2b9ed8 --- /dev/null +++ b/outpost/pkg/proxy/common.go @@ -0,0 +1,20 @@ +package proxy + +import ( + "time" + + "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" +) + +func getCommonOptions() *options.Options { + commonOpts := options.NewOptions() + commonOpts.Cookie.Name = "authentik_proxy" + commonOpts.Cookie.Expire = 24 * time.Hour + commonOpts.EmailDomains = []string{"*"} + commonOpts.ProviderType = "oidc" + commonOpts.ProxyPrefix = "/akprox" + commonOpts.Logging.SilencePing = true + commonOpts.SetAuthorization = false + commonOpts.Scope = "openid email profile ak_proxy" + return commonOpts +} diff --git a/outpost/pkg/server/middleware.go b/outpost/pkg/proxy/middleware.go similarity index 97% rename from outpost/pkg/server/middleware.go rename to outpost/pkg/proxy/middleware.go index 2076bb3a3..ae95ed346 100644 --- a/outpost/pkg/server/middleware.go +++ b/outpost/pkg/proxy/middleware.go @@ -1,4 +1,4 @@ -package server +package proxy import ( "bufio" @@ -95,7 +95,7 @@ type loggingHandler struct { func LoggingHandler(h http.Handler) http.Handler { return loggingHandler{ handler: h, - logger: log.WithField("component", "http-server"), + logger: log.WithField("component", "proxy-http-server"), } } diff --git a/outpost/pkg/server/server.go b/outpost/pkg/proxy/server.go similarity index 58% rename from outpost/pkg/server/server.go rename to outpost/pkg/proxy/server.go index 895c4b2e4..e1c1e9567 100644 --- a/outpost/pkg/server/server.go +++ b/outpost/pkg/proxy/server.go @@ -1,4 +1,4 @@ -package server +package proxy import ( "context" @@ -8,6 +8,7 @@ import ( "net/http" "time" + "github.com/BeryJu/authentik/outpost/pkg/ak" log "github.com/sirupsen/logrus" ) @@ -15,70 +16,26 @@ import ( type Server struct { Handlers map[string]*providerBundle - stop chan struct{} // channel for waiting shutdown - logger *log.Entry - + stop chan struct{} // channel for waiting shutdown + logger *log.Entry + ak *ak.APIController defaultCert tls.Certificate } // NewServer initialise a new HTTP Server -func NewServer() *Server { - defaultCert, err := generateSelfSignedCert() +func NewServer(ac *ak.APIController) *Server { + defaultCert, err := ak.GenerateSelfSignedCert() if err != nil { log.Warning(err) } return &Server{ Handlers: make(map[string]*providerBundle), - logger: log.WithField("component", "http-server"), + logger: log.WithField("component", "proxy-http-server"), defaultCert: defaultCert, + ak: ac, } } -// ServeHTTP constructs a net.Listener and starts handling HTTP requests -func (s *Server) ServeHTTP() { - listenAddress := "0.0.0.0:4180" - listener, err := net.Listen("tcp", listenAddress) - if err != nil { - s.logger.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err) - } - s.logger.Printf("listening on %s", listener.Addr()) - s.serve(listener) - s.logger.Printf("closing %s", listener.Addr()) -} - -func (s *Server) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) { - handler, ok := s.Handlers[info.ServerName] - if !ok { - s.logger.WithField("server-name", info.ServerName).Debug("Handler does not exist") - return &s.defaultCert, nil - } - if handler.cert == nil { - s.logger.WithField("server-name", info.ServerName).Debug("Handler does not have a certificate") - return &s.defaultCert, nil - } - return handler.cert, nil -} - -// ServeHTTPS constructs a net.Listener and starts handling HTTPS requests -func (s *Server) ServeHTTPS() { - listenAddress := "0.0.0.0:4443" - config := &tls.Config{ - MinVersion: tls.VersionTLS12, - MaxVersion: tls.VersionTLS12, - GetCertificate: s.getCertificates, - } - - ln, err := net.Listen("tcp", listenAddress) - if err != nil { - s.logger.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err) - } - s.logger.Printf("listening on %s", ln.Addr()) - - tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config) - s.serve(tlsListener) - s.logger.Printf("closing %s", tlsListener.Addr()) -} - func (s *Server) handler(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/akprox/ping" { w.WriteHeader(204) diff --git a/outpost/pkg/proxy/server_https.go b/outpost/pkg/proxy/server_https.go new file mode 100644 index 000000000..8c47a3b6a --- /dev/null +++ b/outpost/pkg/proxy/server_https.go @@ -0,0 +1,68 @@ +package proxy + +import ( + "crypto/tls" + "net" + "sync" +) + +// ServeHTTP constructs a net.Listener and starts handling HTTP requests +func (s *Server) ServeHTTP() { + listenAddress := "0.0.0.0:4180" + listener, err := net.Listen("tcp", listenAddress) + if err != nil { + s.logger.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err) + } + s.logger.Printf("listening on %s", listener.Addr()) + s.serve(listener) + s.logger.Printf("closing %s", listener.Addr()) +} + +func (s *Server) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + handler, ok := s.Handlers[info.ServerName] + if !ok { + s.logger.WithField("server-name", info.ServerName).Debug("Handler does not exist") + return &s.defaultCert, nil + } + if handler.cert == nil { + s.logger.WithField("server-name", info.ServerName).Debug("Handler does not have a certificate") + return &s.defaultCert, nil + } + return handler.cert, nil +} + +// ServeHTTPS constructs a net.Listener and starts handling HTTPS requests +func (s *Server) ServeHTTPS() { + listenAddress := "0.0.0.0:4443" + config := &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS12, + GetCertificate: s.getCertificates, + } + + ln, err := net.Listen("tcp", listenAddress) + if err != nil { + s.logger.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err) + } + s.logger.Printf("listening on %s", ln.Addr()) + + tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config) + s.serve(tlsListener) + s.logger.Printf("closing %s", tlsListener.Addr()) +} + +func (s *Server) Start() error { + wg := sync.WaitGroup{} + wg.Add(2) + go func() { + defer wg.Done() + s.logger.Debug("Starting HTTP Server...") + s.ServeHTTP() + }() + go func() { + defer wg.Done() + s.logger.Debug("Starting HTTPs Server...") + s.ServeHTTPS() + }() + return nil +} diff --git a/outpost/pkg/server/api.go b/outpost/pkg/server/api.go deleted file mode 100644 index 193c22ba8..000000000 --- a/outpost/pkg/server/api.go +++ /dev/null @@ -1,225 +0,0 @@ -package server - -import ( - "crypto/sha512" - "encoding/hex" - "fmt" - "math/rand" - "net/http" - "net/url" - "os" - "strings" - "time" - - "github.com/BeryJu/authentik/outpost/pkg" - "github.com/BeryJu/authentik/outpost/pkg/client" - "github.com/BeryJu/authentik/outpost/pkg/client/outposts" - "github.com/getsentry/sentry-go" - "github.com/go-openapi/runtime" - "github.com/recws-org/recws" - - httptransport "github.com/go-openapi/runtime/client" - "github.com/go-openapi/strfmt" - "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options" - log "github.com/sirupsen/logrus" -) - -const ConfigLogLevel = "log_level" -const ConfigErrorReportingEnabled = "error_reporting_enabled" -const ConfigErrorReportingEnvironment = "error_reporting_environment" - -// APIController main controller which connects to the authentik api via http and ws -type APIController struct { - client *client.Authentik - auth runtime.ClientAuthInfoWriter - token string - - server *Server - - commonOpts *options.Options - - lastBundleHash string - logger *log.Entry - - reloadOffset time.Duration - - wsConn *recws.RecConn -} - -func getCommonOptions() *options.Options { - commonOpts := options.NewOptions() - commonOpts.Cookie.Name = "authentik_proxy" - commonOpts.Cookie.Expire = 24 * time.Hour - commonOpts.EmailDomains = []string{"*"} - commonOpts.ProviderType = "oidc" - commonOpts.ProxyPrefix = "/akprox" - commonOpts.Logging.SilencePing = true - commonOpts.SetAuthorization = false - commonOpts.Scope = "openid email profile ak_proxy" - return commonOpts -} - -func doGlobalSetup(config map[string]interface{}) { - log.SetFormatter(&log.JSONFormatter{}) - switch config[ConfigLogLevel].(string) { - case "debug": - log.SetLevel(log.DebugLevel) - case "info": - log.SetLevel(log.InfoLevel) - case "warning": - log.SetLevel(log.WarnLevel) - case "error": - log.SetLevel(log.ErrorLevel) - default: - log.SetLevel(log.DebugLevel) - } - log.WithField("version", pkg.VERSION).Info("Starting authentik proxy") - - var dsn string - if config[ConfigErrorReportingEnabled].(bool) { - dsn = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8" - log.Debug("Error reporting enabled") - } - - err := sentry.Init(sentry.ClientOptions{ - Dsn: dsn, - Environment: config[ConfigErrorReportingEnvironment].(string), - }) - if err != nil { - log.Fatalf("sentry.Init: %s", err) - } - - defer sentry.Flush(2 * time.Second) -} - -func getTLSTransport() http.RoundTripper { - value, set := os.LookupEnv("AUTHENTIK_INSECURE") - if !set { - value = "false" - } - tlsTransport, err := httptransport.TLSTransport(httptransport.TLSClientOptions{ - InsecureSkipVerify: strings.ToLower(value) == "true", - }) - if err != nil { - panic(err) - } - return tlsTransport -} - -// NewAPIController initialise new API Controller instance from URL and API token -func NewAPIController(pbURL url.URL, token string) *APIController { - transport := httptransport.New(pbURL.Host, client.DefaultBasePath, []string{pbURL.Scheme}) - transport.Transport = SetUserAgent(getTLSTransport(), fmt.Sprintf("authentik-proxy@%s", pkg.VERSION)) - - // create the transport - auth := httptransport.BasicAuth("", token) - - // create the API client, with the transport - apiClient := client.New(transport, strfmt.Default) - - // 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, err := apiClient.Outposts.OutpostsOutpostsList(outposts.NewOutpostsOutpostsListParams(), auth) - - if err != nil { - panic(err) - } - outpost := outposts.Payload.Results[0] - doGlobalSetup(outpost.Config.(map[string]interface{})) - - ac := &APIController{ - client: apiClient, - auth: auth, - token: token, - - logger: log.WithField("component", "api-controller"), - commonOpts: getCommonOptions(), - server: NewServer(), - - reloadOffset: time.Duration(rand.Intn(10)) * time.Second, - - lastBundleHash: "", - } - ac.logger.Debugf("HA Reload offset: %s", ac.reloadOffset) - ac.initWS(pbURL, outpost.Pk) - return ac -} - -func (a *APIController) bundleProviders() ([]*providerBundle, error) { - providers, err := a.client.Outposts.OutpostsProxyList(outposts.NewOutpostsProxyListParams(), a.auth) - if err != nil { - a.logger.WithError(err).Error("Failed to fetch providers") - return nil, err - } - // Check provider hash to see if anything is changed - hasher := sha512.New() - bin, _ := providers.Payload.MarshalBinary() - hash := hex.EncodeToString(hasher.Sum(bin)) - if hash == a.lastBundleHash { - return nil, nil - } - a.lastBundleHash = hash - - bundles := make([]*providerBundle, len(providers.Payload.Results)) - - for idx, provider := range providers.Payload.Results { - externalHost, err := url.Parse(*provider.ExternalHost) - if err != nil { - log.WithError(err).Warning("Failed to parse URL, skipping provider") - } - bundles[idx] = &providerBundle{ - a: a, - Host: externalHost.Host, - } - bundles[idx].Build(provider) - } - return bundles, nil -} - -func (a *APIController) updateHTTPServer(bundles []*providerBundle) { - newMap := make(map[string]*providerBundle) - for _, bundle := range bundles { - newMap[bundle.Host] = bundle - } - a.logger.Debug("Swapped maps") - a.server.Handlers = newMap -} - -// UpdateIfRequired Updates the HTTP Server config if required, automatically swaps the handlers -func (a *APIController) UpdateIfRequired() error { - bundles, err := a.bundleProviders() - if err != nil { - return err - } - if bundles == nil { - a.logger.Debug("Providers have not changed, not updating") - return nil - } - a.updateHTTPServer(bundles) - return nil -} - -// Start Starts all handlers, non-blocking -func (a *APIController) Start() error { - err := a.UpdateIfRequired() - if err != nil { - return err - } - go func() { - a.logger.Debug("Starting HTTP Server...") - a.server.ServeHTTP() - }() - go func() { - a.logger.Debug("Starting HTTPs Server...") - a.server.ServeHTTPS() - }() - go func() { - a.logger.Debug("Starting WS Handler...") - a.startWSHandler() - }() - go func() { - a.logger.Debug("Starting WS Health notifier...") - a.startWSHealth() - }() - return nil -}