Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,33 @@ type EventsConfig struct {
// variables, individual fields are not documented here; instead, see the `README.md` section on
// configuration.
type RedisConfig struct {
Host string `conf:"REDIS_HOST"`
Port ct.OptIntGreaterThanZero
URL ct.OptURLAbsolute `conf:"REDIS_URL"`
LocalTTL ct.OptDuration `conf:"CACHE_TTL"`
TLS bool `conf:"REDIS_TLS"`
Username string `conf:"REDIS_USERNAME"`
Password string `conf:"REDIS_PASSWORD"`
Host string `conf:"REDIS_HOST"`
Port ct.OptIntGreaterThanZero
URL ct.OptURLAbsolute `conf:"REDIS_URL"`
LocalTTL ct.OptDuration `conf:"CACHE_TTL"`
TLS bool `conf:"REDIS_TLS"`
Username string `conf:"REDIS_USERNAME"`
Password string `conf:"REDIS_PASSWORD"`
AWSAuth bool `conf:"REDIS_AWS_AUTH"`
AWSCacheName string `conf:"REDIS_AWS_CACHE_NAME"`
// AWSRegion overrides the AWS region used for SigV4 signing of ElastiCache IAM
// authentication tokens. If empty, the region is resolved from the standard AWS
// credential chain (AWS_DEFAULT_REGION env var, ~/.aws/config, IMDS, etc.).
// Only meaningful when AWSAuth is true; ignored otherwise.
AWSRegion string `conf:"REDIS_AWS_REGION"`
// AWSServerless indicates that the target ElastiCache cache is a Serverless cache.
// When true, the SigV4 presigned token includes the query parameter
// ResourceType=ServerlessCache, which is required by ElastiCache Serverless for IAM
// authentication. Without this flag, authentication against a Serverless cache fails
// with an opaque WRONGPASS error. Only meaningful when AWSAuth is true; ignored otherwise.
//
// EXPERIMENTAL / NOT FUNCTIONALLY SUPPORTED: this flag covers only the IAM auth
// handshake. ElastiCache Serverless is cluster-mode-only, and Relay's Redis clients are
// not cluster-aware, so multi-key operations (SDK data-store writes, big-segments
// Watch/TxPipelined transactions) will fail with CROSSSLOT. Full Serverless support is
// blocked on cluster-aware client work that has not shipped. Do not rely on this in
// production. See docs/persistent-storage.md.
AWSServerless bool `conf:"REDIS_AWS_SERVERLESS"`
}

// ConsulConfig configures the optional Consul integration.
Expand Down
30 changes: 30 additions & 0 deletions config/config_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ var (
` if using DynamoDB, table name) must be specified and must contain "` + AutoConfigEnvironmentIDPlaceholder + `"`)
errRedisURLWithHostAndPort = errors.New("please specify Redis URL or host/port, but not both")
errRedisBadHostname = errors.New("invalid Redis hostname")
errRedisAWSAuthRequiresTLS = errors.New("REDIS_AWS_AUTH requires REDIS_TLS=true (ElastiCache IAM authentication only works over TLS)")
errRedisAWSAuthRequiresUsername = errors.New("REDIS_AWS_AUTH requires REDIS_USERNAME")
errRedisAWSAuthRequiresCacheName = errors.New("REDIS_AWS_AUTH requires REDIS_AWS_CACHE_NAME")
errRedisAWSAuthForbidsPassword = errors.New("REDIS_AWS_AUTH cannot be combined with REDIS_PASSWORD (mutually exclusive)")
errConsulTokenAndTokenFile = errors.New("Consul token must be specified as either an inline value or a file, but not both") //nolint:staticcheck
errAutoConfWithFilters = errors.New("cannot configure filters if auto-configuration is enabled")
errCacheKeyWithoutStore = errors.New("AUTO_CONFIG_CACHE_KEY requires Redis or DynamoDB to be enabled")
Expand Down Expand Up @@ -308,4 +312,30 @@ func normalizeRedisConfig(result *ct.ValidationResult, c *Config) {
c.Redis.Host = ""
c.Redis.Port = ct.OptIntGreaterThanZero{}
}

if c.Redis.AWSAuth {
if !c.Redis.TLS {
result.AddError(nil, errRedisAWSAuthRequiresTLS)
}
if c.Redis.Username == "" {
result.AddError(nil, errRedisAWSAuthRequiresUsername)
}
if c.Redis.AWSCacheName == "" {
result.AddError(nil, errRedisAWSAuthRequiresCacheName)
}
if c.Redis.Password != "" {
result.AddError(nil, errRedisAWSAuthForbidsPassword)
}
// A password embedded in the URL (e.g. rediss://user:secret@host) would
// also bypass IAM auth and get sent to ElastiCache as a static AUTH —
// reject it for the same reason as REDIS_PASSWORD.
if u := c.Redis.URL.Get(); u != nil && u.User != nil {
if _, hasPassword := u.User.Password(); hasPassword {
result.AddError(nil, errRedisAWSAuthForbidsPassword)
}
}
if c.Redis.AWSCacheName != "" {
c.Redis.AWSCacheName = strings.ToLower(c.Redis.AWSCacheName)
}
}
}
111 changes: 111 additions & 0 deletions config/test_data_configs_invalid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ func makeInvalidConfigs() []testDataInvalidConfig {
makeInvalidConfigRedisConflictingParams(),
makeInvalidConfigRedisNoPrefix(),
makeInvalidConfigRedisAutoConfNoPrefix(),
makeInvalidConfigRedisAWSAuthWithoutTLS(),
makeInvalidConfigRedisAWSAuthWithoutUsername(),
makeInvalidConfigRedisAWSAuthWithoutCacheName(),
makeInvalidConfigRedisAWSAuthWithPassword(),
makeInvalidConfigRedisAWSAuthWithURLEmbeddedPassword(),
makeInvalidConfigConsulNoPrefix(),
makeInvalidConfigConsulAutoConfNoPrefix(),
makeInvalidConfigConsulTokenAndTokenFile(),
Expand Down Expand Up @@ -467,3 +472,109 @@ Enabled = true
`
return c
}

func makeInvalidConfigRedisAWSAuthWithoutTLS() testDataInvalidConfig {
c := testDataInvalidConfig{name: "Redis - AWS auth without TLS"}
c.envVarsError = errRedisAWSAuthRequiresTLS.Error()
c.envVars = map[string]string{
"USE_REDIS": "1",
"REDIS_URL": "redis://my-cluster.amazonaws.com:6379",
"REDIS_AWS_AUTH": "1",
"REDIS_USERNAME": "iam-user",
"REDIS_AWS_CACHE_NAME": "my-cache",
}
c.fileContent = `
[Redis]
Url = "redis://my-cluster.amazonaws.com:6379"
AWSAuth = true
Username = "iam-user"
AWSCacheName = "my-cache"
`
return c
}

func makeInvalidConfigRedisAWSAuthWithoutUsername() testDataInvalidConfig {
c := testDataInvalidConfig{name: "Redis - AWS auth without username"}
c.envVarsError = errRedisAWSAuthRequiresUsername.Error()
c.envVars = map[string]string{
"USE_REDIS": "1",
"REDIS_URL": "rediss://my-cluster.amazonaws.com:6379",
"REDIS_TLS": "1",
"REDIS_AWS_AUTH": "1",
"REDIS_AWS_CACHE_NAME": "my-cache",
}
c.fileContent = `
[Redis]
Url = "rediss://my-cluster.amazonaws.com:6379"
TLS = true
AWSAuth = true
AWSCacheName = "my-cache"
`
return c
}

func makeInvalidConfigRedisAWSAuthWithoutCacheName() testDataInvalidConfig {
c := testDataInvalidConfig{name: "Redis - AWS auth without cache name"}
c.envVarsError = errRedisAWSAuthRequiresCacheName.Error()
c.envVars = map[string]string{
"USE_REDIS": "1",
"REDIS_URL": "rediss://my-cluster.amazonaws.com:6379",
"REDIS_TLS": "1",
"REDIS_AWS_AUTH": "1",
"REDIS_USERNAME": "iam-user",
}
c.fileContent = `
[Redis]
Url = "rediss://my-cluster.amazonaws.com:6379"
TLS = true
AWSAuth = true
Username = "iam-user"
`
return c
}

func makeInvalidConfigRedisAWSAuthWithPassword() testDataInvalidConfig {
c := testDataInvalidConfig{name: "Redis - AWS auth with password"}
c.envVarsError = errRedisAWSAuthForbidsPassword.Error()
c.envVars = map[string]string{
"USE_REDIS": "1",
"REDIS_URL": "rediss://my-cluster.amazonaws.com:6379",
"REDIS_TLS": "1",
"REDIS_AWS_AUTH": "1",
"REDIS_USERNAME": "iam-user",
"REDIS_AWS_CACHE_NAME": "my-cache",
"REDIS_PASSWORD": "should-not-be-here",
}
c.fileContent = `
[Redis]
Url = "rediss://my-cluster.amazonaws.com:6379"
TLS = true
AWSAuth = true
Username = "iam-user"
AWSCacheName = "my-cache"
Password = "should-not-be-here"
`
return c
}

func makeInvalidConfigRedisAWSAuthWithURLEmbeddedPassword() testDataInvalidConfig {
c := testDataInvalidConfig{name: "Redis - AWS auth with password embedded in URL"}
c.envVarsError = errRedisAWSAuthForbidsPassword.Error()
c.envVars = map[string]string{
"USE_REDIS": "1",
"REDIS_URL": "rediss://iam-user:embedded-pw@my-cluster.amazonaws.com:6379",
"REDIS_TLS": "1",
"REDIS_AWS_AUTH": "1",
"REDIS_USERNAME": "iam-user",
"REDIS_AWS_CACHE_NAME": "my-cache",
}
c.fileContent = `
[Redis]
Url = "rediss://iam-user:embedded-pw@my-cluster.amazonaws.com:6379"
TLS = true
AWSAuth = true
Username = "iam-user"
AWSCacheName = "my-cache"
`
return c
}
130 changes: 130 additions & 0 deletions config/test_data_configs_valid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ func makeValidConfigs() []testDataValidConfig {
makeValidConfigRedisPortOnly(),
makeValidConfigRedisDockerPort(),
makeValidConfigRedisOneEnvNoPrefix(),
makeValidConfigRedisAWSAuth(),
makeValidConfigRedisAWSAuthLowercasesCacheName(),
makeValidConfigRedisAWSAuthWithRegion(),
makeValidConfigRedisAWSAuthServerless(),
makeValidConfigConsulMinimal(),
makeValidConfigConsulAll(),
makeValidConfigConsulOneEnvNoPrefix(),
Expand Down Expand Up @@ -534,6 +538,132 @@ Host = localhost
return c
}

func makeValidConfigRedisAWSAuth() testDataValidConfig {
c := testDataValidConfig{name: "Redis - AWS IAM auth happy path"}
c.makeConfig = func(c *Config) {
c.Redis = RedisConfig{
URL: newOptURLAbsoluteMustBeValid("rediss://my-cluster.amazonaws.com:6379"),
TLS: true,
Username: "iam-user",
AWSAuth: true,
AWSCacheName: "my-cache",
}
}
c.envVars = map[string]string{
"USE_REDIS": "1",
"REDIS_URL": "rediss://my-cluster.amazonaws.com:6379",
"REDIS_TLS": "1",
"REDIS_USERNAME": "iam-user",
"REDIS_AWS_AUTH": "1",
"REDIS_AWS_CACHE_NAME": "my-cache",
}
c.fileContent = `
[Redis]
Url = "rediss://my-cluster.amazonaws.com:6379"
TLS = true
Username = "iam-user"
AWSAuth = true
AWSCacheName = "my-cache"
`
return c
}

func makeValidConfigRedisAWSAuthWithRegion() testDataValidConfig {
c := testDataValidConfig{name: "Redis - AWS IAM auth with explicit region override"}
c.makeConfig = func(c *Config) {
c.Redis = RedisConfig{
URL: newOptURLAbsoluteMustBeValid("rediss://my-cluster.amazonaws.com:6379"),
TLS: true,
Username: "iam-user",
AWSAuth: true,
AWSCacheName: "my-cache",
AWSRegion: "eu-west-1",
}
}
c.envVars = map[string]string{
"USE_REDIS": "1",
"REDIS_URL": "rediss://my-cluster.amazonaws.com:6379",
"REDIS_TLS": "1",
"REDIS_USERNAME": "iam-user",
"REDIS_AWS_AUTH": "1",
"REDIS_AWS_CACHE_NAME": "my-cache",
"REDIS_AWS_REGION": "eu-west-1",
}
c.fileContent = `
[Redis]
Url = "rediss://my-cluster.amazonaws.com:6379"
TLS = true
Username = "iam-user"
AWSAuth = true
AWSCacheName = "my-cache"
AWSRegion = "eu-west-1"
`
return c
}

func makeValidConfigRedisAWSAuthServerless() testDataValidConfig {
c := testDataValidConfig{name: "Redis - AWS IAM auth for Serverless cache"}
c.makeConfig = func(c *Config) {
c.Redis = RedisConfig{
URL: newOptURLAbsoluteMustBeValid("rediss://my-serverless.amazonaws.com:6379"),
TLS: true,
Username: "iam-user",
AWSAuth: true,
AWSCacheName: "my-serverless",
AWSServerless: true,
}
}
c.envVars = map[string]string{
"USE_REDIS": "1",
"REDIS_URL": "rediss://my-serverless.amazonaws.com:6379",
"REDIS_TLS": "1",
"REDIS_USERNAME": "iam-user",
"REDIS_AWS_AUTH": "1",
"REDIS_AWS_CACHE_NAME": "my-serverless",
"REDIS_AWS_SERVERLESS": "1",
}
c.fileContent = `
[Redis]
Url = "rediss://my-serverless.amazonaws.com:6379"
TLS = true
Username = "iam-user"
AWSAuth = true
AWSCacheName = "my-serverless"
AWSServerless = true
`
return c
}

func makeValidConfigRedisAWSAuthLowercasesCacheName() testDataValidConfig {
c := testDataValidConfig{name: "Redis - AWS IAM auth lowercases cache name"}
c.makeConfig = func(c *Config) {
c.Redis = RedisConfig{
URL: newOptURLAbsoluteMustBeValid("rediss://my-cluster.amazonaws.com:6379"),
TLS: true,
Username: "iam-user",
AWSAuth: true,
AWSCacheName: "my-cache",
}
}
c.envVars = map[string]string{
"USE_REDIS": "1",
"REDIS_URL": "rediss://my-cluster.amazonaws.com:6379",
"REDIS_TLS": "1",
"REDIS_USERNAME": "iam-user",
"REDIS_AWS_AUTH": "1",
"REDIS_AWS_CACHE_NAME": "My-Cache",
}
c.fileContent = `
[Redis]
Url = "rediss://my-cluster.amazonaws.com:6379"
TLS = true
Username = "iam-user"
AWSAuth = true
AWSCacheName = "My-Cache"
`
return c
}

func makeValidConfigConsulMinimal() testDataValidConfig {
c := testDataValidConfig{name: "Consul - minimal parameters"}
c.makeConfig = func(c *Config) {
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ To learn more, read [Persistent storage](./persistent-storage.md).
| `tls` | `REDIS_TLS` | Boolean | `false` | If `true`, will use a secure connection to Redis (not all Redis servers support this). If you specified a `redis://` URL, setting `tls` to `true` will change it to `rediss://`. |
| `password` | `REDIS_PASSWORD` | String | | Optional password if Redis requires authentication. |
| `username` | `REDIS_USERNAME` | String | | Optional username if Redis requires authentication. |
| `awsAuth` | `REDIS_AWS_AUTH` | Boolean | `false` | Enable AWS IAM authentication for ElastiCache. Requires `REDIS_TLS=true`, `REDIS_USERNAME`, and `REDIS_AWS_CACHE_NAME`. Mutually exclusive with `REDIS_PASSWORD`. |
| `awsCacheName` | `REDIS_AWS_CACHE_NAME` | String | | ElastiCache cluster name (lowercased automatically). Required when `REDIS_AWS_AUTH` is `true`. Used to construct the SigV4 presigned authentication URL. |
| `awsRegion` | `REDIS_AWS_REGION` | String | | AWS region override for SigV4 signing of ElastiCache IAM tokens. If empty, the region is resolved from the standard AWS credential chain (`AWS_DEFAULT_REGION`, `~/.aws/config`, IMDS, etc.). Only meaningful when `REDIS_AWS_AUTH` is `true`. |
| `awsServerless` | `REDIS_AWS_SERVERLESS` | Boolean | `false` | **Experimental — not functionally supported.** Adds `ResourceType=ServerlessCache` to the SigV4 presigned token URL, which is required for a Serverless cache to accept the IAM auth handshake. This covers authentication only; ElastiCache Serverless is cluster-mode-only and the Relay Proxy's Redis clients are not cluster-aware, so multi-key operations will fail with `CROSSSLOT`. Full Serverless support depends on cluster-aware client work that has not shipped. Do not use in production. See [Persistent storage](./persistent-storage.md#elasticache-serverless-experimental--not-functionally-supported). Only meaningful when `REDIS_AWS_AUTH` is `true`. |
| `localTtl` | `CACHE_TTL` | Duration | `30s` | Length of time that database items can be cached in memory. |

Note that the TLS and password options can also be specified as part of the URL: `rediss://` instead of `redis://`
Expand Down
Loading
Loading