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
+3 -2
View File
@@ -75,8 +75,9 @@ type AuthConfig struct {
OpenID *OpenIDConfig
APIKey bool
SessionKeysFile string
SessionHashKey []byte `json:"-"`
SessionEncryptKey []byte `json:"-"`
SessionHashKey []byte `json:"-"`
SessionEncryptKey []byte `json:"-"`
SessionDriver map[string]any `mapstructure:",omitempty"`
}
type BearerConfig struct {
+8 -8
View File
@@ -81,7 +81,7 @@ func ParseRedisUniversalOptions(redisConfig map[string]interface{}, //nolint: go
opts.Addrs = val
}
if val, ok := getString(redisConfig, "client_name", false, log); ok {
if val, ok := GetString(redisConfig, "client_name", false, log); ok {
opts.ClientName = val
}
@@ -93,19 +93,19 @@ func ParseRedisUniversalOptions(redisConfig map[string]interface{}, //nolint: go
opts.Protocol = val
}
if val, ok := getString(redisConfig, "username", false, log); ok {
if val, ok := GetString(redisConfig, "username", false, log); ok {
opts.Username = val
}
if val, ok := getString(redisConfig, "password", true, log); ok {
if val, ok := GetString(redisConfig, "password", true, log); ok {
opts.Password = val
}
if val, ok := getString(redisConfig, "sentinel_username", false, log); ok {
if val, ok := GetString(redisConfig, "sentinel_username", false, log); ok {
opts.SentinelUsername = val
}
if val, ok := getString(redisConfig, "sentinel_password", true, log); ok {
if val, ok := GetString(redisConfig, "sentinel_password", true, log); ok {
opts.SentinelPassword = val
}
@@ -185,7 +185,7 @@ func ParseRedisUniversalOptions(redisConfig map[string]interface{}, //nolint: go
opts.RouteRandomly = val
}
if val, ok := getString(redisConfig, "master_name", false, log); ok {
if val, ok := GetString(redisConfig, "master_name", false, log); ok {
opts.MasterName = val
}
@@ -193,7 +193,7 @@ func ParseRedisUniversalOptions(redisConfig map[string]interface{}, //nolint: go
opts.DisableIdentity = val
}
if val, ok := getString(redisConfig, "identity_suffix", false, log); ok {
if val, ok := GetString(redisConfig, "identity_suffix", false, log); ok {
opts.IdentitySuffix = val
}
@@ -246,7 +246,7 @@ func getInt(dict map[string]interface{}, key string, log log.Logger) (int, bool)
return ret, true
}
func getString(dict map[string]interface{}, key string, hideValue bool, log log.Logger) (string, bool) {
func GetString(dict map[string]interface{}, key string, hideValue bool, log log.Logger) (string, bool) {
value, ok := dict[key]
if !ok {
return "", false