providers/proxy: outpost wide logout implementation (#4605)
* initial outpost wide logout implementation Signed-off-by: Jens Langhammer <jens@goauthentik.io> * handle deserialize error Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix file cleanup, add tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
798245b8db
commit
7d4ce41e12
|
@ -46,5 +46,6 @@
|
||||||
"url": "https://github.com/goauthentik/authentik/issues/<num>",
|
"url": "https://github.com/goauthentik/authentik/issues/<num>",
|
||||||
"ignoreCase": false
|
"ignoreCase": false
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"go.testFlags": ["-count=1"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,21 +237,22 @@ func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) {
|
||||||
redirect := a.endpoint.EndSessionEndpoint
|
redirect := a.endpoint.EndSessionEndpoint
|
||||||
s, err := a.sessions.Get(r, constants.SessionName)
|
s, err := a.sessions.Get(r, constants.SessionName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Redirect(rw, r, redirect, http.StatusFound)
|
a.redirectToStart(rw, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c, exists := s.Values[constants.SessionClaims]; c == nil || !exists {
|
c, exists := s.Values[constants.SessionClaims]
|
||||||
cc := c.(Claims)
|
if c == nil && !exists {
|
||||||
uv := url.Values{
|
a.redirectToStart(rw, r)
|
||||||
"id_token_hint": []string{cc.RawToken},
|
return
|
||||||
}
|
|
||||||
redirect += "?" + uv.Encode()
|
|
||||||
}
|
}
|
||||||
s.Options.MaxAge = -1
|
cc := c.(Claims)
|
||||||
err = s.Save(r, rw)
|
uv := url.Values{
|
||||||
|
"id_token_hint": []string{cc.RawToken},
|
||||||
|
}
|
||||||
|
redirect += "?" + uv.Encode()
|
||||||
|
err = a.Logout(cc.Sub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Redirect(rw, r, redirect, http.StatusFound)
|
a.log.WithError(err).Warning("failed to logout of other sessions")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
http.Redirect(rw, r, redirect, http.StatusFound)
|
http.Redirect(rw, r, redirect, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"goauthentik.io/api/v3"
|
"goauthentik.io/api/v3"
|
||||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||||
|
@ -69,6 +70,8 @@ func TestForwardHandleCaddy_Single_Claims(t *testing.T) {
|
||||||
a.forwardHandleCaddy(rr, req)
|
a.forwardHandleCaddy(rr, req)
|
||||||
|
|
||||||
s, _ := a.sessions.Get(req, constants.SessionName)
|
s, _ := a.sessions.Get(req, constants.SessionName)
|
||||||
|
s.ID = uuid.New().String()
|
||||||
|
s.Options.MaxAge = 86400
|
||||||
s.Values[constants.SessionClaims] = Claims{
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
Sub: "foo",
|
Sub: "foo",
|
||||||
Proxy: &ProxyClaims{
|
Proxy: &ProxyClaims{
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"goauthentik.io/api/v3"
|
"goauthentik.io/api/v3"
|
||||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||||
|
@ -51,6 +52,8 @@ func TestForwardHandleEnvoy_Single_Claims(t *testing.T) {
|
||||||
a.forwardHandleEnvoy(rr, req)
|
a.forwardHandleEnvoy(rr, req)
|
||||||
|
|
||||||
s, _ := a.sessions.Get(req, constants.SessionName)
|
s, _ := a.sessions.Get(req, constants.SessionName)
|
||||||
|
s.ID = uuid.New().String()
|
||||||
|
s.Options.MaxAge = 86400
|
||||||
s.Values[constants.SessionClaims] = Claims{
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
Sub: "foo",
|
Sub: "foo",
|
||||||
Proxy: &ProxyClaims{
|
Proxy: &ProxyClaims{
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"goauthentik.io/api/v3"
|
"goauthentik.io/api/v3"
|
||||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||||
|
@ -68,6 +69,8 @@ func TestForwardHandleNginx_Single_Claims(t *testing.T) {
|
||||||
a.forwardHandleNginx(rr, req)
|
a.forwardHandleNginx(rr, req)
|
||||||
|
|
||||||
s, _ := a.sessions.Get(req, constants.SessionName)
|
s, _ := a.sessions.Get(req, constants.SessionName)
|
||||||
|
s.ID = uuid.New().String()
|
||||||
|
s.Options.MaxAge = 86400
|
||||||
s.Values[constants.SessionClaims] = Claims{
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
Sub: "foo",
|
Sub: "foo",
|
||||||
Proxy: &ProxyClaims{
|
Proxy: &ProxyClaims{
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"goauthentik.io/api/v3"
|
"goauthentik.io/api/v3"
|
||||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||||
|
@ -69,6 +70,8 @@ func TestForwardHandleTraefik_Single_Claims(t *testing.T) {
|
||||||
a.forwardHandleTraefik(rr, req)
|
a.forwardHandleTraefik(rr, req)
|
||||||
|
|
||||||
s, _ := a.sessions.Get(req, constants.SessionName)
|
s, _ := a.sessions.Get(req, constants.SessionName)
|
||||||
|
s.ID = uuid.New().String()
|
||||||
|
s.Options.MaxAge = 86400
|
||||||
s.Values[constants.SessionClaims] = Claims{
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
Sub: "foo",
|
Sub: "foo",
|
||||||
Proxy: &ProxyClaims{
|
Proxy: &ProxyClaims{
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||||
)
|
)
|
||||||
|
@ -70,6 +71,8 @@ func TestProxy_ModifyRequest_Claims(t *testing.T) {
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
s, _ := a.sessions.Get(req, constants.SessionName)
|
s, _ := a.sessions.Get(req, constants.SessionName)
|
||||||
|
s.ID = uuid.New().String()
|
||||||
|
s.Options.MaxAge = 86400
|
||||||
s.Values[constants.SessionClaims] = Claims{
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
Sub: "foo",
|
Sub: "foo",
|
||||||
Proxy: &ProxyClaims{
|
Proxy: &ProxyClaims{
|
||||||
|
@ -98,6 +101,8 @@ func TestProxy_ModifyRequest_Claims_Invalid(t *testing.T) {
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
s, _ := a.sessions.Get(req, constants.SessionName)
|
s, _ := a.sessions.Get(req, constants.SessionName)
|
||||||
|
s.ID = uuid.New().String()
|
||||||
|
s.Options.MaxAge = 86400
|
||||||
s.Values[constants.SessionClaims] = Claims{
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
Sub: "foo",
|
Sub: "foo",
|
||||||
Proxy: &ProxyClaims{
|
Proxy: &ProxyClaims{
|
||||||
|
|
|
@ -5,14 +5,21 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/garyburd/redigo/redis"
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"goauthentik.io/api/v3"
|
"goauthentik.io/api/v3"
|
||||||
"goauthentik.io/internal/config"
|
"goauthentik.io/internal/config"
|
||||||
|
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||||
"gopkg.in/boj/redistore.v1"
|
"gopkg.in/boj/redistore.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const RedisKeyPrefix = "authentik_proxy_session_"
|
||||||
|
|
||||||
func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) sessions.Store {
|
func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) sessions.Store {
|
||||||
var store sessions.Store
|
var store sessions.Store
|
||||||
if config.Get().Redis.Host != "" {
|
if config.Get().Redis.Host != "" {
|
||||||
|
@ -21,7 +28,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
rs.SetMaxLength(math.MaxInt)
|
rs.SetMaxLength(math.MaxInt)
|
||||||
rs.SetKeyPrefix("authentik_proxy_session_")
|
rs.SetKeyPrefix(RedisKeyPrefix)
|
||||||
if p.TokenValidity.IsSet() {
|
if p.TokenValidity.IsSet() {
|
||||||
t := p.TokenValidity.Get()
|
t := p.TokenValidity.Get()
|
||||||
// Add one to the validity to ensure we don't have a session with indefinite length
|
// Add one to the validity to ensure we don't have a session with indefinite length
|
||||||
|
@ -55,3 +62,86 @@ func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL)
|
||||||
}
|
}
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Application) Logout(sub string) error {
|
||||||
|
if fs, ok := a.sessions.(*sessions.FilesystemStore); ok {
|
||||||
|
files, err := os.ReadDir(os.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
s := sessions.Session{}
|
||||||
|
if !strings.HasPrefix(file.Name(), "session_") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fullPath := path.Join(os.TempDir(), file.Name())
|
||||||
|
data, err := os.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
a.log.WithError(err).Warning("failed to read file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = securecookie.DecodeMulti(
|
||||||
|
constants.SessionName, string(data),
|
||||||
|
&s.Values, fs.Codecs...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
a.log.WithError(err).Trace("failed to decode session")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
claims := s.Values[constants.SessionClaims].(Claims)
|
||||||
|
if claims.Sub == sub {
|
||||||
|
a.log.WithField("path", fullPath).Trace("deleting session")
|
||||||
|
err := os.Remove(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
a.log.WithError(err).Warning("failed to delete session")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rs, ok := a.sessions.(*redistore.RediStore); ok {
|
||||||
|
pool := rs.Pool.Get()
|
||||||
|
defer pool.Close()
|
||||||
|
rep, err := pool.Do("KEYS", fmt.Sprintf("%s*", RedisKeyPrefix))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keys, err := redis.Strings(rep, err)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ser := redistore.GobSerializer{}
|
||||||
|
for _, key := range keys {
|
||||||
|
v, err := pool.Do("GET", key)
|
||||||
|
if err != nil {
|
||||||
|
a.log.WithError(err).Warning("failed to get value")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b, err := redis.Bytes(v, err)
|
||||||
|
if err != nil {
|
||||||
|
a.log.WithError(err).Warning("failed to load value")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s := sessions.Session{}
|
||||||
|
err = ser.Deserialize(b, &s)
|
||||||
|
if err != nil {
|
||||||
|
a.log.WithError(err).Warning("failed to deserialize")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c := s.Values[constants.SessionClaims]
|
||||||
|
if c == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
claims := c.(Claims)
|
||||||
|
if claims.Sub == sub {
|
||||||
|
a.log.WithField("key", key).Trace("deleting session")
|
||||||
|
_, err := pool.Do("DEL", key)
|
||||||
|
if err != nil {
|
||||||
|
a.log.WithError(err).Warning("failed to delete key")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogout(t *testing.T) {
|
||||||
|
a := newTestApplication()
|
||||||
|
_ = a.configureProxy()
|
||||||
|
req, _ := http.NewRequest("GET", "https://ext.t.goauthentik.io/foo", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// Login once
|
||||||
|
s, _ := a.sessions.Get(req, constants.SessionName)
|
||||||
|
s.ID = uuid.New().String()
|
||||||
|
s.Options.MaxAge = 86400
|
||||||
|
s.Values[constants.SessionClaims] = Claims{
|
||||||
|
Sub: "foo",
|
||||||
|
}
|
||||||
|
err := a.sessions.Save(req, rr, s)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.mux.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadGateway, rr.Code)
|
||||||
|
|
||||||
|
// Login twice
|
||||||
|
s2, _ := a.sessions.Get(req, constants.SessionName)
|
||||||
|
s2.ID = uuid.New().String()
|
||||||
|
s2.Options.MaxAge = 86400
|
||||||
|
s2.Values[constants.SessionClaims] = Claims{
|
||||||
|
Sub: "foo",
|
||||||
|
}
|
||||||
|
err = a.sessions.Save(req, rr, s2)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.mux.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadGateway, rr.Code)
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
req, _ = http.NewRequest("GET", "https://ext.t.goauthentik.io/outpost.goauthentik.io/sign_out", nil)
|
||||||
|
s3, _ := a.sessions.Get(req, constants.SessionName)
|
||||||
|
s3.ID = uuid.New().String()
|
||||||
|
s3.Options.MaxAge = 86400
|
||||||
|
s3.Values[constants.SessionClaims] = Claims{
|
||||||
|
Sub: "foo",
|
||||||
|
}
|
||||||
|
err = a.sessions.Save(req, rr, s3)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
a.handleSignOut(rr, req)
|
||||||
|
assert.Equal(t, http.StatusFound, rr.Code)
|
||||||
|
|
||||||
|
s1Name := filepath.Join(os.TempDir(), "session_"+s.ID)
|
||||||
|
_, err = os.Stat(s1Name)
|
||||||
|
assert.True(t, errors.Is(err, os.ErrNotExist))
|
||||||
|
s2Name := filepath.Join(os.TempDir(), "session_"+s2.ID)
|
||||||
|
_, err = os.Stat(s2Name)
|
||||||
|
assert.True(t, errors.Is(err, os.ErrNotExist))
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package application
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/quasoft/memstore"
|
|
||||||
"goauthentik.io/api/v3"
|
"goauthentik.io/api/v3"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
)
|
)
|
||||||
|
@ -41,8 +40,5 @@ func newTestApplication() *Application {
|
||||||
ak.MockConfig(),
|
ak.MockConfig(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
a.sessions = memstore.NewMemStore(
|
|
||||||
[]byte(ak.TestSecret()),
|
|
||||||
)
|
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,15 +74,15 @@ func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func (a *Application) redirect(rw http.ResponseWriter, r *http.Request) {
|
func (a *Application) redirect(rw http.ResponseWriter, r *http.Request) {
|
||||||
redirect := a.proxyConfig.ExternalHost
|
redirect := a.proxyConfig.ExternalHost
|
||||||
rd, ok := a.checkRedirectParam(r)
|
|
||||||
if ok {
|
|
||||||
redirect = rd
|
|
||||||
}
|
|
||||||
s, _ := a.sessions.Get(r, constants.SessionName)
|
s, _ := a.sessions.Get(r, constants.SessionName)
|
||||||
redirectR, ok := s.Values[constants.SessionRedirect]
|
redirectR, ok := s.Values[constants.SessionRedirect]
|
||||||
if ok {
|
if ok {
|
||||||
redirect = redirectR.(string)
|
redirect = redirectR.(string)
|
||||||
}
|
}
|
||||||
|
rd, ok := a.checkRedirectParam(r)
|
||||||
|
if ok {
|
||||||
|
redirect = rd
|
||||||
|
}
|
||||||
a.log.WithField("redirect", redirect).Trace("final redirect")
|
a.log.WithField("redirect", redirect).Trace("final redirect")
|
||||||
http.Redirect(rw, r, redirect, http.StatusFound)
|
http.Redirect(rw, r, redirect, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,8 @@ When using domain-level mode, navigate to `auth.domain.tld/outpost.goauthentik.i
|
||||||
|
|
||||||
To log out, navigate to `/outpost.goauthentik.io/sign_out`.
|
To log out, navigate to `/outpost.goauthentik.io/sign_out`.
|
||||||
|
|
||||||
|
Starting with authentik 2023.2, when logging out of a provider, all the users sessions within the respective outpost are invalidated.
|
||||||
|
|
||||||
## Allowing unauthenticated requests
|
## Allowing unauthenticated requests
|
||||||
|
|
||||||
To allow un-authenticated requests to certain paths/URLs, you can use the _Unauthenticated URLs_ / _Unauthenticated Paths_ field.
|
To allow un-authenticated requests to certain paths/URLs, you can use the _Unauthenticated URLs_ / _Unauthenticated Paths_ field.
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
title: Release 2023.2
|
||||||
|
slug: "/releases/2023.2"
|
||||||
|
---
|
||||||
|
|
||||||
|
## New features
|
||||||
|
|
||||||
|
- Proxy provider logout improvements
|
||||||
|
|
||||||
|
In previous versions, logging out of a single proxied application would only invalidate that application's session. Starting with this release, when logging out of a proxied application (via the _/outpost.goauthentik.io/sign_out_ URL), all the users session within the outpost are terminated. Sessions in other outposts and with other protocols are unaffected.
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
This release does not introduce any new requirements.
|
||||||
|
|
||||||
|
### docker-compose
|
||||||
|
|
||||||
|
Download the docker-compose file for 2023.2 from [here](https://goauthentik.io/version/2023.2/docker-compose.yml). Afterwards, simply run `docker-compose up -d`.
|
||||||
|
|
||||||
|
### Kubernetes
|
||||||
|
|
||||||
|
Update your values to use the new images:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/goauthentik/server
|
||||||
|
tag: 2023.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Minor changes/fixes
|
||||||
|
|
||||||
|
## API Changes
|
||||||
|
|
||||||
|
_Insert output of `make gen-diff` here_
|
Reference in New Issue