From a70254ab76baf97a217b3e357d0f49bf3a1263fc Mon Sep 17 00:00:00 2001 From: Caleb Doxsey Date: Tue, 14 Jul 2020 08:33:24 -0600 Subject: [PATCH] kubernetes apiserver integration (#1063) * sessions: support bearer tokens in authorization * wip * remove dead code * refactor signed jwt code * use function * update per comments * fix test --- authorize/check_response.go | 34 +++++-- authorize/evaluator/evaluator.go | 99 +++++++++++++------ authorize/evaluator/evaluator_test.go | 4 +- authorize/evaluator/opa/policy/authz.rego | 7 +- authorize/evaluator/opa/policy/statik.go | 2 +- authorize/grpc.go | 2 +- config/policy.go | 3 + go.sum | 9 -- internal/sessions/header/header_store.go | 14 ++- internal/sessions/header/header_store_test.go | 23 +++++ 10 files changed, 140 insertions(+), 57 deletions(-) create mode 100644 internal/sessions/header/header_store_test.go diff --git a/authorize/check_response.go b/authorize/check_response.go index 91e71a533..cf8109455 100644 --- a/authorize/check_response.go +++ b/authorize/check_response.go @@ -9,6 +9,7 @@ import ( envoy_api_v2_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core" envoy_service_auth_v2 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v2" envoy_type "github.com/envoyproxy/go-control-plane/envoy/type" + "github.com/golang/protobuf/ptypes/wrappers" "google.golang.org/genproto/googleapis/rpc/status" "google.golang.org/grpc/codes" @@ -25,7 +26,9 @@ func (a *Authorize) okResponse(reply *evaluator.Result) *envoy_service_auth_v2.C } requestHeaders = append(requestHeaders, - mkHeader(httputil.HeaderPomeriumJWTAssertion, reply.SignedJWT)) + mkHeader(httputil.HeaderPomeriumJWTAssertion, reply.SignedJWT, false)) + + requestHeaders = append(requestHeaders, getKubernetesHeaders(reply)...) return &envoy_service_auth_v2.CheckResponse{ Status: &status.Status{Code: int32(codes.OK), Message: "OK"}, @@ -82,10 +85,10 @@ func (a *Authorize) htmlDeniedResponse(code int32, reason string, headers map[st } envoyHeaders := []*envoy_api_v2_core.HeaderValueOption{ - mkHeader("Content-Type", "text/html"), + mkHeader("Content-Type", "text/html", false), } for k, v := range headers { - envoyHeaders = append(envoyHeaders, mkHeader(k, v)) + envoyHeaders = append(envoyHeaders, mkHeader(k, v, false)) } return &envoy_service_auth_v2.CheckResponse{ @@ -104,10 +107,10 @@ func (a *Authorize) htmlDeniedResponse(code int32, reason string, headers map[st func (a *Authorize) plainTextDeniedResponse(code int32, reason string, headers map[string]string) *envoy_service_auth_v2.CheckResponse { envoyHeaders := []*envoy_api_v2_core.HeaderValueOption{ - mkHeader("Content-Type", "text/plain"), + mkHeader("Content-Type", "text/plain", false), } for k, v := range headers { - envoyHeaders = append(envoyHeaders, mkHeader(k, v)) + envoyHeaders = append(envoyHeaders, mkHeader(k, v, false)) } return &envoy_service_auth_v2.CheckResponse{ @@ -138,11 +141,30 @@ func (a *Authorize) redirectResponse(in *envoy_service_auth_v2.CheckRequest) *en }) } -func mkHeader(k, v string) *envoy_api_v2_core.HeaderValueOption { +func getKubernetesHeaders(reply *evaluator.Result) []*envoy_api_v2_core.HeaderValueOption { + var requestHeaders []*envoy_api_v2_core.HeaderValueOption + if reply.MatchingPolicy != nil && reply.MatchingPolicy.KubernetesServiceAccountToken != "" { + requestHeaders = append(requestHeaders, + mkHeader("Authorization", "Bearer "+reply.MatchingPolicy.KubernetesServiceAccountToken, false)) + + if reply.UserEmail != "" { + requestHeaders = append(requestHeaders, mkHeader("Impersonate-User", reply.UserEmail, false)) + } + for _, group := range reply.UserGroups { + requestHeaders = append(requestHeaders, mkHeader("Impersonate-Group", group, true)) + } + } + return requestHeaders +} + +func mkHeader(k, v string, shouldAppend bool) *envoy_api_v2_core.HeaderValueOption { return &envoy_api_v2_core.HeaderValueOption{ Header: &envoy_api_v2_core.HeaderValue{ Key: k, Value: v, }, + Append: &wrappers.BoolValue{ + Value: shouldAppend, + }, } } diff --git a/authorize/evaluator/evaluator.go b/authorize/evaluator/evaluator.go index 07726ce20..fc783ca33 100644 --- a/authorize/evaluator/evaluator.go +++ b/authorize/evaluator/evaluator.go @@ -38,8 +38,9 @@ const ( // Evaluator specifies the interface for a policy engine. type Evaluator struct { - rego *rego.Rego - query rego.PreparedEvalQuery + rego *rego.Rego + query rego.PreparedEvalQuery + policies []config.Policy clientCA string authenticateHost string @@ -51,6 +52,7 @@ type Evaluator struct { func New(options *config.Options) (*Evaluator, error) { e := &Evaluator{ authenticateHost: options.AuthenticateURL.Host, + policies: options.Policies, } if options.ClientCA != "" { e.clientCA = options.ClientCA @@ -129,33 +131,40 @@ func (e *Evaluator) Evaluate(ctx context.Context, req *Request) (*Result, error) return &deny[0], nil } - signedJWT, err := e.SignedJWT(req) + payload := e.JWTPayload(req) + + signedJWT, err := e.SignedJWT(payload) if err != nil { return nil, fmt.Errorf("error signing JWT: %w", err) } + evalResult := &Result{ + MatchingPolicy: getMatchingPolicy(res[0].Bindings.WithoutWildcards(), e.policies), + SignedJWT: signedJWT, + } + if e, ok := payload["email"].(string); ok { + evalResult.UserEmail = e + } + if gs, ok := payload["groups"].([]string); ok { + evalResult.UserGroups = gs + } + allow := allowed(res[0].Bindings.WithoutWildcards()) if allow { - return &Result{ - Status: http.StatusOK, - Message: "OK", - SignedJWT: signedJWT, - }, nil + evalResult.Status = http.StatusOK + evalResult.Message = "OK" + return evalResult, nil } if req.Session.ID == "" { - return &Result{ - Status: http.StatusUnauthorized, - Message: "login required", - SignedJWT: signedJWT, - }, nil + evalResult.Status = http.StatusUnauthorized + evalResult.Message = "login required" + return evalResult, nil } - return &Result{ - Status: http.StatusForbidden, - Message: "forbidden", - SignedJWT: signedJWT, - }, nil + evalResult.Status = http.StatusForbidden + evalResult.Message = "forbidden" + return evalResult, nil } // ParseSignedJWT parses the input signature and return its payload. @@ -167,17 +176,8 @@ func (e *Evaluator) ParseSignedJWT(signature string) ([]byte, error) { return object.Verify(&(e.jwk.(*ecdsa.PrivateKey).PublicKey)) } -// SignedJWT returns the signature of given request. -func (e *Evaluator) SignedJWT(req *Request) (string, error) { - signerOpt := &jose.SignerOptions{} - signer, err := jose.NewSigner(jose.SigningKey{ - Algorithm: jose.ES256, - Key: e.jwk, - }, signerOpt.WithHeader("kid", e.kid)) - if err != nil { - return "", err - } - +// JWTPayload returns the JWT payload for a request. +func (e *Evaluator) JWTPayload(req *Request) map[string]interface{} { payload := map[string]interface{}{ "iss": e.authenticateHost, } @@ -200,6 +200,19 @@ func (e *Evaluator) SignedJWT(req *Request) (string, error) { payload["groups"] = du.GetGroups() } } + return payload +} + +// SignedJWT returns the signature of given request. +func (e *Evaluator) SignedJWT(payload map[string]interface{}) (string, error) { + signerOpt := &jose.SignerOptions{} + signer, err := jose.NewSigner(jose.SigningKey{ + Algorithm: jose.ES256, + Key: e.jwk, + }, signerOpt.WithHeader("kid", e.kid)) + if err != nil { + return "", err + } bs, err := json.Marshal(payload) if err != nil { @@ -266,9 +279,31 @@ type ( // Result is the result of evaluation. type Result struct { - Status int - Message string - SignedJWT string + Status int + Message string + SignedJWT string + MatchingPolicy *config.Policy + + UserEmail string + UserGroups []string +} + +func getMatchingPolicy(vars rego.Vars, policies []config.Policy) *config.Policy { + result, ok := vars["result"].(map[string]interface{}) + if !ok { + return nil + } + + idx, err := strconv.Atoi(fmt.Sprint(result["route_policy_idx"])) + if err != nil { + return nil + } + + if idx >= len(policies) { + return nil + } + + return &policies[idx] } func allowed(vars rego.Vars) bool { diff --git a/authorize/evaluator/evaluator_test.go b/authorize/evaluator/evaluator_test.go index 6fc7c9887..f8833764d 100644 --- a/authorize/evaluator/evaluator_test.go +++ b/authorize/evaluator/evaluator_test.go @@ -80,7 +80,7 @@ func TestEvaluator_SignedJWT(t *testing.T) { URL: "https://example.com", }, } - signedJWT, err := e.SignedJWT(req) + signedJWT, err := e.SignedJWT(e.JWTPayload(req)) require.NoError(t, err) assert.NotEmpty(t, signedJWT) @@ -101,7 +101,7 @@ func TestEvaluator_JWTWithKID(t *testing.T) { URL: "https://example.com", }, } - signedJWT, err := e.SignedJWT(req) + signedJWT, err := e.SignedJWT(e.JWTPayload(req)) require.NoError(t, err) assert.NotEmpty(t, signedJWT) diff --git a/authorize/evaluator/opa/policy/authz.rego b/authorize/evaluator/opa/policy/authz.rego index dedfc4b16..db7a8f0d8 100644 --- a/authorize/evaluator/opa/policy/authz.rego +++ b/authorize/evaluator/opa/policy/authz.rego @@ -3,7 +3,8 @@ package pomerium.authz default allow = false -route_policy := first_allowed_route_policy(input.http.url) +route_policy_idx := first_allowed_route_policy_idx(input.http.url) +route_policy := data.route_policies[route_policy_idx] session := input.databroker_data.session user := input.databroker_data.user directory_user := input.databroker_data.directory_user @@ -84,8 +85,8 @@ deny[reason] { } # returns the first matching route -first_allowed_route_policy(input_url) = first_policy { - first_policy := [policy | some i, policy; policy = data.route_policies[i]; allowed_route(input.http.url, policy)][0] +first_allowed_route_policy_idx(input_url) = first_policy_idx { + first_policy_idx := [idx | some idx, policy; policy = data.route_policies[idx]; allowed_route(input.http.url, policy)][0] } allowed_route(input_url, policy){ diff --git a/authorize/evaluator/opa/policy/statik.go b/authorize/evaluator/opa/policy/statik.go index d38c16221..a497cdd4b 100644 --- a/authorize/evaluator/opa/policy/statik.go +++ b/authorize/evaluator/opa/policy/statik.go @@ -9,6 +9,6 @@ import ( const Rego = "rego" // static asset namespace func init() { - data := "PK\x03\x04\x14\x00\x08\x00\x08\x00ct\xd9P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00 \x00authz.regoUT\x05\x00\x01\x1a\xb6\xf4^\xacWMs\xdb6\x10=\x13\xbfb\x83\\\xc4\x96\xa6\x92~\x1c\xaa\x8c\xeafr\xea\xa1u&iO\x1c\x85\x81\xc8\x95\x88\x84\x04X\x00\xac\xed\xb8\xfe\xef\x9d\x05H\x8b\x92%\xda\xads\xb1\xc8\xc5\xc3\xdb}\xbb\xcb\x05\xdc\x8a\xe2\xb3\xd8\"\xb4\xbaA#\xbb&\x15\x9d\xab\xbe0V\xe2Ft\xb5\x03Q\xd7\xfa\x12\x96\xb0\x11\xb5E\xc6\x98\xd1\x9d\xc3\xbc\xd5\xb5,\xaea\xb1\x84\x8d4\xd6\xe5\x1e\x85e>^\x9dI\xd5v.\xad\x9ck\xd3\xce\xd41\xb3h\xad\xd4\x8av\x85\xa5R8\xb16\xfa3\x9a\x9c\x1e\xd3\x1e\xc0:\x8b\xe64\x8aVY)\x0d\x16N\x9b\xeb|\x1a\xbc\x8fc\x8c=\xef\x15\xb5\xdd\xba\x96\x05\x0b/7,\x1aG\x9e\xbe&\xeb[\x8f\xf8SQBP9Y\x08\x87\xe5\xeb\xa2@ka\xb9\x04g:d\xb7;\xc2B\x1b\x0b\xad\xc1M-\xb7\x95;A\xfc\xe6\xe2\xdd\xfb@>\x00\xef\xa8\xa2Q\xba\x1at\x95.i\x89_\xbc\xfd\xe3\xd7\x8b\xdf\xdfs\x16\x15\xbaSn\xa6\xd7\x9f\xb0p\xe9\x16\xdd8\xbf\x15\x8a\x12\x8dM\x80\x87\x00\xcf\xdeh\xe5\x8c\xae\xcf\xde\xe1_\x1dZw\xf6\x9bg\xe4 d\xab8\x86\x9f\xe1\xc5c\xf9.\x8c\xdcJ5\xde8\xd2\xbc\xbe\x06l\x84\xacwj)\xcb\xa9\xb7Q\xf4{\xda\x87\x1e!\x88\xcd\xf2\xd5\x98hkt\xd7\xeeX\xacn\xb0\xb7E\xfb\x15L\xbd\x95\xb6\xc3r@\x1c\xf5r\x1f\xb7\x1f\xb7lZ4V+\xe1\xf0P\xc3d\xd40tZ\xdf\xac\xe9\x88(\x0fD\xa7\x1dM\xc8\x0f\xf8|\x84\xef+\xfc .$\xd6\xa0\xeb\x8c\xb2\xe0*\x0cG\x1f4\xc2\x15\x15%\xd4\xa7\x92=t\x1e\xe6t\x14\xc2pn\xf6\xe7\xe8\x0d\x8b\xf6\xde\x17K\xc8\xfa\xc7\x7f\xc0\x7f.2\x81`x\xd5\xff\xc2\x12|\xaeG\x1e$\xdaL\xae^\xc1\x9e\xf3{-\x13\xb6\xc7\xab\xec\x85\x9f\xc0G\xc0\xf9\x18w\xd3\x8f&2\xe6z\xfd\x89bk\x85\xb1H\x86\x91$\x16\xedk\xb6\xba3\xc5\x88\x90\xf6\xde\x91\x1e\x82\xe9\xd8\x94W\x8f\x05\x0bW=\x12jp\x8b'i\x0f\xc5O\x87L5\x1a\x9d\x95\xc1\x9a\x00\x0f\x9bx\x02\x9c\xc7\xfe\xc0\xe6\xec\xf6\xab\xf3>\xf3\xbcQ\xb0\x1d\xafD?\x10\x03$>(ZZi\xebo\x1a\xfb\x0c\xde|?\x0f\x93\xd58\x15o\xd84\x99\x87\xa7\xf3\x0eyp\xc28{)\x0f\xfb \xa5\xd6\x18\x18\xd3\xb0\xf3H\x9d'\x1a\xe8d\x14\xc2U\xd3\xda\x9e\xc4\xd9\xeb\x1a\x02\x17\xae\"7\xf7\xb5\xdd\xd72\xd5\xe1\xa7\x1c\xfb=\x93j\x9e\xca\xda\xeb1\x98\xfb\xe984\xa7\x87$Gt\xf9\"\xedz\xd9:C#\xf2\x06\xb8-*l\x90/ <$\xc0\xa9e\xf9\x02\xe8g\xc8\xe1\x02|\xc6n)\xb2,O\xee\xb0\x01c\xc4%-\xd3\xad\xc8\xfbO7R\x954\xa0s\xeb\x8cT\xdb\xdcvk\x1fe\xaef,\x8a>\xce\xce\x173\x9a\x96\x99]\x9d\xc7\x8b\xf9<>\x9fe\x1f\xe6\xabo\xe3Y\xf6\xe1\xfc\xf9\xea\x9b\xf8c\xc2\xa2\xc8:\x93\xc0\xcb\x98\x86h\x14\xea\x05J\x9bF\xd4\xf2K\xf8\xbc|C\xf4\xbe\xbd\xbc#\xcb\xbdN>\xe7\xfeZ\xe4\xcc]9N\x83 \xd5\x83\x9f\xf5`vx\x87\xeaoJ\xe1\xcd\x17\xec\x8a\x86\x85mk\xe9\x86E\xfe\x0b\x8f\x87\xff\x01\xae|\x1f|\xc7\xa2\xab\xec\xe5\x8a\x1e\xfb\xcb\x19Q\x1f\x9c\xef\xf4'\xf1\xa7>\xf1\x02\xd0{\xb8r\x92\x8d\xdd\xb2\x7f\x03\x00\x00\xff\xffPK\x07\x08\xcb\xeau\x85!\x04\x00\x00P\x0e\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xaan\xd9P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00 \x00authz_test.regoUT\x05\x00\x01P\xac\xf4^\xccWAO\xeb8\x10>'\xbf\xc2\xf2iY\xb5\x8d\xb8VB\x0bB\xab\xd5\x1ev\x8b\x80w\xaa\xaa\xc8M\xe65~$q\xb0\x1dQ\xa8\xfa\xdf\x9f\xc6v\xd2\xa6$\x0f\x13\x81\x1e\\Hl\xcf\xcc7\xdf|\x9f \x15K\x1e\xd8\x06H%\n\x90\xbc.f\xac\xd6\xd9K\x18jP:\x86\x82\xf1\xbd\x9b7fsh\x88\xa5\x05/]\xcdL(}\x8a\xa63\xbdDH\x15\xa3Ps\xbe\xc9\xf4\xef\x99\xa1=x\xbd\xb8\xbd\xb32n\xd0xX\xddD\x16\xa03a\xae\xaf\xc5\xcd\xfd\xbf\x8b\xff\xef\\\xe9a\xb2\x1av\x80\xa5\x16\x95\xf3\xd8B\xf2\x0d/\x0dJ%\n\x10\xf6u\xd5\x98\xccXiz-J-E>\xbd\x85\xc7\x1a\x94\x9e\xfe\xd7\x94_\xd2\x7f\xfe\xbew\xceu$\xf7q\xfce\xc5\xf5\x85yt\xfedRA\\\xcb\x1c\xeb\xe0\xaf\xf9\x05i\xd7\xfe\xe8\x03\x88\xd5#\xfc\xd8\xf8\xebQ\xd13\x134SI\x06\x05\x90\x8b\x0b\xdb\x12\xb5\xab\xe8\x14\xb3\xd6\xb5\nna\xbc\xd9:\xa4\xa3-\xa6\x86x;5;\xa2\xd6>\xcdz\x1f6:!\xbb\x81\x91\xee\xcf\xde\x1f\xdfs`l\x1a5&O\xf4Q\x89\xde\xce\x13\xf9%\xf2@d3\xb56\x1c\xcc&\xe4\xe6\xd7\xe3\xeaW\x83\xfd\x00\xf0W\xc3\xd1\x07\x83g\x8b\xe6\xae7\xb24\xaa\x87\x0b\xf0\x19{@\xcf2\x96\x8f\xaff\xd9\xfb\xf9\xea\xdbx\x96\xbd\xbfz\xbe\xfa&\xfe\x90\x90(\xb2\xce$\xf0\"\xc6!\x1a\x85z\x81\xd2\xa6\xe6\x95\xfc\x1c>/\xdf\x10\x9dm\x1f\xde\x91\xe3.N:\xa7\xfe\x8e\xe4\xccc9N\x83\x11\xd5\x81\x9fu`2\xbePu\xd7\xa6\xf0\xe6\x0b\xe6w\x8am*\xe9\xfaC\xfa\x0b\x8d\xfb\x7f\x08n}\x1f|G\xa2\xdb\xec\xc5\n\x7fv75\xa4\x1e-{\xfc\x93\xf8+\x00\xf2\x02\xe0{\xb8\x7f\xa2\x8c<\x90\x7f\x03\x00\x00\xff\xffPK\x07\x08\x9f\xedE\xac,\x04\x00\x00\x9b\x0e\x00\x00PK\x03\x04\x14\x00\x08\x00\x08\x00\xaan\xd9P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00 \x00authz_test.regoUT\x05\x00\x01P\xac\xf4^\xccWAO\xeb8\x10>'\xbf\xc2\xf2iY\xb5\x8d\xb8VB\x0bB\xab\xd5\x1ev\x8b\x80w\xaa\xaa\xc8M\xe65~$q\xb0\x1dQ\xa8\xfa\xdf\x9f\xc6v\xd2\xa6$\x0f\x13\x81\x1e\\Hl\xcf\xcc7\xdf|\x9f \x15K\x1e\xd8\x06H%\n\x90\xbc.f\xac\xd6\xd9K\x18jP:\x86\x82\xf1\xbd\x9b7fsh\x88\xa5\x05/]\xcdL(}\x8a\xa63\xbdDH\x15\xa3Ps\xbe\xc9\xf4\xef\x99\xa1=x\xbd\xb8\xbd\xb32n\xd0xX\xddD\x16\xa03a\xae\xaf\xc5\xcd\xfd\xbf\x8b\xff\xef\\\xe9a\xb2\x1av\x80\xa5\x16\x95\xf3\xd8B\xf2\x0d/\x0dJ%\n\x10\xf6u\xd5\x98\xccXiz-J-E>\xbd\x85\xc7\x1a\x94\x9e\xfe\xd7\x94_\xd2\x7f\xfe\xbew\xceu$\xf7q\xfce\xc5\xf5\x85yt\xfedRA\\\xcb\x1c\xeb\xe0\xaf\xf9\x05i\xd7\xfe\xe8\x03\x88\xd5#\xfc\xd8\xf8\xebQ\xd13\x134SI\x06\x05\x90\x8b\x0b\xdb\x12\xb5\xab\xe8\x14\xb3\xd6\xb5\nna\xbc\xd9:\xa4\xa3-\xa6\x86x;5;\xa2\xd6>\xcdz\x1f6:!\xbb\x81\x91\xee\xcf\xde\x1f\xdfs`l\x1a5&O\xf4Q\x89\xde\xce\x13\xf9%\xf2@d3\xb56\x1c\xcc&\xe4\xe6\xd7\xe3\xeaW\x83\xfd\x00\xf0W\xc3\xd1\x07\x83g\x8b\xe6\xae7\xb24\xaa atSize && strings.EqualFold(bearer[0:atSize], authType) { - return bearer[atSize+1:] + // Authorization: Pomerium + prefix := authType + " " + if strings.HasPrefix(bearer, prefix) { + return bearer[len(prefix):] } + + // Authorization: Bearer Pomerium- + prefix = "Bearer " + authType + "-" + if strings.HasPrefix(bearer, prefix) { + return bearer[len(prefix):] + } + return "" } diff --git a/internal/sessions/header/header_store_test.go b/internal/sessions/header/header_store_test.go new file mode 100644 index 000000000..4d92f1b8b --- /dev/null +++ b/internal/sessions/header/header_store_test.go @@ -0,0 +1,23 @@ +package header + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTokenFromHeader(t *testing.T) { + t.Run("pomerium type", func(t *testing.T) { + r, _ := http.NewRequest("GET", "http://localhost/some/url", nil) + r.Header.Set("Authorization", "Pomerium JWT") + v := TokenFromHeader(r, "Authorization", "Pomerium") + assert.Equal(t, "JWT", v) + }) + t.Run("bearer type", func(t *testing.T) { + r, _ := http.NewRequest("GET", "http://localhost/some/url", nil) + r.Header.Set("Authorization", "Bearer Pomerium-JWT") + v := TokenFromHeader(r, "Authorization", "Pomerium") + assert.Equal(t, "JWT", v) + }) +}