outposts/proxyv2: add basic envoy support (#3026)

* outposts/proxyv2: add basic envoy support

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* don't crash when backend is not available

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

* add envoy tests and docs

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2022-06-03 00:06:09 +02:00 committed by GitHub
parent 8f0572d11e
commit f9a419107a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 231 additions and 5 deletions

View file

@ -13,6 +13,7 @@ func EnableDebugServer() {
l := log.WithField("logger", "authentik.go_debugger") l := log.WithField("logger", "authentik.go_debugger")
if deb := os.Getenv("AUTHENTIK_DEBUG"); strings.ToLower(deb) != "true" { if deb := os.Getenv("AUTHENTIK_DEBUG"); strings.ToLower(deb) != "true" {
l.Info("not enabling debug server, set `AUTHENTIK_DEBUG` to `true` to enable it.") l.Info("not enabling debug server, set `AUTHENTIK_DEBUG` to `true` to enable it.")
return
} }
h := http.NewServeMux() h := http.NewServeMux()
h.HandleFunc("/debug/pprof/", pprof.Index) h.HandleFunc("/debug/pprof/", pprof.Index)

View file

@ -68,8 +68,9 @@ func NewAPIController(akURL url.URL, token string) *APIController {
outposts, _, err := apiClient.OutpostsApi.OutpostsInstancesList(context.Background()).Execute() outposts, _, err := apiClient.OutpostsApi.OutpostsInstancesList(context.Background()).Execute()
if err != nil { if err != nil {
log.WithError(err).Error("Failed to fetch outpost configuration") log.WithError(err).Error("Failed to fetch outpost configuration, retrying in 3 seconds")
return nil time.Sleep(time.Second * 3)
return NewAPIController(akURL, token)
} }
outpost := outposts.Results[0] outpost := outposts.Results[0]

View file

@ -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/traefik", a.forwardHandleTraefik)
a.mux.HandleFunc("/outpost.goauthentik.io/auth/nginx", a.forwardHandleNginx) a.mux.HandleFunc("/outpost.goauthentik.io/auth/nginx", a.forwardHandleNginx)
a.mux.PathPrefix("/").HandlerFunc(a.forwardHandleEnvoy)
return nil return nil
} }
@ -126,3 +127,54 @@ func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request
} }
http.Error(rw, "unauthorized request", http.StatusUnauthorized) 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)
}

View file

@ -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])
}

View file

@ -58,6 +58,7 @@ func (a *Application) configureProxy() error {
"outpost_name": a.outpostName, "outpost_name": a.outpostName,
"upstream_host": r.URL.Host, "upstream_host": r.URL.Host,
"method": r.Method, "method": r.Method,
"scheme": r.URL.Scheme,
"host": web.GetHost(r), "host": web.GetHost(r),
}).Observe(float64(after)) }).Observe(float64(after))
}) })

View file

@ -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 <service-name>.<namespace>.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"
```

View file

@ -2,6 +2,9 @@
title: Forward auth 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 Using forward auth uses your existing reverse proxy to do the proxying, and only uses the
authentik outpost to check authentication and authorization. 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 ## Nginx
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
<Tabs <Tabs
defaultValue="standalone-nginx" defaultValue="standalone-nginx"
values={[ values={[
@ -106,3 +106,27 @@ import TraefikIngress from "./_traefik_ingress.md";
</TabItem> </TabItem>
</Tabs> </Tabs>
## 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).
:::
<Tabs
defaultValue="envoy-istio"
values={[
{label: 'Envoy (Istio)', value: 'envoy-istio'},
]}>
<TabItem value="envoy-istio">
import EnvoyIstio from "./_envoy_istio.md";
<EnvoyIstio />
</TabItem>
</Tabs>

View file

@ -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. - 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 ## Minor changes/fixes
## Upgrading ## Upgrading