Skip to content

ERR unknown command 'CONFIG' when using Secured Redis #124

Closed
@danveloper

Description

@danveloper
Contributor

Redis security recommends disabling the CONFIG command so that remote users cannot reconfigure an instance. The RedisHttpSessionConfiguration requires access to this during its initialization. Hosted Redis services, like AWS ElastiCache disable this command by default, with no option to re-enable it.

Activity

added this to the 1.0.1 milestone on Jan 26, 2015
rwinch

rwinch commented on Jan 26, 2015

@rwinch
Member

Thanks for the report @danveloper! This indeed seems to be a bug with the RedisHttpSessionConfiguration and thus the @EnableRedisHttpSession annotation.

UPDATE Fixing in 1.0.1

As of Spring Session 1.0.1 this can be disabled by exposing ConfigureRedisAction.NO_OP as a bean.

An XML Configuration example

<util:constant
        static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/>

A Java Configuration example

@Bean
public static ConfigureRedisAction configureRedisAction() {
    return ConfigureRedisAction.NO_OP;
}

Fixing the Issue

I'm debating what the best approach to fixing this would be though and wondering what your thoughts were @danveloper.

There is certainly a need for a fix, so I'm not debating that we need to fix something. However, I like the fact that it updates the Redis configuration by default for two reasons:

  • It makes it very easy to get things up and working in a development environment
  • Making it enabled by default means that users would have to explicitly disable for production. Since there is an explicit step to disable the configuration, they should be aware that it is necessary to configure Redis to send the namespace notifications. This is critical for applications that require SessionDestroyedEvent to be fired to clean up resources. In particular, this is important for WebSocket applications to ensure open WebSockets are closed when the HttpSession expires.

My initial thoughts on how we should update the configuration is:

  • RedisHttpSessionConfiguration should by default update the Redis configuration only if Spring WebSocket support is enabled.
  • RedisHttpSessionConfiguration should allow disabling updating the Redis configuration
  • RedisHttpSessionConfiguration should by default try to subscribe to keyspace notifications only if Spring WebSocket support is enabled. This will help increase performance for applications simply using Spring Session for HttpSession which typically does not need to receive the SessionDestroyedEvent
  • RedisHttpSessionConfiguration should allow explicitly configuring if the application should subscribe to keyspace notifications
  • We should update the documentation to discuss the changes

Workaround

In the meantime, a workaround is to remove @EnableRedisHttpSession from your configuration and then include a configuration with a fix. For example:

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.ExpiringSession;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.SessionMessageListener;
import org.springframework.session.web.http.HttpSessionStrategy;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.ClassUtils;

@Configuration
@EnableScheduling
public class RedisHttpSessionConfiguration {
    @Value("${spring.session.maxInactive ?: 1800}")
    private Integer maxInactiveIntervalInSeconds;

    private HttpSessionStrategy httpSessionStrategy;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(redisSessionMessageListener(),
                Arrays.asList(new PatternTopic("__keyevent@*:del"),new PatternTopic("__keyevent@*:expired")));
        return container;
    }

    @Bean
    public SessionMessageListener redisSessionMessageListener() {
        return new SessionMessageListener(eventPublisher);
    }

    @Bean
    public RedisTemplate<String,ExpiringSession> sessionRedisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, ExpiringSession> template = new RedisTemplate<String, ExpiringSession>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setConnectionFactory(connectionFactory);
        return template;
    }

    @Bean
    public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> sessionRedisTemplate) {
        RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
        sessionRepository.setDefaultMaxInactiveInterval(maxInactiveIntervalInSeconds);
        return sessionRepository;
    }

    @Bean
    public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository);
        if(httpSessionStrategy != null) {
            sessionRepositoryFilter.setHttpSessionStrategy(httpSessionStrategy);
        }
        return sessionRepositoryFilter;
    }

    @Autowired(required = false)
    public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
        this.httpSessionStrategy = httpSessionStrategy;
    }
}

