diff --git a/.travis.yml b/.travis.yml index ab08ad706..ea43ffb90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ sudo: required language: go go: "1.11" services: redis-server -install: make bootstrap -before_script: redis-server --port 6380 & -script: make check_format tests +before_install: sudo apt-get update -y && sudo apt-get install stunnel4 -y +install: make bootstrap bootstrap_redis_tls +before_script: +- redis-server --port 6380 & +- redis-server --port 6381 --requirepass password123 & +- redis-server --port 6382 --requirepass password123 & +script: make check_format tests diff --git a/Makefile b/Makefile index 1ba5b8c5b..2110f1075 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,37 @@ bootstrap: .PHONY: bootstrap_tests bootstrap_tests: cd ./vendor/github.com/golang/mock/mockgen && go install - +define REDIS_STUNNEL +cert = private.pem +pid = /var/run/stunnel.pid +[redis] +accept = 127.0.0.1:16381 +connect = 127.0.0.1:6381 +endef +define REDIS_PER_SECOND_STUNNEL +cert = private.pem +pid = /var/run/stunnel-2.pid +[redis] +accept = 127.0.0.1:16382 +connect = 127.0.0.1:6382 +endef +export REDIS_STUNNEL +export REDIS_PER_SECOND_STUNNEL +redis.conf: + echo "$$REDIS_STUNNEL" >> $@ +redis-per-second.conf: + echo "$$REDIS_PER_SECOND_STUNNEL" >> $@ +.PHONY: bootstrap_redis_tls +bootstrap_redis_tls: redis.conf redis-per-second.conf + openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 \ + -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=localhost" \ + -keyout key.pem -out cert.pem + cat key.pem cert.pem > private.pem + sudo cp cert.pem /usr/local/share/ca-certificates/redis-stunnel.crt + chmod 640 key.pem cert.pem private.pem + sudo update-ca-certificates + sudo stunnel redis.conf + sudo stunnel redis-per-second.conf .PHONY: docs_format docs_format: script/docs_check_format diff --git a/README.md b/README.md index 4b263195a..5e1f3e163 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,11 @@ Ratelimit uses Redis as its caching layer. Ratelimit supports two operation mode 1. One Redis server for all limits. 1. Two Redis instances: one for per second limits and another one for all other limits. +As well Ratelimit supports TLS connections and authentication over TLS connections. These can be configured using the following environment variables: + +1. `REDIS_TLS` & `REDIS_PERSECOND_TLS`: set to `"true"` to enable a TLS connection for the specific connection type. +1. `REDIS_AUTH` & `REDIS_PERSECOND_AUTH`: set to `"password"` to enable authentication to the redis host. This requires TLS to be enabled as well for the specific connection. + ## One Redis Instance To configure one Redis instance use the following environment variables: diff --git a/src/redis/driver_impl.go b/src/redis/driver_impl.go index eac51fbf5..15c56f0f0 100644 --- a/src/redis/driver_impl.go +++ b/src/redis/driver_impl.go @@ -1,7 +1,9 @@ package redis import ( - "github.com/lyft/gostats" + "crypto/tls" + + stats "github.com/lyft/gostats" "github.com/lyft/ratelimit/src/assert" "github.com/mediocregopher/radix.v2/pool" "github.com/mediocregopher/radix.v2/redis" @@ -73,6 +75,34 @@ func NewPoolImpl(scope stats.Scope, socketType string, url string, poolSize int) stats: newPoolStats(scope)} } +func NewAuthTLSPoolImpl(scope stats.Scope, auth string, url string, poolSize int) Pool { + logger.Warnf("connecting to redis on tls %s with pool size %d", url, poolSize) + df := func(network, addr string) (*redis.Client, error) { + conn, err := tls.Dial("tcp", addr, &tls.Config{}) + if err != nil { + return nil, err + } + client, err := redis.NewClient(conn) + + if err != nil { + return nil, err + } + if auth != "" { + logger.Warnf("enabling authentication to redis on tls %s", url) + if err = client.Cmd("AUTH", auth).Err; err != nil { + client.Close() + return nil, err + } + } + return client, nil + } + pool, err := pool.NewCustom("tcp", url, poolSize, df) + checkError(err) + return &poolImpl{ + pool: pool, + stats: newPoolStats(scope)} +} + func (this *connectionImpl) PipeAppend(cmd string, args ...interface{}) { this.client.PipeAppend(cmd, args...) this.pending++ diff --git a/src/service_cmd/runner/runner.go b/src/service_cmd/runner/runner.go index cd8535176..c5af584fe 100644 --- a/src/service_cmd/runner/runner.go +++ b/src/service_cmd/runner/runner.go @@ -12,7 +12,7 @@ import ( "github.com/lyft/ratelimit/src/config" "github.com/lyft/ratelimit/src/redis" "github.com/lyft/ratelimit/src/server" - "github.com/lyft/ratelimit/src/service" + ratelimit "github.com/lyft/ratelimit/src/service" "github.com/lyft/ratelimit/src/settings" logger "github.com/sirupsen/logrus" ) @@ -31,14 +31,23 @@ func Run() { var perSecondPool redis.Pool if s.RedisPerSecond { - perSecondPool = redis.NewPoolImpl(srv.Scope().Scope("redis_per_second_pool"), s.RedisPerSecondSocketType, s.RedisPerSecondUrl, s.RedisPerSecondPoolSize) + if s.RedisPerSecondAuth != "" || s.RedisPerSecondTls { + perSecondPool = redis.NewAuthTLSPoolImpl(srv.Scope().Scope("redis_per_second_pool"), s.RedisPerSecondAuth, s.RedisPerSecondUrl, s.RedisPerSecondPoolSize) + } else { + perSecondPool = redis.NewPoolImpl(srv.Scope().Scope("redis_per_second_pool"), s.RedisSocketType, s.RedisPerSecondUrl, s.RedisPerSecondPoolSize) + } } - + var otherPool redis.Pool + if s.RedisAuth != "" || s.RedisTls { + otherPool = redis.NewAuthTLSPoolImpl(srv.Scope().Scope("redis_pool"), s.RedisAuth, s.RedisUrl, s.RedisPoolSize) + } else { + otherPool = redis.NewPoolImpl(srv.Scope().Scope("redis_pool"), s.RedisSocketType, s.RedisUrl, s.RedisPoolSize) + } service := ratelimit.NewService( srv.Runtime(), redis.NewRateLimitCacheImpl( - redis.NewPoolImpl(srv.Scope().Scope("redis_pool"), s.RedisSocketType, s.RedisUrl, s.RedisPoolSize), + otherPool, perSecondPool, redis.NewTimeSourceImpl(), rand.New(redis.NewLockedSource(time.Now().Unix())), diff --git a/src/settings/settings.go b/src/settings/settings.go index 890955477..2028c6222 100644 --- a/src/settings/settings.go +++ b/src/settings/settings.go @@ -22,10 +22,14 @@ type Settings struct { RedisSocketType string `envconfig:"REDIS_SOCKET_TYPE" default:"unix"` RedisUrl string `envconfig:"REDIS_URL" default:"/var/run/nutcracker/ratelimit.sock"` RedisPoolSize int `envconfig:"REDIS_POOL_SIZE" default:"10"` + RedisAuth string `envconfig:"REDIS_AUTH" default:""` + RedisTls bool `envconfig:"REDIS_TLS" default:"false"` RedisPerSecond bool `envconfig:"REDIS_PERSECOND" default:"false"` RedisPerSecondSocketType string `envconfig:"REDIS_PERSECOND_SOCKET_TYPE" default:"unix"` RedisPerSecondUrl string `envconfig:"REDIS_PERSECOND_URL" default:"/var/run/nutcracker/ratelimitpersecond.sock"` RedisPerSecondPoolSize int `envconfig:"REDIS_PERSECOND_POOL_SIZE" default:"10"` + RedisPerSecondAuth string `envconfig:"REDIS_PERSECOND_AUTH" default:""` + RedisPerSecondTls bool `envconfig:"REDIS_PERSECOND_TLS" default:"false"` ExpirationJitterMaxSeconds int64 `envconfig:"EXPIRATION_JITTER_MAX_SECONDS" default:"300"` } diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go index c48387f3e..e1aeff798 100644 --- a/test/integration/integration_test.go +++ b/test/integration/integration_test.go @@ -45,8 +45,25 @@ func TestBasicConfig(t *testing.T) { t.Run("WithoutPerSecondRedis", testBasicConfig("8083", "false")) t.Run("WithPerSecondRedis", testBasicConfig("8085", "true")) } - +func TestBasicTLSConfig(t *testing.T) { + t.Run("WithoutPerSecondRedisTLS", testBasicConfigAuthTLS("8087", "false")) + t.Run("WithPerSecondRedisTLS", testBasicConfigAuthTLS("8089", "true")) +} +func testBasicConfigAuthTLS(grpcPort, perSecond string) func(*testing.T) { + os.Setenv("REDIS_PERSECOND_URL", "localhost:16382") + os.Setenv("REDIS_URL", "localhost:16381") + os.Setenv("REDIS_AUTH", "password123") + os.Setenv("REDIS_PERSECOND_AUTH", "password123") + return testBasicBaseConfig(grpcPort, perSecond) +} func testBasicConfig(grpcPort, perSecond string) func(*testing.T) { + os.Setenv("REDIS_PERSECOND_URL", "localhost:6380") + os.Setenv("REDIS_URL", "localhost:6379") + os.Setenv("REDIS_TLS", "false") + os.Setenv("REDIS_PERSECOND_TLS", "false") + return testBasicBaseConfig(grpcPort, perSecond) +} +func testBasicBaseConfig(grpcPort, perSecond string) func(*testing.T) { return func(t *testing.T) { os.Setenv("REDIS_PERSECOND", perSecond) os.Setenv("PORT", "8082") @@ -55,16 +72,14 @@ func testBasicConfig(grpcPort, perSecond string) func(*testing.T) { os.Setenv("RUNTIME_ROOT", "runtime/current") os.Setenv("RUNTIME_SUBDIRECTORY", "ratelimit") os.Setenv("REDIS_PERSECOND_SOCKET_TYPE", "tcp") - os.Setenv("REDIS_PERSECOND_URL", "localhost:6380") os.Setenv("REDIS_SOCKET_TYPE", "tcp") - os.Setenv("REDIS_URL", "localhost:6379") go func() { runner.Run() }() // HACK: Wait for the server to come up. Make a hook that we can wait on. - time.Sleep(100 * time.Millisecond) + time.Sleep(1 * time.Second) assert := assert.New(t) conn, err := grpc.Dial(fmt.Sprintf("localhost:%s", grpcPort), grpc.WithInsecure()) @@ -156,6 +171,10 @@ func TestBasicConfigLegacy(t *testing.T) { os.Setenv("RUNTIME_ROOT", "runtime/current") os.Setenv("RUNTIME_SUBDIRECTORY", "ratelimit") + os.Setenv("REDIS_PERSECOND_URL", "localhost:6380") + os.Setenv("REDIS_URL", "localhost:6379") + os.Setenv("REDIS_TLS", "false") + os.Setenv("REDIS_PERSECOND_TLS", "false") go func() { runner.Run() }()