git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #722 from grnhse/redis-startup-validation
160685aJoel Speed
CHANGELOG.md+3
@@ -11,9 +11,12 @@
11
11
12## Breaking Changes
12## Breaking Changes
13
13
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+
14## Changes since v6.1.1
16## Changes since v6.1.1
15
17
16- [#575](https://github.com/oauth2-proxy/oauth2-proxy/pull/575) Stop accepting legacy SHA1 signed cookies (@NickMeves)
18- [#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)
17- [#764](https://github.com/oauth2-proxy/oauth2-proxy/pull/764) Document bcrypt encryption for htpasswd (and hide SHA) (@lentzi90)
20- [#764](https://github.com/oauth2-proxy/oauth2-proxy/pull/764) Document bcrypt encryption for htpasswd (and hide SHA) (@lentzi90)
18- [#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)
21- [#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)
19
22
pkg/sessions/redis/redis_store.go+3−3
@@ -23,7 +23,7 @@ type SessionStore struct {
23// NewRedisSessionStore initialises a new instance of the SessionStore and wraps
23// NewRedisSessionStore initialises a new instance of the SessionStore and wraps
24// it in a persistence.Manager
24// it in a persistence.Manager
25func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) {
25func NewRedisSessionStore(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessions.SessionStore, error) {
26− client, err := newRedisClient(opts.Redis)
26+ client, err := NewRedisClient(opts.Redis)
27 if err != nil {
27 if err != nil {
28 return nil, fmt.Errorf("error constructing redis client: %v", err)
28 return nil, fmt.Errorf("error constructing redis client: %v", err)
29 }
29 }
@@ -64,9 +64,9 @@ func (store *SessionStore) Clear(ctx context.Context, key string) error {
64 return nil
64 return nil
65}
65}
66
66
67−// newRedisClient makes a redis.Client (either standalone, sentinel aware, or
67+// NewRedisClient makes a redis.Client (either standalone, sentinel aware, or
68// redis cluster)
68// redis cluster)
69−func newRedisClient(opts options.RedisStoreOptions) (Client, error) {
69+func NewRedisClient(opts options.RedisStoreOptions) (Client, error) {
70 if opts.UseSentinel && opts.UseCluster {
70 if opts.UseSentinel && opts.UseCluster {
71 return nil, fmt.Errorf("options redis-use-sentinel and redis-use-cluster are mutually exclusive")
71 return nil, fmt.Errorf("options redis-use-sentinel and redis-use-cluster are mutually exclusive")
72 }
72 }
pkg/validation/options.go+1
@@ -28,6 +28,7 @@ import (
28func Validate(o *options.Options) error {
28func Validate(o *options.Options) error {
29 msgs := validateCookie(o.Cookie)
29 msgs := validateCookie(o.Cookie)
30 msgs = append(msgs, validateSessionCookieMinimal(o)...)
30 msgs = append(msgs, validateSessionCookieMinimal(o)...)
31+ msgs = append(msgs, validateRedisSessionStore(o)...)
31
32
32 if o.SSLInsecureSkipVerify {
33 if o.SSLInsecureSkipVerify {
33 // InsecureSkipVerify is a configurable option we allow
34 // InsecureSkipVerify is a configurable option we allow
pkg/validation/sessions.go+51
@@ -1,9 +1,13 @@
1package validation
1package validation
2
2
3import (
3import (
4+ "context"
5+ "fmt"
4 "time"
6 "time"
5
7
6 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
8 "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"
7)
11)
8
12
9func validateSessionCookieMinimal(o *options.Options) []string {
13func validateSessionCookieMinimal(o *options.Options) []string {
@@ -30,3 +34,50 @@ func validateSessionCookieMinimal(o *options.Options) []string {
30 }
34 }
31 return msgs
35 return msgs
32}
36}
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 @@
1package validation
1package validation
2
2
3import (
3import (
4− "testing"
5 "time"
4 "time"
6
5
6+ "github.com/Bose/minisentinel"
7+ "github.com/alicebob/miniredis/v2"
7 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
8 "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
9+ . "github.com/onsi/ginkgo"
10+ . "github.com/onsi/ginkgo/extensions/table"
8 . "github.com/onsi/gomega"
11 . "github.com/onsi/gomega"
9)
12)
10
13
11−func Test_validateSessionCookieMinimal(t *testing.T) {
14+var _ = Describe("Sessions", func() {
12 const (
15 const (
13 passAuthorizationMsg = "pass_authorization_header requires oauth tokens in sessions. session_cookie_minimal cannot be set"
16 passAuthorizationMsg = "pass_authorization_header requires oauth tokens in sessions. session_cookie_minimal cannot be set"
14 setAuthorizationMsg = "set_authorization_header requires oauth tokens in sessions. session_cookie_minimal cannot be set"
17 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) {
16 cookieRefreshMsg = "cookie_refresh > 0 requires oauth tokens in sessions. session_cookie_minimal cannot be set"
19 cookieRefreshMsg = "cookie_refresh > 0 requires oauth tokens in sessions. session_cookie_minimal cannot be set"
17 )
20 )
18
21
19− testCases := map[string]struct {
22+ type cookieMinimalTableInput struct {
20 opts *options.Options
23 opts *options.Options
21 errStrings []string
24 errStrings []string
22− }{
25+ }
26+
27+ DescribeTable("validateSessionCookieMinimal",
28+ func(o *cookieMinimalTableInput) {
29+ Expect(validateSessionCookieMinimal(o.opts)).To(ConsistOf(o.errStrings))
30+ },
23− "No minimal cookie session": {
31+ Entry("No minimal cookie session", &cookieMinimalTableInput{
24 opts: &options.Options{
32 opts: &options.Options{
25 Session: options.SessionOptions{
33 Session: options.SessionOptions{
26 Cookie: options.CookieStoreOptions{
34 Cookie: options.CookieStoreOptions{
@@ -29,8 +37,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
29 },
37 },
30 },
38 },
31 errStrings: []string{},
39 errStrings: []string{},
32− },
40+ }),
33− "No minimal cookie session & passAuthorization": {
41+ Entry("No minimal cookie session & passAuthorization", &cookieMinimalTableInput{
34 opts: &options.Options{
42 opts: &options.Options{
35 Session: options.SessionOptions{
43 Session: options.SessionOptions{
36 Cookie: options.CookieStoreOptions{
44 Cookie: options.CookieStoreOptions{
@@ -40,8 +48,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
40 PassAuthorization: true,
48 PassAuthorization: true,
41 },
49 },
42 errStrings: []string{},
50 errStrings: []string{},
43− },
51+ }),
44− "Minimal cookie session no conflicts": {
52+ Entry("Minimal cookie session no conflicts", &cookieMinimalTableInput{
45 opts: &options.Options{
53 opts: &options.Options{
46 Session: options.SessionOptions{
54 Session: options.SessionOptions{
47 Cookie: options.CookieStoreOptions{
55 Cookie: options.CookieStoreOptions{
@@ -50,8 +58,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
50 },
58 },
51 },
59 },
52 errStrings: []string{},
60 errStrings: []string{},
53− },
61+ }),
54− "PassAuthorization conflict": {
62+ Entry("PassAuthorization conflict", &cookieMinimalTableInput{
55 opts: &options.Options{
63 opts: &options.Options{
56 Session: options.SessionOptions{
64 Session: options.SessionOptions{
57 Cookie: options.CookieStoreOptions{
65 Cookie: options.CookieStoreOptions{
@@ -61,8 +69,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
61 PassAuthorization: true,
69 PassAuthorization: true,
62 },
70 },
63 errStrings: []string{passAuthorizationMsg},
71 errStrings: []string{passAuthorizationMsg},
64− },
72+ }),
65− "SetAuthorization conflict": {
73+ Entry("SetAuthorization conflict", &cookieMinimalTableInput{
66 opts: &options.Options{
74 opts: &options.Options{
67 Session: options.SessionOptions{
75 Session: options.SessionOptions{
68 Cookie: options.CookieStoreOptions{
76 Cookie: options.CookieStoreOptions{
@@ -72,8 +80,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
72 SetAuthorization: true,
80 SetAuthorization: true,
73 },
81 },
74 errStrings: []string{setAuthorizationMsg},
82 errStrings: []string{setAuthorizationMsg},
75− },
83+ }),
76− "PassAccessToken conflict": {
84+ Entry("PassAccessToken conflict", &cookieMinimalTableInput{
77 opts: &options.Options{
85 opts: &options.Options{
78 Session: options.SessionOptions{
86 Session: options.SessionOptions{
79 Cookie: options.CookieStoreOptions{
87 Cookie: options.CookieStoreOptions{
@@ -83,8 +91,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
83 PassAccessToken: true,
91 PassAccessToken: true,
84 },
92 },
85 errStrings: []string{passAccessTokenMsg},
93 errStrings: []string{passAccessTokenMsg},
86− },
94+ }),
87− "CookieRefresh conflict": {
95+ Entry("CookieRefresh conflict", &cookieMinimalTableInput{
88 opts: &options.Options{
96 opts: &options.Options{
89 Cookie: options.Cookie{
97 Cookie: options.Cookie{
90 Refresh: time.Hour,
98 Refresh: time.Hour,
@@ -96,8 +104,8 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
96 },
104 },
97 },
105 },
98 errStrings: []string{cookieRefreshMsg},
106 errStrings: []string{cookieRefreshMsg},
99− },
107+ }),
100− "Multiple conflicts": {
108+ Entry("Multiple conflicts", &cookieMinimalTableInput{
101 opts: &options.Options{
109 opts: &options.Options{
102 Session: options.SessionOptions{
110 Session: options.SessionOptions{
103 Cookie: options.CookieStoreOptions{
111 Cookie: options.CookieStoreOptions{
@@ -108,14 +116,228 @@ func Test_validateSessionCookieMinimal(t *testing.T) {
108 PassAccessToken: true,
116 PassAccessToken: true,
109 },
117 },
110 errStrings: []string{passAuthorizationMsg, passAccessTokenMsg},
118 errStrings: []string{passAuthorizationMsg, passAccessTokenMsg},
111− },
119+ }),
112− }
120+ )
113
121
114− for testName, tc := range testCases {
122+ const (
115− t.Run(testName, func(t *testing.T) {
123+ clusterAndSentinelMsg = "unable to initialize a redis client: options redis-use-sentinel and redis-use-cluster are mutually exclusive"
116− errStrings := validateSessionCookieMinimal(tc.opts)
124+ parseWrongSchemeMsg = "unable to initialize a redis client: unable to parse redis url: invalid redis URL scheme: https"
117− g := NewWithT(t)
125+ parseWrongFormatMsg = "unable to initialize a redis client: unable to parse redis url: invalid redis database number: \"wrong\""
118− g.Expect(errStrings).To(ConsistOf(tc.errStrings))
126+ invalidPasswordSetMsg = "unable to set a redis initialization key: WRONGPASS invalid username-password pair"
119− })
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
120 }
144 }
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()
121−}
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+})