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")
|
||||
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)
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
"upstream_host": r.URL.Host,
|
||||
"method": r.Method,
|
||||
"scheme": r.URL.Scheme,
|
||||
"host": web.GetHost(r),
|
||||
}).Observe(float64(after))
|
||||
})
|
||||
|
|
|
@ -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
|
||||
---
|
||||
|
||||
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>
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in New Issue