Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redis TLS and Auth support #96

Merged
merged 9 commits into from
Oct 17, 2019
Merged
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
10 changes: 7 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 31 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 31 additions & 1 deletion src/redis/driver_impl.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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++
Expand Down
17 changes: 13 additions & 4 deletions src/service_cmd/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two thoughts here:

  1. Should we realistically support enabling TLS for one of the pools but not the other?
  2. For the auth case, right now we need the user to set up both the tls envvar and the auth one, should we make it so that if auth is set, tls is set by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. This was done this way, since it seemed that the two types of pools where meant to be uniquely configurable. I don't mind having a single tls envvar to enable tls on both pools though.
  2. I think that makes sense, since you'll almost always want tls on when using auth, so having it as the default makes sense with the option to override.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I guess it is not that much complexity, and it leaves it open for configurability.
  2. lgtm, thanks!

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())),
Expand Down
4 changes: 4 additions & 0 deletions src/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
27 changes: 23 additions & 4 deletions test/integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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())
Expand Down Expand Up @@ -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()
}()
Expand Down