From a0cae6852325e5163027805dc2193523d07ff786 Mon Sep 17 00:00:00 2001 From: Ben Ash <32777270+benashz@users.noreply.github.com> Date: Thu, 17 Feb 2022 14:56:31 -0500 Subject: [PATCH] Ensure valid entity alias names created for projected volume tokens. (#137) --- go.mod | 2 + go.sum | 2 + path_login.go | 8 +- path_login_test.go | 311 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 320 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2df32a1d..3790bf73 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,14 @@ go 1.12 require ( github.com/briankassouf/jose v0.9.2-0.20180619214549-d2569464773f github.com/go-test/deep v1.0.8 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/hashicorp/errwrap v1.1.0 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-hclog v1.0.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 github.com/hashicorp/go-sockaddr v1.0.2 + github.com/hashicorp/go-uuid v1.0.2 github.com/hashicorp/vault/api v1.2.0 github.com/hashicorp/vault/sdk v0.2.1 github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect diff --git a/go.sum b/go.sum index b00b5f64..971c46f2 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,8 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= diff --git a/path_login.go b/path_login.go index ec8d8a1b..00183a74 100644 --- a/path_login.go +++ b/path_login.go @@ -11,7 +11,7 @@ import ( "github.com/briankassouf/jose/jws" "github.com/briankassouf/jose/jwt" "github.com/hashicorp/errwrap" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/strutil" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/cidrutil" @@ -160,7 +160,11 @@ func (b *kubeAuthBackend) getAliasName(role *roleStorageEntry, serviceAccount *s } return uid, nil case aliasNameSourceSAName: - return fmt.Sprintf("%s/%s", serviceAccount.Namespace, serviceAccount.Name), nil + ns, name := serviceAccount.namespace(), serviceAccount.name() + if ns == "" || name == "" { + return "", fmt.Errorf("service account namespace and name must be set") + } + return fmt.Sprintf("%s/%s", ns, name), nil default: return "", fmt.Errorf("unknown alias_name_source %q", role.AliasNameSource) } diff --git a/path_login_test.go b/path_login_test.go index 5d7ac0b8..fa6f95ef 100644 --- a/path_login_test.go +++ b/path_login_test.go @@ -2,14 +2,26 @@ package kubeauth import ( "context" + "crypto/rand" "crypto/rsa" "errors" "fmt" + "reflect" "testing" + "time" + "github.com/briankassouf/jose/jws" + // TODO: using github.com/golang-jwt/jwt for tests only, + // as a part of moving away from jose we should consider standardizing + // on a single JWT library for tests and runtime uses. + "github.com/golang-jwt/jwt" "github.com/hashicorp/errwrap" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/sdk/logical" + "github.com/mitchellh/mapstructure" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" ) var ( @@ -1088,3 +1100,300 @@ WKDrXOZIZAMPHZtt2MojxdGpPxiBSVODn6hw8n4hGBWuH7UABU+2h2kZI0ctxWaX UIX4hSHyjlKYDGEezrUP1mm7AX5pN1qrjtxasTSPPX8nZY/3HtM77n4PfYEwCrew rwIDAQAB -----END PUBLIC KEY-----` + +func Test_kubeAuthBackend_getAliasName(t *testing.T) { + expectedErr := fmt.Errorf("service account namespace and name must be set") + issuerDefault := "kubernetes/serviceaccount" + issuerProjected := "https://kubernetes.default.svc.cluster.local" + + tests := []struct { + name string + role *roleStorageEntry + signRequest *jwtSignTestRequest + want string + wantErr bool + }{ + { + name: "default", + role: &roleStorageEntry{ + AliasNameSource: aliasNameSourceDefault, + }, + signRequest: &jwtSignTestRequest{ + issuer: issuerDefault, + ns: "default", + sa: "sa", + uid: testUID, + projected: false, + }, + want: testUID, + wantErr: false, + }, + { + name: "default-sa-uid", + role: &roleStorageEntry{ + AliasNameSource: aliasNameSourceSAUid, + }, + signRequest: &jwtSignTestRequest{ + issuer: issuerDefault, + ns: "default", + sa: "sa", + uid: testUID, + projected: false, + }, + want: testUID, + wantErr: false, + }, + { + name: "default-sa-name", + role: &roleStorageEntry{ + AliasNameSource: aliasNameSourceSAName, + }, + signRequest: &jwtSignTestRequest{ + issuer: issuerDefault, + ns: "default", + sa: "sa", + projected: false, + }, + want: fmt.Sprintf("%s/%s", "default", "sa"), + wantErr: false, + }, + { + name: "invalid-default-empty-ns", + role: &roleStorageEntry{ + AliasNameSource: aliasNameSourceSAName, + }, + signRequest: &jwtSignTestRequest{ + issuer: issuerProjected, + ns: "", + sa: "sa2", + projected: false, + }, + want: "", + wantErr: true, + }, + { + name: "invalid-default-empty-sa", + role: &roleStorageEntry{ + AliasNameSource: aliasNameSourceSAName, + }, + signRequest: &jwtSignTestRequest{ + issuer: issuerProjected, + ns: "default", + sa: "", + projected: false, + }, + want: "", + wantErr: true, + }, + { + name: "projected", + role: &roleStorageEntry{ + AliasNameSource: aliasNameSourceDefault, + }, + signRequest: &jwtSignTestRequest{ + issuer: issuerProjected, + ns: "default", + sa: "sa", + uid: testProjectedUID, + projected: true, + }, + want: testProjectedUID, + wantErr: false, + }, + { + name: "projected-sa-uid", + role: &roleStorageEntry{ + AliasNameSource: aliasNameSourceSAUid, + }, + signRequest: &jwtSignTestRequest{ + issuer: issuerProjected, + ns: "default", + sa: "sa", + uid: testProjectedUID, + projected: true, + }, + want: testProjectedUID, + wantErr: false, + }, + { + name: "projected-sa-name", + role: &roleStorageEntry{ + AliasNameSource: aliasNameSourceSAName, + }, + signRequest: &jwtSignTestRequest{ + issuer: issuerProjected, + ns: "ns1", + sa: "sa", + projected: true, + }, + want: fmt.Sprintf("%s/%s", "ns1", "sa"), + wantErr: false, + }, + { + name: "invalid-projected-empty-ns", + role: &roleStorageEntry{ + AliasNameSource: aliasNameSourceSAName, + }, + signRequest: &jwtSignTestRequest{ + issuer: issuerProjected, + ns: "", + sa: "sa2", + projected: true, + }, + want: "", + wantErr: true, + }, + { + name: "invalid-projected-empty-sa", + role: &roleStorageEntry{ + AliasNameSource: aliasNameSourceSAName, + }, + signRequest: &jwtSignTestRequest{ + issuer: issuerProjected, + ns: "default", + sa: "", + projected: true, + }, + want: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := &kubeAuthBackend{} + + s, err := signTestJWTRequest(tt.signRequest) + if err != nil { + t.Fatal(err) + } + + parsedJWT, err := jws.ParseJWT([]byte(s)) + if err != nil { + t.Fatal(err) + } + + sa := &serviceAccount{} + if err := mapstructure.Decode(parsedJWT.Claims(), sa); err != nil { + t.Fatal(err) + } + + got, err := b.getAliasName(tt.role, sa) + + if tt.wantErr { + if err == nil { + t.Errorf("getAliasName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(expectedErr, err) { + t.Errorf("getAliasName() expected error = %v, actual %v", expectedErr, err) + } + } + + if got != tt.want { + t.Errorf("getAliasName() got = %v, want %v", got, tt.want) + } + }) + } +} + +type jwtSignTestRequest struct { + ns string + sa string + uid string + projected bool + issuer string + expired bool +} + +func (r *jwtSignTestRequest) getUID() string { + var uid string + if r.uid == "" { + uid, _ = uuid.GenerateUUID() + r.uid = uid + } + + return r.uid +} + +func signTestJWTRequest(req *jwtSignTestRequest) (string, error) { + var claims jwt.Claims + if req.projected { + claims = projectedJWTTestClaims(req) + } else { + claims = defaultJWTTestClaims(req) + } + + return signTestJWT(claims) +} + +func jwtStandardTestClaims(req *jwtSignTestRequest) jwt.StandardClaims { + now := time.Now() + var horizon int64 = 86400 + if req.expired { + horizon = horizon * -1 + } + return jwt.StandardClaims{ + IssuedAt: now.Unix(), + ExpiresAt: now.Unix() + horizon, + Issuer: req.issuer, + } +} + +func projectedJWTTestClaims(req *jwtSignTestRequest) jwt.Claims { + type testToken struct { + Namespace string `json:"namespace"` + Pod *v1.ObjectMeta `json:"pod"` + ServiceAccount *v1.ObjectMeta `json:"serviceaccount"` + } + + type Claims struct { + Audiences []string `json:"aud"` + Token *testToken `json:"kubernetes.io"` + jwt.StandardClaims + } + + uid := types.UID(req.getUID()) + return &Claims{ + Audiences: []string{"baz"}, + Token: &testToken{ + Namespace: req.ns, + Pod: &v1.ObjectMeta{ + Name: "pod", + UID: uid, + }, + ServiceAccount: &v1.ObjectMeta{ + Name: req.sa, + UID: uid, + }, + }, + StandardClaims: jwtStandardTestClaims(req), + } +} + +func defaultJWTTestClaims(req *jwtSignTestRequest) jwt.Claims { + type Claims struct { + Namespace string `json:"kubernetes.io/serviceaccount/namespace"` + SecretName string `json:"kubernetes.io/serviceaccount/secret.name"` + ServiceAccountName string `json:"kubernetes.io/serviceaccount/service-account.name"` + UID string `json:"kubernetes.io/serviceaccount/service-account.uid"` + Sub string `json:"sub"` + jwt.StandardClaims + } + + return &Claims{ + Namespace: req.ns, + ServiceAccountName: req.sa, + UID: req.getUID(), + StandardClaims: jwtStandardTestClaims(req), + } +} + +func signTestJWT(claims jwt.Claims) (string, error) { + pkey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", err + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + + return token.SignedString(pkey) +}