feat(sessions): add support for remote redis session store (#3345)

Description
====================
zot currently stores session cookies in memory or in a local directory.
For cases where the session cookies should be independent of the
instance where they were created such as multiple instances of zot, or a
fully stateless zot instance, there is a need to support a remote
session storage.
This change adds support for using Redis and Redis-compatible services as a
remote session driver as well as introduces a new configuration option
for it.

What has changed
=======================
- New config added under Auth config to specify configuration for
  the session driver.
- Examples README updated with details of the new Auth config.
- The config supports only 2 drivers in this change - local and redis
- Using the local driver is backwards compatible and behaves the same
  way that zot currently works for local session storage.
- Omitting this config does not result in an error. In this case, zot
  behaves as it normally does for local session storage.
- When configured, zot can use redis for persisting cookie
  information for zot UI.
- The cookie in the store is deleted on logout or after the max
  expiry time for the cookie.
- Configuration for the redis session driver accepts the same configuration
  values as that of the remote meta cache.
- A separate connection is established for the session driver. An
  existing connection for meta cache will not be re-used for the
  session driver.
- A key prefix is configurable for the redis session driver. The value will be
  converted into a string for use. If no value is provided, a default
  prefix of "zotsession" will be used.
- Redis sessions does not support hash key or encryption in this change.
- New BATS test added to verify zot behavior with Redis session store.
- Github workflow updated to install valkey-tools dependency for BATS.

Signed-off-by: Vishwas Rajashekar <dev@vrajashkr.com>
This commit is contained in:
Vishwas Rajashekar
2025-10-05 12:43:38 +05:30
committed by GitHub
parent cbbd39745c
commit 86af38abfc
16 changed files with 1342 additions and 48 deletions
+56
View File
@@ -332,6 +332,58 @@ func validateCacheConfig(cfg *config.Config, logger zlog.Logger) error {
return nil
}
func validateRemoteSessionStoreConfig(cfg *config.Config, logger zlog.Logger) error {
// it is okay for the session driver config to be nil
// this is backwards compatible for older configs
if cfg.HTTP.Auth.SessionDriver == nil {
return nil
}
sessionDriverName, ok := cfg.HTTP.Auth.SessionDriver["name"]
if !ok {
msg := "must provide session driver name!"
logger.Error().Err(zerr.ErrBadConfig).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
allowedDriverNames := []string{
storageConstants.RedisDriverName,
storageConstants.LocalStorageDriverName,
}
isValidDriver := false
for _, allowedDriverName := range allowedDriverNames {
if allowedDriverName == sessionDriverName {
isValidDriver = true
break
}
}
if !isValidDriver {
msg := fmt.Sprintf("session store driver %s is not allowed!", sessionDriverName)
logger.Error().Err(zerr.ErrBadConfig).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
// If the redis driver is being used, then session keys must not be configured
// as redis session store does not support these yet.
if sessionDriverName == storageConstants.RedisDriverName {
if cfg.HTTP.Auth.SessionKeysFile != "" {
msg := "session keys not supported when redis session driver is used!"
logger.Error().Err(zerr.ErrBadConfig).Msg(msg)
return fmt.Errorf("%w: %s", zerr.ErrBadConfig, msg)
}
}
return nil
}
func validateExtensionsConfig(cfg *config.Config, logger zlog.Logger) error {
if cfg.Extensions != nil && cfg.Extensions.Mgmt != nil {
logger.Warn().Msg("mgmt extensions configuration option has been made redundant and will be ignored.")
@@ -405,6 +457,10 @@ func validateConfiguration(config *config.Config, logger zlog.Logger) error {
return err
}
if err := validateRemoteSessionStoreConfig(config, logger); err != nil {
return err
}
if err := validateExtensionsConfig(config, logger); err != nil {
return err
}
+250
View File
@@ -11,6 +11,7 @@ import (
. "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.dev/zot/errors"
"zotregistry.dev/zot/pkg/api"
"zotregistry.dev/zot/pkg/api/config"
cli "zotregistry.dev/zot/pkg/cli/server"
@@ -572,6 +573,255 @@ storage:
So(err, ShouldBeNil)
})
Convey("Test session store config", t, func(c C) {
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpfile.Name())
tmpSessionKeysFile, err := os.CreateTemp("/tmp", "keys-*.json")
So(err, ShouldBeNil)
defer os.Remove(tmpSessionKeysFile.Name())
_, err = tmpSessionKeysFile.WriteString(`{
"hashKey": "my-very-secret",
"encryptKey": "another-secret"
}`,
)
So(err, ShouldBeNil)
testCases := []struct {
name string
config []byte
isValid bool
errMsg string
}{
{
"Should fail verify if session driver is enabled, but invalid driver provided",
[]byte(`{
"storage":{
"rootDirectory":"/tmp/zot"
},
"http":{
"address":"127.0.0.1",
"port":"8080",
"realm":"zot",
"auth":{
"htpasswd":{
"path":"test/data/htpasswd"
},
"failDelay":1,
"sessionDriver":{
"name": "badDriver"
}
}
},
"extensions":{
"search": {
"cve": {
"updateInterval": "2h"
}
},
"ui": {
"enable": true
}
}
}`),
false,
zerr.ErrBadConfig.Error() +
": session store driver badDriver is not allowed!",
},
{
"Should fail verify if session driver is enabled, but driver name is not provided",
[]byte(`{
"storage":{
"rootDirectory":"/tmp/zot"
},
"http":{
"address":"127.0.0.1",
"port":"8080",
"realm":"zot",
"auth":{
"htpasswd":{
"path":"test/data/htpasswd"
},
"failDelay":1,
"sessionDriver":{
"url": "redis://localhost"
}
}
},
"extensions":{
"search": {
"cve": {
"updateInterval": "2h"
}
},
"ui": {
"enable": true
}
}
}`),
false,
zerr.ErrBadConfig.Error() + ": must provide session driver name!",
},
{
"Should fail verify if session driver is enabled and sessionKeysFile present",
[]byte(fmt.Sprintf(`{
"storage":{
"rootDirectory":"/tmp/zot"
},
"http":{
"address":"127.0.0.1",
"port":"8080",
"realm":"zot",
"auth":{
"htpasswd":{
"path":"test/data/htpasswd"
},
"failDelay":1,
"sessionKeysFile": "%s",
"sessionDriver":{
"name": "redis",
"url": "redis://localhost"
}
}
},
"extensions":{
"search": {
"cve": {
"updateInterval": "2h"
}
},
"ui": {
"enable": true
}
}
}`, tmpSessionKeysFile.Name())),
false,
zerr.ErrBadConfig.Error() + ": session keys not supported when redis session driver is used!",
},
{
"Should be successful if session driver config is valid for redis",
[]byte(`{
"storage":{
"rootDirectory":"/tmp/zot"
},
"http":{
"address":"127.0.0.1",
"port":"8080",
"realm":"zot",
"auth":{
"htpasswd":{
"path":"test/data/htpasswd"
},
"failDelay":1,
"sessionDriver":{
"name": "redis",
"url": "redis://localhost"
}
}
},
"extensions":{
"search": {
"cve": {
"updateInterval": "2h"
}
},
"ui": {
"enable": true
}
}
}`),
true,
"",
},
{
"Should be successful if session driver config is valid for local",
[]byte(`{
"storage":{
"rootDirectory":"/tmp/zot"
},
"http":{
"address":"127.0.0.1",
"port":"8080",
"realm":"zot",
"auth":{
"htpasswd":{
"path":"test/data/htpasswd"
},
"failDelay":1,
"sessionDriver":{
"name": "local"
}
}
},
"extensions":{
"search": {
"cve": {
"updateInterval": "2h"
}
},
"ui": {
"enable": true
}
}
}`),
true,
"",
},
{
"Should be successful if session driver config is missing",
[]byte(`{
"storage":{
"rootDirectory":"/tmp/zot"
},
"http":{
"address":"127.0.0.1",
"port":"8080",
"realm":"zot",
"auth":{
"htpasswd":{
"path":"test/data/htpasswd"
},
"failDelay":1
}
},
"extensions":{
"search": {
"cve": {
"updateInterval": "2h"
}
},
"ui": {
"enable": true
}
}
}`),
true,
"",
},
}
for _, testCase := range testCases {
Convey(testCase.name, func() {
err = os.WriteFile(tmpfile.Name(), testCase.config, 0o0600)
So(err, ShouldBeNil)
os.Args = []string{"cli_test", "verify", tmpfile.Name()}
err = cli.NewServerRootCmd().Execute()
if testCase.isValid {
So(err, ShouldBeNil)
} else {
So(err, ShouldNotBeNil)
So(err.Error(), ShouldEqual, testCase.errMsg)
}
})
}
})
Convey("Test verify with bad gc retention repo patterns", t, func(c C) {
tmpfile, err := os.CreateTemp("", "zot-test*.json")
So(err, ShouldBeNil)