If you are not using the SessionDestroyedEvent you can also disable subscribing to the notifications which should improve performance. For example:

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.ExpiringSession;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
import org.springframework.session.data.redis.SessionMessageListener;
import org.springframework.session.web.http.HttpSessionStrategy;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.ClassUtils;

@Configuration
public class RedisHttpSessionConfiguration {
    @Value("${spring.session.maxInactive ?: 1800}")
    private Integer maxInactiveIntervalInSeconds;

    private HttpSessionStrategy httpSessionStrategy;

    @Bean
    public RedisTemplate<String,ExpiringSession> sessionRedisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, ExpiringSession> template = new RedisTemplate<String, ExpiringSession>();
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setConnectionFactory(connectionFactory);
        return template;
    }

    @Bean
    public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> sessionRedisTemplate) {
        RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
        sessionRepository.setDefaultMaxInactiveInterval(maxInactiveIntervalInSeconds);
        return sessionRepository;
    }

    @Bean
    public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository) {
        SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository);
        if(httpSessionStrategy != null) {
            sessionRepositoryFilter.setHttpSessionStrategy(httpSessionStrategy);
        }
        return sessionRepositoryFilter;
    }

    @Autowired(required = false)
    public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
        this.httpSessionStrategy = httpSessionStrategy;
    }
}
danveloper

danveloper commented on Jan 26, 2015

@danveloper
ContributorAuthor

I think it should be enabled by default, but fail gracefully with a warning. This would allow the same configuration to be used between dev and prod, where dev would JustWork(tm) and prod would require some manual intervention (which would be obvious from the warning).

I was able to work around the problem by subclassing the RedisHttpSessionConfiguration with an implementation that disables the keyspace notifications initializer, and bringing it in through normal configuration means:

package org.springframework.session.data.redis.config.annotation.web.http

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.type.AnnotationMetadata
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.session.ExpiringSession
import org.springframework.session.data.redis.RedisOperationsSessionRepository

@Configuration
class GateRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {

  @Value('${session.expiration:1800}')
  int expiration

  public void setImportMetadata(AnnotationMetadata importMetadata) {
  }

  @Bean
  public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> sessionRedisTemplate) {
    RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
    sessionRepository.setDefaultMaxInactiveInterval(expiration);
    return sessionRepository;
  }

  @Override
  public RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer enableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory) {
    null
  }
}

For posterity, here are the steps for enabling the keyspace notifications on AWS:

Log into the AWS console and choose the ElastiCache service

image

Choose the Cache Parameter Groups and click Create Parameter Group

image

Give the new group and name and description and click Create

image

With the new parameter group created, select it and click Edit Parameters

image

Page through the parameters until you find notify-keyspace-events and enter "eA" in the Value field and click Save Changes

image

Choose Cache Clusters from the context navigation and create a new Redis cache cluster

When specifying your cluster detail, choose the newly created parameter group

image

rwinch

rwinch commented on Jan 26, 2015

@rwinch
Member

@danveloper I'm glad you were able to work around the issue. Thank you for your feedback and for the useful directions on enabling keyspace notifications in AWS.

I have concerns with the configuration not failing when it tries to update the Redis configuration.

Developers likely did thorough testing within another environment. For example, they might be integrating with WebSocket support and validate that when an HttpSession is terminated the WebSocket is indeed closing properly.

This means they are confident that everything is working by the time the get to production. Many users may not even notice a warning being logged. Without Redis keyspace notifications being enabled, the applications WebSocket connections will not properly closed when the HttpSession is destroyed.

Obviously developers should read the instructions completely, look for warnings, and perform some smoke tests in production to avoid such issues. However, I fear that by not failing we are setting up users for failure.

A compromise might be to require users to explicitly set a property to allow "fall back". This means users would be aware that they need to do something in production. Of course this is one more piece of configuration users would need. Thoughts?

danveloper

danveloper commented on Jan 26, 2015

