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")
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)

View file

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

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/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)
}

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,
"upstream_host": r.URL.Host,
"method": r.Method,
"scheme": r.URL.Scheme,
"host": web.GetHost(r),
}).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
---
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";
<Tabs
defaultValue="standalone-nginx"
values={[
@ -106,3 +106,27 @@ import TraefikIngress from "./_traefik_ingress.md";
</TabItem>
</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.
- Preview support for forward auth when using Envoy
## Minor changes/fixes
## Upgrading