From f9a419107a3612b0a226b2160d99d12723351267 Mon Sep 17 00:00:00 2001 From: Jens L Date: Fri, 3 Jun 2022 00:06:09 +0200 Subject: [PATCH] outposts/proxyv2: add basic envoy support (#3026) * outposts/proxyv2: add basic envoy support Signed-off-by: Jens Langhammer * don't crash when backend is not available Signed-off-by: Jens Langhammer * add envoy tests and docs Signed-off-by: Jens Langhammer --- internal/debug/debug.go | 1 + internal/outpost/ak/api.go | 5 +- .../proxyv2/application/mode_forward.go | 52 ++++++++++ .../application/mode_forward_envoy_test.go | 98 +++++++++++++++++++ .../outpost/proxyv2/application/mode_proxy.go | 1 + website/docs/providers/proxy/_envoy_istio.md | 47 +++++++++ website/docs/providers/proxy/forward_auth.mdx | 30 +++++- website/docs/releases/v2022.6.md | 2 + 8 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 internal/outpost/proxyv2/application/mode_forward_envoy_test.go create mode 100644 website/docs/providers/proxy/_envoy_istio.md diff --git a/internal/debug/debug.go b/internal/debug/debug.go index e8cb2ad61..231ba005d 100644 --- a/internal/debug/debug.go +++ b/internal/debug/debug.go @@ -13,6 +13,7 @@ func EnableDebugServer() { l := log.WithField("logger", "authentik.go_debugger") if deb := os.Getenv("AUTHENTIK_DEBUG"); strings.ToLower(deb) != "true" { l.Info("not enabling debug server, set `AUTHENTIK_DEBUG` to `true` to enable it.") + return } h := http.NewServeMux() h.HandleFunc("/debug/pprof/", pprof.Index) diff --git a/internal/outpost/ak/api.go b/internal/outpost/ak/api.go index 5b495d8c0..68d418c7a 100644 --- a/internal/outpost/ak/api.go +++ b/internal/outpost/ak/api.go @@ -68,8 +68,9 @@ func NewAPIController(akURL url.URL, token string) *APIController { outposts, _, err := apiClient.OutpostsApi.OutpostsInstancesList(context.Background()).Execute() if err != nil { - log.WithError(err).Error("Failed to fetch outpost configuration") - return nil + log.WithError(err).Error("Failed to fetch outpost configuration, retrying in 3 seconds") + time.Sleep(time.Second * 3) + return NewAPIController(akURL, token) } outpost := outposts.Results[0] diff --git a/internal/outpost/proxyv2/application/mode_forward.go b/internal/outpost/proxyv2/application/mode_forward.go index d61522cfe..1be5e9fe7 100644 --- a/internal/outpost/proxyv2/application/mode_forward.go +++ b/internal/outpost/proxyv2/application/mode_forward.go @@ -21,6 +21,7 @@ func (a *Application) configureForward() error { }) a.mux.HandleFunc("/outpost.goauthentik.io/auth/traefik", a.forwardHandleTraefik) a.mux.HandleFunc("/outpost.goauthentik.io/auth/nginx", a.forwardHandleNginx) + a.mux.PathPrefix("/").HandlerFunc(a.forwardHandleEnvoy) return nil } @@ -126,3 +127,54 @@ func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request } http.Error(rw, "unauthorized request", http.StatusUnauthorized) } + +func (a *Application) forwardHandleEnvoy(rw http.ResponseWriter, r *http.Request) { + a.log.WithField("header", r.Header).Trace("tracing headers for debug") + fwd := r.URL + + claims, err := a.getClaims(r) + if claims != nil && err == nil { + a.addHeaders(rw.Header(), claims) + rw.Header().Set("User-Agent", r.Header.Get("User-Agent")) + a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth") + return + } else if claims == nil && a.IsAllowlisted(fwd) { + a.log.Trace("path can be accessed without authentication") + return + } + if strings.HasPrefix(r.URL.Path, "/outpost.goauthentik.io") { + a.log.WithField("url", r.URL.String()).Trace("path begins with /outpost.goauthentik.io, allowing access") + return + } + host := "" + s, _ := a.sessions.Get(r, constants.SessionName) + // Optional suffix, which is appended to the URL + if *a.proxyConfig.Mode.Get() == api.PROXYMODE_FORWARD_SINGLE { + host = web.GetHost(r) + } else if *a.proxyConfig.Mode.Get() == api.PROXYMODE_FORWARD_DOMAIN { + eh, err := url.Parse(a.proxyConfig.ExternalHost) + if err != nil { + a.log.WithField("host", a.proxyConfig.ExternalHost).WithError(err).Warning("invalid external_host") + } else { + host = eh.Host + } + } + // set the redirect flag to the current URL we have, since we redirect + // to a (possibly) different domain, but we want to be redirected back + // to the application + // X-Forwarded-Uri is only the path, so we need to build the entire URL + s.Values[constants.SessionRedirect] = fwd.String() + err = s.Save(r, rw) + if err != nil { + a.log.WithError(err).Warning("failed to save session before redirect") + } + // We mostly can't rely on X-Forwarded-Proto here since in most cases that will come from the + // local Envoy sidecar, so we re-used the same proto as the original URL had + scheme := r.Header.Get("X-Forwarded-Proto") + if scheme == "" { + scheme = "http:" + } + rdFinal := fmt.Sprintf("%s//%s%s", scheme, host, "/outpost.goauthentik.io/start") + a.log.WithField("url", rdFinal).Debug("Redirecting to login") + http.Redirect(rw, r, rdFinal, http.StatusTemporaryRedirect) +} diff --git a/internal/outpost/proxyv2/application/mode_forward_envoy_test.go b/internal/outpost/proxyv2/application/mode_forward_envoy_test.go new file mode 100644 index 000000000..83f2107d9 --- /dev/null +++ b/internal/outpost/proxyv2/application/mode_forward_envoy_test.go @@ -0,0 +1,98 @@ +package application + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "goauthentik.io/api/v3" + "goauthentik.io/internal/outpost/proxyv2/constants" +) + +func TestForwardHandleEnvoy_Single_Skip(t *testing.T) { + a := newTestApplication() + req, _ := http.NewRequest("GET", "http://test.goauthentik.io/skip", nil) + + rr := httptest.NewRecorder() + a.forwardHandleEnvoy(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) +} + +func TestForwardHandleEnvoy_Single_Headers(t *testing.T) { + a := newTestApplication() + req, _ := http.NewRequest("GET", "http://test.goauthentik.io/app", nil) + + rr := httptest.NewRecorder() + a.forwardHandleEnvoy(rr, req) + + assert.Equal(t, rr.Code, http.StatusTemporaryRedirect) + loc, _ := rr.Result().Location() + assert.Equal(t, loc.String(), "http://test.goauthentik.io/outpost.goauthentik.io/start") + + s, _ := a.sessions.Get(req, constants.SessionName) + assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) +} + +func TestForwardHandleEnvoy_Single_Claims(t *testing.T) { + a := newTestApplication() + req, _ := http.NewRequest("GET", "http://test.goauthentik.io/app", nil) + + rr := httptest.NewRecorder() + a.forwardHandleEnvoy(rr, req) + + s, _ := a.sessions.Get(req, constants.SessionName) + s.Values[constants.SessionClaims] = Claims{ + Sub: "foo", + Proxy: &ProxyClaims{ + UserAttributes: map[string]interface{}{ + "username": "foo", + "password": "bar", + "additionalHeaders": map[string]interface{}{ + "foo": "bar", + }, + }, + }, + } + err := a.sessions.Save(req, rr, s) + if err != nil { + panic(err) + } + + rr = httptest.NewRecorder() + a.forwardHandleEnvoy(rr, req) + + h := rr.Result().Header + + assert.Equal(t, []string{"Basic Zm9vOmJhcg=="}, h["Authorization"]) + assert.Equal(t, []string{"bar"}, h["Foo"]) + assert.Equal(t, []string{""}, h["User-Agent"]) + assert.Equal(t, []string{""}, h["X-Authentik-Email"]) + assert.Equal(t, []string{""}, h["X-Authentik-Groups"]) + assert.Equal(t, []string{""}, h["X-Authentik-Jwt"]) + assert.Equal(t, []string{""}, h["X-Authentik-Meta-App"]) + assert.Equal(t, []string{""}, h["X-Authentik-Meta-Jwks"]) + assert.Equal(t, []string{""}, h["X-Authentik-Meta-Outpost"]) + assert.Equal(t, []string{""}, h["X-Authentik-Name"]) + assert.Equal(t, []string{"foo"}, h["X-Authentik-Uid"]) + assert.Equal(t, []string{""}, h["X-Authentik-Username"]) +} + +func TestForwardHandleEnvoy_Domain_Header(t *testing.T) { + a := newTestApplication() + a.proxyConfig.Mode = *api.NewNullableProxyMode(api.PROXYMODE_FORWARD_DOMAIN.Ptr()) + a.proxyConfig.CookieDomain = api.PtrString("foo") + a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io" + req, _ := http.NewRequest("GET", "http://test.goauthentik.io/app", nil) + + rr := httptest.NewRecorder() + a.forwardHandleEnvoy(rr, req) + + assert.Equal(t, http.StatusTemporaryRedirect, rr.Code) + loc, _ := rr.Result().Location() + assert.Equal(t, "http://auth.test.goauthentik.io/outpost.goauthentik.io/start", loc.String()) + + s, _ := a.sessions.Get(req, constants.SessionName) + assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) +} diff --git a/internal/outpost/proxyv2/application/mode_proxy.go b/internal/outpost/proxyv2/application/mode_proxy.go index dc58d5158..5d9f907ae 100644 --- a/internal/outpost/proxyv2/application/mode_proxy.go +++ b/internal/outpost/proxyv2/application/mode_proxy.go @@ -58,6 +58,7 @@ func (a *Application) configureProxy() error { "outpost_name": a.outpostName, "upstream_host": r.URL.Host, "method": r.Method, + "scheme": r.URL.Scheme, "host": web.GetHost(r), }).Observe(float64(after)) }) diff --git a/website/docs/providers/proxy/_envoy_istio.md b/website/docs/providers/proxy/_envoy_istio.md new file mode 100644 index 000000000..c321b6221 --- /dev/null +++ b/website/docs/providers/proxy/_envoy_istio.md @@ -0,0 +1,47 @@ +Set the following settings on the _IstioOperator_ resource: + +```yaml +apiVersion: install.istio.io/v1alpha1 +kind: IstioOperator +metadata: + name: istio + namespace: istio-system +spec: + meshConfig: + extensionProviders: + - name: "authentik" + envoyExtAuthzHttp: + # Replace with ..svc.cluster.local + service: "ak-outpost-authentik-embedded-outpost.authentik.svc.cluster.local" + port: "9000" + headersToDownstreamOnAllow: + - cookie + headersToUpstreamOnAllow: + - set-cookie + - x-authentik-* + includeRequestHeadersInCheck: + - cookie +``` + +Afterwards, you can create _AuthorizationPolicy_ resources to protect your applications like this: + +```yaml +apiVersion: security.istio.io/v1beta1 +kind: AuthorizationPolicy +metadata: + name: authentik-policy + namespace: istio-system +spec: + selector: + matchLabels: + istio: ingressgateway + action: CUSTOM + provider: + name: "authentik" + rules: + - to: + - operation: + hosts: + # You can create a single resource and list all Domain names here, or create multiple resources + - "app.company" +``` diff --git a/website/docs/providers/proxy/forward_auth.mdx b/website/docs/providers/proxy/forward_auth.mdx index 76547dda3..9de3a934e 100644 --- a/website/docs/providers/proxy/forward_auth.mdx +++ b/website/docs/providers/proxy/forward_auth.mdx @@ -2,6 +2,9 @@ title: Forward auth --- +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + Using forward auth uses your existing reverse proxy to do the proxying, and only uses the authentik outpost to check authentication and authorization. @@ -42,9 +45,6 @@ _outpost.company_ is used as a placeholder for the outpost. When using the embed ## Nginx -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; - + +## Enovy (Istio) + +:::info +Requires authentik 2022.6 +::: + +:::info +Support for this is still in preview, please report bugs on [GitHub](https://github.com/goauthentik/authentik/issues). +::: + + + + +import EnvoyIstio from "./_envoy_istio.md"; + + + + + diff --git a/website/docs/releases/v2022.6.md b/website/docs/releases/v2022.6.md index 81e60bdf0..4a397a8f9 100644 --- a/website/docs/releases/v2022.6.md +++ b/website/docs/releases/v2022.6.md @@ -23,6 +23,8 @@ slug: "2022.6" - The LDAP outpost would incorrectly return `groupOfUniqueNames` as a group class when the members where returned in a manner like `groupOfNames` requires. `groupOfNames` has been added as an objectClass for LDAP Groups, and `groupOfUniqueNames` will be removed in the next version. +- Preview support for forward auth when using Envoy + ## Minor changes/fixes ## Upgrading