@danveloper
ContributorAuthor

Could it provide a different strategy for ascertaining the database's configuration? Maybe in the absence of the CONFIG command, the event notification initializer could run a test to determine if expirations were working? This might introduce some slowness during startup, but might be favorable to total failure.

I'd probably prefer a slower startup over managing feature flags between dev & prod. If this isn't possible or is otherwise infeasible, then feature flag might be the only way to go.

rwinch

rwinch commented on Jan 26, 2015

@rwinch
Member

Great idea! I think this may be doable, but we will see when I get into the details. Thanks for your feedback!

njariwala

njariwala commented on Jan 30, 2015

@njariwala

RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer is not visible for outsiders extending RedisHttpSessionConfiguration. The workaround doesn't work. Can you help me with this?

rwinch

rwinch commented on Feb 3, 2015

@rwinch
Member

@njariwala You would need to place it in the same package as illustrated in the example above.

chrylis

chrylis commented on Feb 4, 2015

@chrylis
Contributor

If the class is semantically usable for non-core extensions, it seems like a bad practice to trespass on the distribution package, especially since that can fail in interesting ways with particular classloader hierarchies.

rwinch

rwinch commented on Feb 4, 2015

@rwinch
Member

@chrylis Yes this is considered a workaround until we can get a fix out. The alternative is to use the more verbose workaround I provided.

17 remaining items

amit-jadhav

amit-jadhav commented on Feb 11, 2020

@amit-jadhav

I am facing the same issue even after trying the solutions provided above

JesusIgnacio

JesusIgnacio commented on Jul 2, 2020

@JesusIgnacio

I think it should be enabled by default, but fail gracefully with a warning. This would allow the same configuration to be used between dev and prod, where dev would JustWork(tm) and prod would require some manual intervention (which would be obvious from the warning).

I was able to work around the problem by subclassing the RedisHttpSessionConfiguration with an implementation that disables the keyspace notifications initializer, and bringing it in through normal configuration means:

package org.springframework.session.data.redis.config.annotation.web.http

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.type.AnnotationMetadata
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.session.ExpiringSession
import org.springframework.session.data.redis.RedisOperationsSessionRepository

@Configuration
class GateRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {

  @Value('${session.expiration:1800}')
  int expiration

  public void setImportMetadata(AnnotationMetadata importMetadata) {
  }

  @Bean
  public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> sessionRedisTemplate) {
    RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
    sessionRepository.setDefaultMaxInactiveInterval(expiration);
    return sessionRepository;
  }

  @Override
  public RedisHttpSessionConfiguration.EnableRedisKeyspaceNotificationsInitializer enableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory) {
    null
  }
}

For posterity, here are the steps for enabling the keyspace notifications on AWS:

Log into the AWS console and choose the ElastiCache service

image

Choose the Cache Parameter Groups and click Create Parameter Group

image

Give the new group and name and description and click Create

image

With the new parameter group created, select it and click Edit Parameters

image

Page through the parameters until you find notify-keyspace-events and enter "eA" in the Value field and click Save Changes__

__ image #### Choose Cache Clusters from the context navigation and create a new Redis cache cluster #### When specifying your cluster detail, choose the newly created parameter group image__

The value to set in notify-keyspace-events obbeys to the below information :

K — Keyspace events, published with a prefix of __keyspace@<db>__
E — Key-event events, published with a prefix of __keyevent@<db>__
g — Generic, non-specific commands such as DEL, EXPIRE, RENAME, etc.
$ — String commands
l — List commands
s — Set commands
h — Hash commands
z — Sorted set commands
x — Expired events (events generated every time a key expires)
e — Evicted events (events generated when a key is evicted for maxmemory)
A — An alias for g$lshzxe

This information is located in https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/ParameterGroups.Redis.html#ParameterGroups.Redis.2-8-6

Thanks @danveloper for your explanation.

LukasVyhlidka

