git.iko.soy/typeable/oauth2-proxy
on master help sign in
Validate Redis session store health on startup
commit6db1aeb9c69c9693b0dd91c28b9c4e1951993dfa authorNick Meves <nick.meves@greenhouse.io> dateThu 6 Aug 2020, 15:43 (6 years ago) parent93870ec
5 files changed+308−31
CHANGELOG.md+3 pkg/sessions/redis/redis_store.go+3−3 pkg/validation/options.go+1 pkg/validation/sessions.go+51 pkg/validation/sessions_test.go+250−28
CHANGELOG.md+3
@@ -11,9 +11,12 @@
1111
1212## Breaking Changes
1313
14+- [#722](https://github.com/oauth2-proxy/oauth2-proxy/pull/722) When a Redis session store is configured, OAuth2-Proxy will fail to start up unless connection and health checks to Redis pass
15+
1416## Changes since v6.1.1
1517
1618- [#575](https://github.com/oauth2-proxy/oauth2-proxy/pull/575) Stop accepting legacy SHA1 signed cookies (@NickMeves)
19+- [#722](https://github.com/oauth2-proxy/oauth2-proxy/pull/722) Validate Redis configuration options at startup (@NickMeves)
1720- [#764](https://github.com/oauth2-proxy/oauth2-proxy/pull/764) Document bcrypt encryption for htpasswd (and hide SHA) (@lentzi90)
1821- [#616](https://github.com/oauth2-proxy/oauth2-proxy/pull/616) Add support to ensure user belongs in required groups when using the OIDC provider (@stefansedich)
1922
pkg/sessions/redis/redis_store.go+3−3
@@ -23,7 +23,7 @@ type SessionStore struct {
2323// NewRedisSessionStore initialises a new instance of the SessionStore and wraps
2424// it in a persistence.Manager
2525func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) {
26− client, err := newRedisClient(opts.Redis)
26+ client, err := NewRedisClient(opts.Redis)
2727 if err != nil {
2828 return nil, fmt.Errorf("error constructing redis client: %v", err)
2929 }
@@ -64,9 +64,9 @@ func (store *SessionStore) Clear(ctx context.Context, key string) error {
6464 return nil
6565}
6666
67−// newRedisClient makes a redis.Client (either standalone, sentinel aware, or
67+// NewRedisClient makes a redis.Client (either standalone, sentinel aware, or
6868// redis cluster)
69−func newRedisClient(opts options.RedisStoreOptions) (Client, error) {
69+func NewRedisClient(opts options.RedisStoreOptions) (Client, error) {
7070 if opts.UseSentinel && opts.UseCluster {
7171 return nil, fmt.Errorf("options redis-use-sentinel and redis-use-cluster are mutually exclusive")
7272 }
pkg/validation/options.go+1
@@ -28,6 +28,7 @@ import (
2828func Validate(o *options.Options) error {
2929 msgs := validateCookie(o.Cookie)
3030 msgs = append(msgs, validateSessionCookieMinimal(o)...)
31+ msgs = append(msgs, validateRedisSessionStore(o)...)
3132
3233 if o.SSLInsecureSkipVerify {
3334 // InsecureSkipVerify is a configurable option we allow
pkg/validation/sessions.go+51
@@ -1,9 +1,13 @@
11package validation
22
33import (
4+ "context"
5+ "fmt"
46 "time"
57
68 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
9+ "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
10+ "github.com/oauth2-proxy/oauth2-proxy/pkg/sessions/redis"
711)
812
913func validateSessionCookieMinimal(o *options.Options) []string {
@@ -30,3 +34,50 @@ func validateSessionCookieMinimal(o *options.Options) []string {
3034 }
3135 return msgs
3236}
37+
38+// validateRedisSessionStore builds a Redis Client from the options and
39+// attempts to connect, Set, Get and Del a random health check key
40+func validateRedisSessionStore(o *options.Options) []string {
41+ if o.Session.Type != options.RedisSessionStoreType {
42+ return []string{}
43+ }
44+
45+ client, err := redis.NewRedisClient(o.Session.Redis)
46+ if err != nil {
47+ return []string{fmt.Sprintf("unable to initialize a redis client: %v", err)}
48+ }
49+
50+ nonce, err := encryption.Nonce()
51+ if err != nil {
52+ return []string{fmt.Sprintf("unable to generate a redis initialization test key: %v", err)}
53+ }
54+
55+ key := fmt.Sprintf("%s-healthcheck-%s", o.Cookie.Name, nonce)
56+ return sendRedisConnectionTest(client, key, nonce)
57+}
58+
59+func sendRedisConnectionTest(client redis.Client, key string, val string) []string {
60+ msgs := []string{}
61+ ctx := context.Background()
62+
63+ err := client.Set(ctx, key, []byte(val), time.Duration(60)*time.Second)
64+ if err != nil {
65+ msgs = append(msgs, fmt.Sprintf("unable to set a redis initialization key: %v", err))
66+ } else {
67+ gval, err := client.Get(ctx, key)
68+ if err != nil {
69+ msgs = append(msgs,
70+ fmt.Sprintf("unable to retrieve redis initialization key: %v", err))
71+ }
72+ if string(gval) != val {
73+ msgs = append(msgs,
74+ "the retrieved redis initialization key did not match the value we set")
75+ }
76+ }
77+
78+ err = client.Del(ctx, key)
79+ if err != nil {
80+ msgs = append(msgs, fmt.Sprintf("unable to delete the redis initialization key: %v", err))
81+ }
82+ return msgs
83+}
pkg/validation/sessions_test.go+250−28
@@ -1,14 +1,17 @@
11package validation
22
33import (
4− "testing"
54 "time"
65
6+ "github.com/Bose/minisentinel"
7+ "github.com/alicebob/miniredis/v2"
78 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/ginkgo/extensions/table"
811 . "github.com/onsi/gomega"
912)
1013
11−func Test_validateSessionCookieMinimal(t *testing.T) {
14+var _ = Describe("Sessions", func() {
1215 const (
1316 passAuthorizationMsg = "pass_authorization_header requires oauth tokens in sessions. session_cookie_minimal cannot be set"
1417 setAuthorizationMsg = "set_authorization_header requires oauth tokens in sessions. session_cookie_minimal cannot be set"
@@ -16,11 +19,16 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
1619 cookieRefreshMsg = "cookie_refresh > 0 requires oauth tokens in sessions. session_cookie_minimal cannot be set"
1720 )
1821
19− testCases := map[string]struct {
22+ type cookieMinimalTableInput struct {
2023 opts *options.Options
2124 errStrings []string
22− }{
23− "No minimal cookie session": {
25+ }
26+
27+ DescribeTable("validateSessionCookieMinimal",
28+ func(o *cookieMinimalTableInput) {
29+ Expect(validateSessionCookieMinimal(o.opts)).To(ConsistOf(o.errStrings))
30+ },
31+ Entry("No minimal cookie session", &cookieMinimalTableInput{
2432 opts: &options.Options{
2533 Session: options.SessionOptions{
2634 Cookie: options.CookieStoreOptions{
@@ -29,8 +37,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
2937 },
3038 },
3139 errStrings: []string{},
32− },
33− "No minimal cookie session & passAuthorization": {
40+ }),
41+ Entry("No minimal cookie session & passAuthorization", &cookieMinimalTableInput{
3442 opts: &options.Options{
3543 Session: options.SessionOptions{
3644 Cookie: options.CookieStoreOptions{
@@ -40,8 +48,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
4048 PassAuthorization: true,
4149 },
4250 errStrings: []string{},
43− },
44− "Minimal cookie session no conflicts": {
51+ }),
52+ Entry("Minimal cookie session no conflicts", &cookieMinimalTableInput{
4553 opts: &options.Options{
4654 Session: options.SessionOptions{
4755 Cookie: options.CookieStoreOptions{
@@ -50,8 +58,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
5058 },
5159 },
5260 errStrings: []string{},
53− },
54− "PassAuthorization conflict": {
61+ }),
62+ Entry("PassAuthorization conflict", &cookieMinimalTableInput{
5563 opts: &options.Options{
5664 Session: options.SessionOptions{
5765 Cookie: options.CookieStoreOptions{
@@ -61,8 +69,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
6169 PassAuthorization: true,
6270 },
6371 errStrings: []string{passAuthorizationMsg},
64− },
65− "SetAuthorization conflict": {
72+ }),
73+ Entry("SetAuthorization conflict", &cookieMinimalTableInput{
6674 opts: &options.Options{
6775 Session: options.SessionOptions{
6876 Cookie: options.CookieStoreOptions{
@@ -72,8 +80,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
7280 SetAuthorization: true,
7381 },
7482 errStrings: []string{setAuthorizationMsg},
75− },
76− "PassAccessToken conflict": {
83+ }),
84+ Entry("PassAccessToken conflict", &cookieMinimalTableInput{
7785 opts: &options.Options{
7886 Session: options.SessionOptions{
7987 Cookie: options.CookieStoreOptions{
@@ -83,8 +91,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
8391 PassAccessToken: true,
8492 },
8593 errStrings: []string{passAccessTokenMsg},
86− },
87− "CookieRefresh conflict": {
94+ }),
95+ Entry("CookieRefresh conflict", &cookieMinimalTableInput{
8896 opts: &options.Options{
8997 Cookie: options.Cookie{
9098 Refresh: time.Hour,
@@ -96,8 +104,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
96104 },
97105 },
98106 errStrings: []string{cookieRefreshMsg},
99− },
100− "Multiple conflicts": {
107+ }),
108+ Entry("Multiple conflicts", &cookieMinimalTableInput{
101109 opts: &options.Options{
102110 Session: options.SessionOptions{
103111 Cookie: options.CookieStoreOptions{
@@ -108,14 +116,228 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
108116 PassAccessToken: true,
109117 },
110118 errStrings: []string{passAuthorizationMsg, passAccessTokenMsg},
111− },
112− }
119+ }),
120+ )
113121
114− for testName, tc := range testCases {
115− t.Run(testName, func(t *testing.T) {
116− errStrings := validateSessionCookieMinimal(tc.opts)
117− g := NewWithT(t)
118− g.Expect(errStrings).To(ConsistOf(tc.errStrings))
119− })
122+ const (
123+ clusterAndSentinelMsg = "unable to initialize a redis client: options redis-use-sentinel and redis-use-cluster are mutually exclusive"
124+ parseWrongSchemeMsg = "unable to initialize a redis client: unable to parse redis url: invalid redis URL scheme: https"
125+ parseWrongFormatMsg = "unable to initialize a redis client: unable to parse redis url: invalid redis database number: \"wrong\""
126+ invalidPasswordSetMsg = "unable to set a redis initialization key: WRONGPASS invalid username-password pair"
127+ invalidPasswordDelMsg = "unable to delete the redis initialization key: WRONGPASS invalid username-password pair"
128+ unreachableRedisSetMsg = "unable to set a redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused"
129+ unreachableRedisDelMsg = "unable to delete the redis initialization key: dial tcp 127.0.0.1:65535: connect: connection refused"
130+ unreachableSentinelSetMsg = "unable to set a redis initialization key: redis: all sentinels are unreachable"
131+ unrechableSentinelDelMsg = "unable to delete the redis initialization key: redis: all sentinels are unreachable"
132+ )
133+
134+ type redisStoreTableInput struct {
135+ // miniredis setup details
136+ password string
137+ useSentinel bool
138+ setAddr bool
139+ setSentinelAddr bool
140+ setMasterName bool
141+
142+ opts *options.Options
143+ errStrings []string
120144 }
121−}
145+
146+ DescribeTable("validateRedisSessionStore",
147+ func(o *redisStoreTableInput) {
148+ mr, err := miniredis.Run()
149+ Expect(err).ToNot(HaveOccurred())
150+ mr.RequireAuth(o.password)
151+ defer mr.Close()
152+
153+ if o.setAddr && !o.useSentinel {
154+ o.opts.Session.Redis.ConnectionURL = "redis://" + mr.Addr()
155+ }
156+
157+ if o.useSentinel {
158+ ms := minisentinel.NewSentinel(mr)
159+ Expect(ms.Start()).To(Succeed())
160+ defer ms.Close()
161+
162+ if o.setSentinelAddr {
163+ o.opts.Session.Redis.SentinelConnectionURLs = []string{"redis://" + ms.Addr()}
164+ }
165+ if o.setMasterName {
166+ o.opts.Session.Redis.SentinelMasterName = ms.MasterInfo().Name
167+ }
168+ }
169+
170+ Expect(validateRedisSessionStore(o.opts)).To(ConsistOf(o.errStrings))
171+ },
172+ Entry("cookie sessions are skipped", &redisStoreTableInput{
173+ opts: &options.Options{
174+ Session: options.SessionOptions{
175+ Type: options.CookieSessionStoreType,
176+ },
177+ },
178+ errStrings: []string{},
179+ }),
180+ Entry("connect successfully to pure redis", &redisStoreTableInput{
181+ setAddr: true,
182+
183+ opts: &options.Options{
184+ Session: options.SessionOptions{
185+ Type: options.RedisSessionStoreType,
186+ },
187+ },
188+ errStrings: []string{},
189+ }),
190+ Entry("failed redis connection with wrong address", &redisStoreTableInput{
191+ opts: &options.Options{
192+ Session: options.SessionOptions{
193+ Type: options.RedisSessionStoreType,
194+ Redis: options.RedisStoreOptions{
195+ ConnectionURL: "redis://127.0.0.1:65535",
196+ },
197+ },
198+ },
199+ errStrings: []string{unreachableRedisSetMsg, unreachableRedisDelMsg},
200+ }),
201+ Entry("fail to parse an invalid connection URL with wrong scheme", &redisStoreTableInput{
202+ opts: &options.Options{
203+ Session: options.SessionOptions{
204+ Type: options.RedisSessionStoreType,
205+ Redis: options.RedisStoreOptions{
206+ ConnectionURL: "https://example.com",
207+ },
208+ },
209+ },
210+ errStrings: []string{parseWrongSchemeMsg},
211+ }),
212+ Entry("fail to parse an invalid connection URL with invalid format", &redisStoreTableInput{
213+ opts: &options.Options{
214+ Session: options.SessionOptions{
215+ Type: options.RedisSessionStoreType,
216+ Redis: options.RedisStoreOptions{
217+ ConnectionURL: "redis://127.0.0.1:6379/wrong",
218+ },
219+ },
220+ },
221+ errStrings: []string{parseWrongFormatMsg},
222+ }),
223+ Entry("connect successfully to pure redis with password", &redisStoreTableInput{
224+ password: "abcdef123",
225+ setAddr: true,
226+
227+ opts: &options.Options{
228+ Session: options.SessionOptions{
229+ Type: options.RedisSessionStoreType,
230+ Redis: options.RedisStoreOptions{
231+ Password: "abcdef123",
232+ },
233+ },
234+ },
235+ errStrings: []string{},
236+ }),
237+ Entry("failed connection with wrong password", &redisStoreTableInput{
238+ password: "abcdef123",
239+ setAddr: true,
240+
241+ opts: &options.Options{
242+ Session: options.SessionOptions{
243+ Type: options.RedisSessionStoreType,
244+ Redis: options.RedisStoreOptions{
245+ Password: "zyxwtuv987",
246+ },
247+ },
248+ },
249+ errStrings: []string{invalidPasswordSetMsg, invalidPasswordDelMsg},
250+ }),
251+ Entry("connect successfully to sentinel redis", &redisStoreTableInput{
252+ useSentinel: true,
253+ setSentinelAddr: true,
254+ setMasterName: true,
255+
256+ opts: &options.Options{
257+ Session: options.SessionOptions{
258+ Type: options.RedisSessionStoreType,
259+ Redis: options.RedisStoreOptions{
260+ UseSentinel: true,
261+ },
262+ },
263+ },
264+ errStrings: []string{},
265+ }),
266+ Entry("connect successfully to sentinel redis with password", &redisStoreTableInput{
267+ password: "abcdef123",
268+ useSentinel: true,
269+ setSentinelAddr: true,
270+ setMasterName: true,
271+
272+ opts: &options.Options{
273+ Session: options.SessionOptions{
274+ Type: options.RedisSessionStoreType,
275+ Redis: options.RedisStoreOptions{
276+ Password: "abcdef123",
277+ UseSentinel: true,
278+ },
279+ },
280+ },
281+ errStrings: []string{},
282+ }),
283+ Entry("failed connection to sentinel redis with wrong password", &redisStoreTableInput{
284+ password: "abcdef123",
285+ useSentinel: true,
286+ setSentinelAddr: true,
287+ setMasterName: true,
288+
289+ opts: &options.Options{
290+ Session: options.SessionOptions{
291+ Type: options.RedisSessionStoreType,
292+ Redis: options.RedisStoreOptions{
293+ Password: "zyxwtuv987",
294+ UseSentinel: true,
295+ },
296+ },
297+ },
298+ errStrings: []string{invalidPasswordSetMsg, invalidPasswordDelMsg},
299+ }),
300+ Entry("failed connection to sentinel redis with wrong master name", &redisStoreTableInput{
301+ useSentinel: true,
302+ setSentinelAddr: true,
303+
304+ opts: &options.Options{
305+ Session: options.SessionOptions{
306+ Type: options.RedisSessionStoreType,
307+ Redis: options.RedisStoreOptions{
308+ UseSentinel: true,
309+ SentinelMasterName: "WRONG",
310+ },
311+ },
312+ },
313+ errStrings: []string{unreachableSentinelSetMsg, unrechableSentinelDelMsg},
314+ }),
315+ Entry("failed connection to sentinel redis with wrong address", &redisStoreTableInput{
316+ useSentinel: true,
317+ setMasterName: true,
318+
319+ opts: &options.Options{
320+ Session: options.SessionOptions{
321+ Type: options.RedisSessionStoreType,
322+ Redis: options.RedisStoreOptions{
323+ UseSentinel: true,
324+ SentinelConnectionURLs: []string{"redis://127.0.0.1:65535"},
325+ },
326+ },
327+ },
328+ errStrings: []string{unreachableSentinelSetMsg, unrechableSentinelDelMsg},
329+ }),
330+ Entry("sentinel and cluster both enabled fails", &redisStoreTableInput{
331+ opts: &options.Options{
332+ Session: options.SessionOptions{
333+ Type: options.RedisSessionStoreType,
334+ Redis: options.RedisStoreOptions{
335+ UseSentinel: true,
336+ UseCluster: true,
337+ },
338+ },
339+ },
340+ errStrings: []string{clusterAndSentinelMsg},
341+ }),
342+ )
343+})