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:
parent
8f0572d11e
commit
f9a419107a
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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])
|
||||||
|
}
|
|
@ -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))
|
||||||
})
|
})
|
||||||
|
|
47
website/docs/providers/proxy/_envoy_istio.md
Normal file
47
website/docs/providers/proxy/_envoy_istio.md
Normal 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"
|
||||||
|
```
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
Reference in a new issue