LukasVyhlidka commented on Oct 13, 2021

@LukasVyhlidka

When I configured the elasticache (Redis on AWS) using the guide from previous post I fugured out that the eA parameter for notify-keyspace-events does not work correctly. Correct configuration is EA (E with capital letter).
In the documentation (https://redis.io/topics/notifications) you can see that the A is an alias e.g. for e which means, that eA is effectively just A.
Redis Spring Session functionality needs this E because it consumes those key-events.
So, specify there EA.

ViliusS

ViliusS commented on Apr 30, 2022

@ViliusS

Did anyone found how to use this with Memorystore for Redis on Google Cloud? It looks like it has the same issue, however it doesn't have any interface to control data :|

jackstraw66

jackstraw66 commented on Oct 25, 2022

@jackstraw66

We encountered this issue recently after securing our AWS Redis (ElasticCache) 6.2.6 cluster (running in clustered mode). Adding a ConfigureRedisAction.NO_OP bean meant adding the spring-session-data-redis (v2.5.1) dependency, previously not required.

Issue
This has led to sporadic exceptions on startup when the container is subscribing to patterned topics.

Exception in thread "springSessionRedisMessageListenerContainer-2" java.lang.NullPointerException
	at redis.clients.jedis.BinaryJedisPubSub.psubscribe(BinaryJedisPubSub.java:57)
	at org.springframework.data.redis.connection.jedis.JedisSubscription.doPsubscribe(JedisSubscription.java:59)
	at org.springframework.data.redis.connection.util.AbstractSubscription.pSubscribe(AbstractSubscription.java:159)
	at org.springframework.data.redis.listener.RedisMessageListenerContainer$SubscriptionTask$PatternSubscriptionTask.run(RedisMessageListenerContainer.java:707)
	at java.lang.Thread.run(Thread.java:750)

When these failures occur, keyspace events are no longer received. I believe the sporadic nature of these exceptions is due to the potentially asynchronous behavior of PatternSubscriptionTask.

Application Details
We do not use any patterned topics in our application, and only use ChannelTopics. I understand Spring Session may be subscribing to patterned topics (e.g. spring:session:event:0:created:*)?

The notify-keyspace-events property is configured with KE$. (We only care about the keyspace events on strings.)

I learned another aspect of subscribing to keyspace events in a secured Redis cluster is that the application must subscribe to the topic on each node in the cluster. Redis keystore events are not broadcast cluster-wide, and are only broadcast to topics local to that node. We have 9 nodes in our cluster and the application is configured to subscribe to the topic on each node.

Could this be causing issues with how RedisHttpSessionConfiguration is subscribing to its patterned topics?

Is there a way to disable Spring Session from subscribing to these patterned topics, or disabling Spring Session altogether?

Like I mentioned above, we didn't need Spring Session and only included it when we needed to add the ConfigureRedisAction.NO_OP bean.

Desired Result
We want to prevent Spring Redis from auto-configuring our Redis cluster, and for the application to subscribe to the topic on each node of the cluster to receive the keyspace notifications. We don't care about "delete" or "expiration" notifications - only notifications triggered by the SET operations on a key in the keystore.

vpavic

vpavic commented on Oct 25, 2022

@vpavic
Contributor

Adding a ConfigureRedisAction.NO_OP bean meant adding the spring-session-data-redis (v2.5.1) dependency, previously not required.

That doesn't sound right - ConfigureRedisAction.NO_OP is intended specifically as a configuration option for Spring Session's Redis integration.

If you're not using Spring Session at all, you need something else to resolve the issue. I'd suggest taking a look at Spring Data Redis reference manual, or opening an issue over there if you don't find anything.

added a commit that references this issue on Jan 20, 2025
d2e5005
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @rwinch@LukasVyhlidka@oak-tree@vpavic@JesusIgnacio

        Issue actions

          ERR unknown command 'CONFIG' when using Secured Redis · Issue #124 · spring-projects/spring-session