Browse Source

feat: add gcp workload identity via SA

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 4 years ago
parent
commit
80fac0f697

+ 0 - 1
.golangci.yaml

@@ -63,7 +63,6 @@ linters:
     - gosimple
     - govet
     - ineffassign
-    - interfacer
     - lll
     - misspell
     - nakedret

+ 10 - 1
apis/externalsecrets/v1alpha1/secretstore_gcpsm_types.go

@@ -19,7 +19,10 @@ import (
 )
 
 type GCPSMAuth struct {
-	SecretRef GCPSMAuthSecretRef `json:"secretRef"`
+	// +optional
+	SecretRef *GCPSMAuthSecretRef `json:"secretRef,omitempty"`
+	// +optional
+	WorkloadIdentity *GCPWorkloadIdentity `json:"workloadIdentity,omitempty"`
 }
 
 type GCPSMAuthSecretRef struct {
@@ -28,6 +31,12 @@ type GCPSMAuthSecretRef struct {
 	SecretAccessKey esmeta.SecretKeySelector `json:"secretAccessKeySecretRef,omitempty"`
 }
 
+type GCPWorkloadIdentity struct {
+	ServiceAccountRef esmeta.ServiceAccountSelector `json:"serviceAccountRef"`
+	ClusterLocation   string                        `json:"clusterLocation"`
+	ClusterName       string                        `json:"clusterName"`
+}
+
 // GCPSMProvider Configures a store to sync secrets using the GCP Secret Manager provider.
 type GCPSMProvider struct {
 	// Auth defines the information necessary to authenticate against GCP

+ 27 - 1
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
@@ -601,7 +602,16 @@ func (in *ExternalSecretTemplateMetadata) DeepCopy() *ExternalSecretTemplateMeta
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GCPSMAuth) DeepCopyInto(out *GCPSMAuth) {
 	*out = *in
-	in.SecretRef.DeepCopyInto(&out.SecretRef)
+	if in.SecretRef != nil {
+		in, out := &in.SecretRef, &out.SecretRef
+		*out = new(GCPSMAuthSecretRef)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.WorkloadIdentity != nil {
+		in, out := &in.WorkloadIdentity, &out.WorkloadIdentity
+		*out = new(GCPWorkloadIdentity)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPSMAuth.
@@ -647,6 +657,22 @@ func (in *GCPSMProvider) DeepCopy() *GCPSMProvider {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GCPWorkloadIdentity) DeepCopyInto(out *GCPWorkloadIdentity) {
+	*out = *in
+	in.ServiceAccountRef.DeepCopyInto(&out.ServiceAccountRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPWorkloadIdentity.
+func (in *GCPWorkloadIdentity) DeepCopy() *GCPWorkloadIdentity {
+	if in == nil {
+		return nil
+	}
+	out := new(GCPWorkloadIdentity)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GitlabAuth) DeepCopyInto(out *GitlabAuth) {
 	*out = *in
 	in.SecretRef.DeepCopyInto(&out.SecretRef)

+ 1 - 0
apis/meta/v1/zz_generated.deepcopy.go

@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*

+ 27 - 2
deploy/crds/external-secrets.io_clustersecretstores.yaml

@@ -411,8 +411,33 @@ spec:
                                     type: string
                                 type: object
                             type: object
-                        required:
-                        - secretRef
+                          workloadIdentity:
+                            properties:
+                              clusterLocation:
+                                type: string
+                              clusterName:
+                                type: string
+                              serviceAccountRef:
+                                description: A reference to a ServiceAccount resource.
+                                properties:
+                                  name:
+                                    description: The name of the ServiceAccount resource
+                                      being referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                required:
+                                - name
+                                type: object
+                            required:
+                            - clusterLocation
+                            - clusterName
+                            - serviceAccountRef
+                            type: object
                         type: object
                       projectID:
                         description: ProjectID project where secret is located

+ 27 - 2
deploy/crds/external-secrets.io_secretstores.yaml

@@ -411,8 +411,33 @@ spec:
                                     type: string
                                 type: object
                             type: object
-                        required:
-                        - secretRef
+                          workloadIdentity:
+                            properties:
+                              clusterLocation:
+                                type: string
+                              clusterName:
+                                type: string
+                              serviceAccountRef:
+                                description: A reference to a ServiceAccount resource.
+                                properties:
+                                  name:
+                                    description: The name of the ServiceAccount resource
+                                      being referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                required:
+                                - name
+                                type: object
+                            required:
+                            - clusterLocation
+                            - clusterName
+                            - serviceAccountRef
+                            type: object
                         type: object
                       projectID:
                         description: ProjectID project where secret is located

+ 1 - 1
e2e/suite/gcp/provider.go

@@ -123,7 +123,7 @@ func (s *gcpProvider) BeforeEach() {
 				GCPSM: &esv1alpha1.GCPSMProvider{
 					ProjectID: s.projectID,
 					Auth: esv1alpha1.GCPSMAuth{
-						SecretRef: esv1alpha1.GCPSMAuthSecretRef{
+						SecretRef: &esv1alpha1.GCPSMAuthSecretRef{
 							SecretAccessKey: esmeta.SecretKeySelector{
 								Name: "provider-secret",
 								Key:  "secret-access-credentials",

+ 2 - 1
go.mod

@@ -81,7 +81,8 @@ require (
 	golang.org/x/tools v0.1.7 // indirect
 	google.golang.org/api v0.45.0
 	google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3
-	google.golang.org/grpc v1.37.0
+	google.golang.org/grpc v1.43.0
+	grpc.go4.org v0.0.0-20170609214715-11d0a25b4919
 	honnef.co/go/tools v0.1.4 // indirect
 	k8s.io/api v0.21.3
 	k8s.io/apimachinery v0.21.3

+ 2 - 0
go.sum

@@ -1163,6 +1163,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
 gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY=
+grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 61 - 48
pkg/provider/gcp/secretmanager/secretsmanager.go

@@ -21,10 +21,11 @@ import (
 	secretmanager "cloud.google.com/go/secretmanager/apiv1"
 	"github.com/googleapis/gax-go"
 	"github.com/tidwall/gjson"
+	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
 	"google.golang.org/api/option"
 	secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
-	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -40,11 +41,12 @@ const (
 
 	errGCPSMStore                             = "received invalid GCPSM SecretStore resource"
 	errClientClose                            = "unable to close SecretManager client: %w"
+	errMissingStoreSpec                       = "invalid: missing store spec"
 	errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing GCP SecretAccessKey Namespace"
+	errInvalidClusterStoreMissingSANamespace  = "invalid ClusterSecretStore: missing GCP Service Account Namespace"
 	errFetchSAKSecret                         = "could not fetch SecretAccessKey secret: %w"
 	errMissingSAK                             = "missing SecretAccessKey"
 	errUnableProcessJSONCredentials           = "failed to process the provided JSON credentials: %w"
-	errUnableProcessDefaultCredentials        = "failed to process the default credentials: %w"
 	errUnableCreateGCPSMClient                = "failed to create GCP secretmanager client: %w"
 	errUninitalizedGCPProvider                = "provider GCP is not initialized"
 	errClientGetSecretAccess                  = "unable to access Secret from SecretManager Client: %w"
@@ -63,43 +65,64 @@ type ProviderGCP struct {
 }
 
 type gClient struct {
-	kube        kclient.Client
-	store       *esv1alpha1.GCPSMProvider
-	namespace   string
-	storeKind   string
-	credentials []byte
+	kube             kclient.Client
+	store            *esv1alpha1.GCPSMProvider
+	namespace        string
+	storeKind        string
+	workloadIdentity *workloadIdentity
 }
 
-func (c *gClient) setAuth(ctx context.Context) error {
-	credentialsSecret := &corev1.Secret{}
-	credentialsSecretName := c.store.Auth.SecretRef.SecretAccessKey.Name
+func (c *gClient) getTokenSource(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
+	ts, err := serviceAccountTokenSource(ctx, store, kube, namespace)
+	if ts != nil || err != nil {
+		return ts, err
+	}
+	ts, err = c.workloadIdentity.TokenSource(ctx, store, kube, namespace)
+	if ts != nil || err != nil {
+		return ts, err
+	}
+
+	return google.DefaultTokenSource(ctx, CloudPlatformRole)
+}
+
+func serviceAccountTokenSource(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
+	spec := store.GetSpec()
+	if spec == nil || spec.Provider.GCPSM == nil {
+		return nil, fmt.Errorf(errMissingStoreSpec)
+	}
+	sr := spec.Provider.GCPSM.Auth.SecretRef
+	if sr == nil {
+		return nil, nil
+	}
+	storeKind := store.GetObjectKind().GroupVersionKind().Kind
+	credentialsSecret := &v1.Secret{}
+	credentialsSecretName := sr.SecretAccessKey.Name
 	objectKey := types.NamespacedName{
 		Name:      credentialsSecretName,
-		Namespace: c.namespace,
+		Namespace: namespace,
 	}
 
 	// only ClusterStore is allowed to set namespace (and then it's required)
-	if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
-		if credentialsSecretName != "" && c.store.Auth.SecretRef.SecretAccessKey.Namespace == nil {
-			return fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
+	if storeKind == esv1alpha1.ClusterSecretStoreKind {
+		if credentialsSecretName != "" && sr.SecretAccessKey.Namespace == nil {
+			return nil, fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
 		} else if credentialsSecretName != "" {
-			objectKey.Namespace = *c.store.Auth.SecretRef.SecretAccessKey.Namespace
+			objectKey.Namespace = *sr.SecretAccessKey.Namespace
 		}
 	}
-	if credentialsSecretName == "" {
-		c.credentials = nil
-		return nil
-	}
-	err := c.kube.Get(ctx, objectKey, credentialsSecret)
+	err := kube.Get(ctx, objectKey, credentialsSecret)
 	if err != nil {
-		return fmt.Errorf(errFetchSAKSecret, err)
+		return nil, fmt.Errorf(errFetchSAKSecret, err)
 	}
-
-	c.credentials = credentialsSecret.Data[c.store.Auth.SecretRef.SecretAccessKey.Key]
-	if (c.credentials == nil) || (len(c.credentials) == 0) {
-		return fmt.Errorf(errMissingSAK)
+	credentials := credentialsSecret.Data[sr.SecretAccessKey.Key]
+	if (credentials == nil) || (len(credentials) == 0) {
+		return nil, fmt.Errorf(errMissingSAK)
 	}
-	return nil
+	config, err := google.JWTConfigFromJSON(credentials, CloudPlatformRole)
+	if err != nil {
+		return nil, fmt.Errorf(errUnableProcessJSONCredentials, err)
+	}
+	return config.TokenSource(ctx), nil
 }
 
 // NewClient constructs a GCP Provider.
@@ -110,36 +133,26 @@ func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1alpha1.GenericSt
 	}
 	storeSpecGCPSM := storeSpec.Provider.GCPSM
 
-	cliStore := gClient{
-		kube:      kube,
-		store:     storeSpecGCPSM,
-		namespace: namespace,
-		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+	wi, err := newWorkloadIdentity(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("unable to initialize workload identity")
 	}
 
-	if err := cliStore.setAuth(ctx); err != nil {
-		return nil, err
+	cliStore := gClient{
+		kube:             kube,
+		store:            storeSpecGCPSM,
+		namespace:        namespace,
+		storeKind:        store.GetObjectKind().GroupVersionKind().Kind,
+		workloadIdentity: wi,
 	}
 
 	sm.projectID = cliStore.store.ProjectID
 
-	if cliStore.credentials != nil {
-		config, err := google.JWTConfigFromJSON(cliStore.credentials, CloudPlatformRole)
-		if err != nil {
-			return nil, fmt.Errorf(errUnableProcessJSONCredentials, err)
-		}
-		ts := config.TokenSource(ctx)
-		clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
-		if err != nil {
-			return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
-		}
-		sm.SecretManagerClient = clientGCPSM
-		return sm, nil
-	}
-	ts, err := google.DefaultTokenSource(ctx, CloudPlatformRole)
+	ts, err := cliStore.getTokenSource(ctx, store, kube, namespace)
 	if err != nil {
-		return nil, fmt.Errorf(errUnableProcessDefaultCredentials, err)
+		return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
 	}
+
 	clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
 	if err != nil {
 		return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)

+ 254 - 0
pkg/provider/gcp/secretmanager/secretsmanager_workload_identity.go

@@ -0,0 +1,254 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package secretmanager
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"time"
+
+	iam "cloud.google.com/go/iam/credentials/apiv1"
+	secretmanager "cloud.google.com/go/secretmanager/apiv1"
+	"github.com/googleapis/gax-go"
+	"golang.org/x/oauth2"
+	"google.golang.org/api/option"
+	credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"grpc.go4.org/credentials/oauth"
+	authenticationv1 "k8s.io/api/authentication/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/kubernetes"
+	clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+const (
+	gcpSAAnnotation = "iam.gke.io/gcp-service-account"
+
+	errFetchPodToken  = "unable to fetch pod token: %w"
+	errFetchIBToken   = "unable to fetch identitybindingtoken: %w"
+	errGenAccessToken = "unable to generate gcp access token: %w"
+)
+
+// workloadIdentity holds all clients and generators needed
+// to create a gcp oauth token.
+type workloadIdentity struct {
+	iamClient            IamClient
+	idBindTokenGenerator idBindTokenGenerator
+	saTokenGenerator     saTokenGenerator
+}
+
+// interface to GCP IAM API.
+type IamClient interface {
+	GenerateAccessToken(ctx context.Context, req *credentialspb.GenerateAccessTokenRequest, opts ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
+}
+
+// interface to securetoken/identitybindingtoken API.
+type idBindTokenGenerator interface {
+	Generate(context.Context, *http.Client, string, string, string) (*oauth2.Token, error)
+}
+
+// interface to kubernetes serviceaccount token request API.
+type saTokenGenerator interface {
+	Generate(context.Context, string, string, string) (*authenticationv1.TokenRequest, error)
+}
+
+func newWorkloadIdentity(ctx context.Context) (*workloadIdentity, error) {
+	iamc, err := newIAMClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	satg, err := newSATokenGenerator()
+	if err != nil {
+		return nil, err
+	}
+	return &workloadIdentity{
+		iamClient:            iamc,
+		idBindTokenGenerator: newIDBindTokenGenerator(),
+		saTokenGenerator:     satg,
+	}, nil
+}
+
+func (w *workloadIdentity) TokenSource(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
+	spec := store.GetSpec()
+	if spec == nil || spec.Provider == nil || spec.Provider.GCPSM == nil {
+		return nil, fmt.Errorf(errMissingStoreSpec)
+	}
+	wi := spec.Provider.GCPSM.Auth.WorkloadIdentity
+	if wi == nil {
+		return nil, nil
+	}
+	storeKind := store.GetObjectKind().GroupVersionKind().Kind
+	saKey := types.NamespacedName{
+		Name:      wi.ServiceAccountRef.Name,
+		Namespace: namespace,
+	}
+
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if storeKind == esv1alpha1.ClusterSecretStoreKind {
+		if wi.ServiceAccountRef.Namespace == nil {
+			return nil, fmt.Errorf(errInvalidClusterStoreMissingSANamespace)
+		}
+		saKey.Namespace = *wi.ServiceAccountRef.Namespace
+	}
+
+	sa := &v1.ServiceAccount{}
+	err := kube.Get(ctx, saKey, sa)
+	if err != nil {
+		return nil, err
+	}
+
+	idProvider := fmt.Sprintf("https://container.googleapis.com/v1/projects/%s/locations/%s/clusters/%s",
+		spec.Provider.GCPSM.ProjectID,
+		wi.ClusterLocation,
+		wi.ClusterName)
+	idPool := fmt.Sprintf("%s.svc.id.goog", spec.Provider.GCPSM.ProjectID)
+	gcpSA := sa.Annotations[gcpSAAnnotation]
+
+	resp, err := w.saTokenGenerator.Generate(ctx, idPool, saKey.Name, saKey.Namespace)
+	if err != nil {
+		return nil, fmt.Errorf(errFetchPodToken, err)
+	}
+
+	idBindToken, err := w.idBindTokenGenerator.Generate(ctx, http.DefaultClient, resp.Status.Token, idPool, idProvider)
+	if err != nil {
+		return nil, fmt.Errorf(errFetchIBToken, err)
+	}
+
+	// If no `iam.gke.io/gcp-service-account` annotation is present the
+	// identitybindingtoken will be used directly, allowing bindings on secrets
+	// of the form "serviceAccount:<project>.svc.id.goog[<namespace>/<sa>]".
+	if gcpSA == "" {
+		return oauth2.StaticTokenSource(idBindToken), nil
+	}
+	gcpSAResp, err := w.iamClient.GenerateAccessToken(ctx, &credentialspb.GenerateAccessTokenRequest{
+		Name:  fmt.Sprintf("projects/-/serviceAccounts/%s", gcpSA),
+		Scope: secretmanager.DefaultAuthScopes(),
+	}, gax.WithGRPCOptions(grpc.PerRPCCredentials(oauth.TokenSource{TokenSource: oauth2.StaticTokenSource(idBindToken)})))
+	if err != nil {
+		return nil, fmt.Errorf(errGenAccessToken, err)
+	}
+	return oauth2.StaticTokenSource(&oauth2.Token{
+		AccessToken: gcpSAResp.GetAccessToken(),
+	}), nil
+}
+
+func newIAMClient(ctx context.Context) (IamClient, error) {
+	iamOpts := []option.ClientOption{
+		option.WithUserAgent("external-secrets-operator"),
+		// tell the secretmanager library to not add transport-level ADC since
+		// we need to override on a per call basis
+		option.WithoutAuthentication(),
+		// grpc oauth TokenSource credentials require transport security, so
+		// this must be set explicitly even though TLS is used
+		option.WithGRPCDialOption(grpc.WithTransportCredentials(credentials.NewTLS(nil))),
+		option.WithGRPCConnectionPool(5),
+	}
+	return iam.NewIamCredentialsClient(ctx, iamOpts...)
+}
+
+type k8sSATokenGenerator struct {
+	corev1 clientcorev1.CoreV1Interface
+}
+
+func (g *k8sSATokenGenerator) Generate(ctx context.Context, idPool, name, namespace string) (*authenticationv1.TokenRequest, error) {
+	// Request a serviceaccount token for the pod
+	ttl := int64((15 * time.Minute).Seconds())
+	return g.corev1.
+		ServiceAccounts(namespace).
+		CreateToken(ctx, name,
+			&authenticationv1.TokenRequest{
+				Spec: authenticationv1.TokenRequestSpec{
+					ExpirationSeconds: &ttl,
+					Audiences:         []string{idPool},
+				},
+			},
+			metav1.CreateOptions{},
+		)
+}
+
+func newSATokenGenerator() (saTokenGenerator, error) {
+	cfg, err := ctrlcfg.GetConfig()
+	if err != nil {
+		return nil, err
+	}
+	clientset, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return nil, err
+	}
+	return &k8sSATokenGenerator{
+		corev1: clientset.CoreV1(),
+	}, nil
+}
+
+// Trades the kubernetes token for an identitybindingtoken token.
+type gcpIDBindTokenGenerator struct {
+	targetURL string
+}
+
+func newIDBindTokenGenerator() idBindTokenGenerator {
+	return &gcpIDBindTokenGenerator{
+		targetURL: "https://securetoken.googleapis.com/v1/identitybindingtoken",
+	}
+}
+
+func (g *gcpIDBindTokenGenerator) Generate(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
+	body, err := json.Marshal(map[string]string{
+		"grant_type":           "urn:ietf:params:oauth:grant-type:token-exchange",
+		"subject_token_type":   "urn:ietf:params:oauth:token-type:jwt",
+		"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+		"subject_token":        k8sToken,
+		"audience":             fmt.Sprintf("identitynamespace:%s:%s", idPool, idProvider),
+		"scope":                "https://www.googleapis.com/auth/cloud-platform",
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequestWithContext(ctx, "POST", g.targetURL, bytes.NewBuffer(body))
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("could not get idbindtoken token, status: %v", resp.StatusCode)
+	}
+
+	defer resp.Body.Close()
+	respBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	idBindToken := &oauth2.Token{}
+	if err := json.Unmarshal(respBody, idBindToken); err != nil {
+		return nil, err
+	}
+	return idBindToken, nil
+}

+ 392 - 0
pkg/provider/gcp/secretmanager/secretsmanager_workload_identity_test.go

@@ -0,0 +1,392 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package secretmanager
+
+import (
+	"context"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/googleapis/gax-go"
+	"github.com/stretchr/testify/assert"
+	"golang.org/x/oauth2"
+	credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
+	authv1 "k8s.io/api/authentication/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type workloadIdentityTest struct {
+	name           string
+	expTS          bool
+	expToken       *oauth2.Token
+	expErr         string
+	genAccessToken func(context.Context, *credentialspb.GenerateAccessTokenRequest, ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
+	genIDBindToken func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error)
+	genSAToken     func(c context.Context, s1, s2, s3 string) (*authv1.TokenRequest, error)
+	store          esv1alpha1.GenericStore
+	kubeObjects    []client.Object
+}
+
+func TestWorkloadIdentity(t *testing.T) {
+	clusterSANamespace := "foobar"
+	tbl := []*workloadIdentityTest{
+		composeTestcase(
+			defaultTestCase("missing store spec should result in error"),
+			withErr("invalid: missing store spec"),
+			withStore(&esv1alpha1.SecretStore{}),
+		),
+		composeTestcase(
+			defaultTestCase("should skip when no workload identity is configured: TokenSource and error must be nil"),
+			withStore(&esv1alpha1.SecretStore{
+				Spec: esv1alpha1.SecretStoreSpec{
+					Provider: &esv1alpha1.SecretStoreProvider{
+						GCPSM: &esv1alpha1.GCPSMProvider{},
+					},
+				},
+			}),
+		),
+		composeTestcase(
+			defaultTestCase("return access token from GenerateAccessTokenRequest with SecretStore"),
+			withStore(defaultStore()),
+			expTokenSource(),
+			expectToken(defaultGenAccessToken),
+		),
+		composeTestcase(
+			defaultTestCase("return idBindToken when no annotation is set with SecretStore"),
+			expTokenSource(),
+			expectToken(defaultIDBindToken),
+			withStore(defaultStore()),
+			withK8sResources([]client.Object{
+				&v1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:        "example",
+						Namespace:   "default",
+						Annotations: map[string]string{},
+					},
+				},
+			}),
+		),
+		composeTestcase(
+			defaultTestCase("invalid ClusterSecretStore: missing service account namespace"),
+			expErr("invalid ClusterSecretStore: missing GCP Service Account Namespace"),
+			withStore(
+				composeStore(defaultClusterStore()),
+			),
+			withK8sResources([]client.Object{
+				&v1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:        "example",
+						Namespace:   "default",
+						Annotations: map[string]string{},
+					},
+				},
+			}),
+		),
+		composeTestcase(
+			defaultTestCase("return access token from GenerateAccessTokenRequest with ClusterSecretStore"),
+			expTokenSource(),
+			expectToken(defaultGenAccessToken),
+			withStore(
+				composeStore(defaultClusterStore(), withSANamespace(clusterSANamespace)),
+			),
+			withK8sResources([]client.Object{
+				&v1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "example",
+						Namespace: clusterSANamespace,
+						Annotations: map[string]string{
+							gcpSAAnnotation: "example",
+						},
+					},
+				},
+			}),
+		),
+	}
+
+	for _, row := range tbl {
+		t.Run(row.name, func(t *testing.T) {
+			fakeIam := &fakeIAMClient{generateAccessTokenFunc: row.genAccessToken}
+			fakeIDBGen := &fakeIDBindTokenGen{generateFunc: row.genIDBindToken}
+			fakeSATG := &fakeSATokenGen{GenerateFunc: row.genSAToken}
+			w := &workloadIdentity{
+				iamClient:            fakeIam,
+				idBindTokenGenerator: fakeIDBGen,
+				saTokenGenerator:     fakeSATG,
+			}
+			cb := clientfake.NewClientBuilder()
+			cb.WithObjects(row.kubeObjects...)
+			client := cb.Build()
+			ts, err := w.TokenSource(context.Background(), row.store, client, "default")
+			// assert err
+			if row.expErr == "" {
+				assert.NoError(t, err)
+			} else {
+				assert.Error(t, err, row.expErr)
+			}
+			// assert ts
+			if row.expTS {
+				assert.NotNil(t, ts)
+				if row.expToken != nil {
+					tk, err := ts.Token()
+					assert.NoError(t, err)
+					assert.EqualValues(t, tk, row.expToken)
+				}
+			} else {
+				assert.Nil(t, ts)
+			}
+		})
+	}
+}
+
+func TestSATokenGen(t *testing.T) {
+	corev1 := &fakeK8sV1{}
+	g := &k8sSATokenGenerator{
+		corev1: corev1,
+	}
+	token, err := g.Generate(context.Background(), "my-fake-audience", "bar", "default")
+	assert.Nil(t, err)
+	assert.Equal(t, token.Status.Token, defaultSAToken)
+	assert.Equal(t, token.Spec.Audiences[0], "my-fake-audience")
+}
+
+func TestIDBTokenGen(t *testing.T) {
+	srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		payload := make(map[string]string)
+		rb, err := ioutil.ReadAll(r.Body)
+		assert.Nil(t, err)
+		err = json.Unmarshal(rb, &payload)
+		assert.Nil(t, err)
+		assert.Equal(t, payload["audience"], "identitynamespace:some-idpool:some-id-provider")
+
+		bt, err := json.Marshal(&oauth2.Token{
+			AccessToken: "12345",
+		})
+		assert.Nil(t, err)
+		rw.WriteHeader(http.StatusOK)
+		rw.Write(bt)
+	}))
+	defer srv.Close()
+	gen := &gcpIDBindTokenGenerator{
+		targetURL: srv.URL,
+	}
+	token, err := gen.Generate(context.Background(), http.DefaultClient, "some-token", "some-idpool", "some-id-provider")
+	assert.Nil(t, err)
+	assert.Equal(t, token.AccessToken, "12345")
+}
+
+type testCaseMutator func(tc *workloadIdentityTest)
+
+func composeTestcase(tc *workloadIdentityTest, mutators ...testCaseMutator) *workloadIdentityTest {
+	for _, m := range mutators {
+		m(tc)
+	}
+	return tc
+}
+
+func withErr(err string) testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.expErr = err
+	}
+}
+
+func withStore(store esv1alpha1.GenericStore) testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.store = store
+	}
+}
+
+func expTokenSource() testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.expTS = true
+	}
+}
+
+func expectToken(token string) testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.expToken = &oauth2.Token{
+			AccessToken: token,
+		}
+	}
+}
+
+func expErr(err string) testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.expErr = err
+	}
+}
+
+func withK8sResources(objs []client.Object) testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.kubeObjects = objs
+	}
+}
+
+var (
+	defaultGenAccessToken = "default-gen-access-token"
+	defaultIDBindToken    = "default-id-bind-token"
+	defaultSAToken        = "default-k8s-sa-token"
+)
+
+func defaultTestCase(name string) *workloadIdentityTest {
+	return &workloadIdentityTest{
+		name: name,
+		genAccessToken: func(c context.Context, gatr *credentialspb.GenerateAccessTokenRequest, co ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error) {
+			return &credentialspb.GenerateAccessTokenResponse{
+				AccessToken: defaultGenAccessToken,
+			}, nil
+		},
+		genIDBindToken: func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
+			return &oauth2.Token{
+				AccessToken: defaultIDBindToken,
+			}, nil
+		},
+		genSAToken: func(c context.Context, s1, s2, s3 string) (*authv1.TokenRequest, error) {
+			return &authv1.TokenRequest{
+				Status: authv1.TokenRequestStatus{
+					Token: defaultSAToken,
+				},
+			}, nil
+		},
+		kubeObjects: []client.Object{
+			&v1.ServiceAccount{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "example",
+					Namespace: "default",
+					Annotations: map[string]string{
+						gcpSAAnnotation: "example",
+					},
+				},
+			},
+		},
+	}
+}
+
+func defaultStore() *esv1alpha1.SecretStore {
+	return &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "foobar",
+			Namespace: "default",
+		},
+		Spec: defaultStoreSpec(),
+	}
+}
+
+func defaultClusterStore() *esv1alpha1.ClusterSecretStore {
+	return &esv1alpha1.ClusterSecretStore{
+		TypeMeta: metav1.TypeMeta{
+			Kind: esv1alpha1.ClusterSecretStoreKind,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "foobar",
+		},
+		Spec: defaultStoreSpec(),
+	}
+}
+
+func defaultStoreSpec() esv1alpha1.SecretStoreSpec {
+	return esv1alpha1.SecretStoreSpec{
+		Provider: &esv1alpha1.SecretStoreProvider{
+			GCPSM: &esv1alpha1.GCPSMProvider{
+				Auth: esv1alpha1.GCPSMAuth{
+					WorkloadIdentity: &esv1alpha1.GCPWorkloadIdentity{
+						ServiceAccountRef: esmeta.ServiceAccountSelector{
+							Name: "example",
+						},
+						ClusterLocation: "example",
+						ClusterName:     "foobar",
+					},
+				},
+				ProjectID: "1234",
+			},
+		},
+	}
+}
+
+type storeMutator func(spc esv1alpha1.GenericStore)
+
+func composeStore(store esv1alpha1.GenericStore, mutators ...storeMutator) esv1alpha1.GenericStore {
+	for _, m := range mutators {
+		m(store)
+	}
+	return store
+}
+
+func withSANamespace(namespace string) storeMutator {
+	return func(store esv1alpha1.GenericStore) {
+		spc := store.GetSpec()
+		spc.Provider.GCPSM.Auth.WorkloadIdentity.ServiceAccountRef.Namespace = &namespace
+	}
+}
+
+// fake IDBindToken Generator.
+type fakeIDBindTokenGen struct {
+	generateFunc func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error)
+}
+
+func (g *fakeIDBindTokenGen) Generate(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
+	return g.generateFunc(ctx, client, k8sToken, idPool, idProvider)
+}
+
+// fake IAM Client.
+type fakeIAMClient struct {
+	generateAccessTokenFunc func(context.Context, *credentialspb.GenerateAccessTokenRequest, ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
+}
+
+func (f *fakeIAMClient) GenerateAccessToken(ctx context.Context, req *credentialspb.GenerateAccessTokenRequest, opts ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error) {
+	return f.generateAccessTokenFunc(ctx, req, opts...)
+}
+
+// fake SA Token Generator.
+type fakeSATokenGen struct {
+	GenerateFunc func(context.Context, string, string, string) (*authv1.TokenRequest, error)
+}
+
+func (f *fakeSATokenGen) Generate(ctx context.Context, idPool, namespace, name string) (*authv1.TokenRequest, error) {
+	return f.GenerateFunc(ctx, idPool, namespace, name)
+}
+
+// fake k8s client for creating tokens.
+type fakeK8sV1 struct {
+	k8sv1.CoreV1Interface
+}
+
+func (m *fakeK8sV1) ServiceAccounts(namespace string) k8sv1.ServiceAccountInterface {
+	return &fakeK8sV1SA{v1mock: m}
+}
+
+// Mock the K8s service account client.
+type fakeK8sV1SA struct {
+	k8sv1.ServiceAccountInterface
+	v1mock *fakeK8sV1
+}
+
+func (ma *fakeK8sV1SA) CreateToken(
+	ctx context.Context,
+	serviceAccountName string,
+	tokenRequest *authv1.TokenRequest,
+	opts metav1.CreateOptions,
+) (*authv1.TokenRequest, error) {
+	tokenRequest.Status.Token = defaultSAToken
+	return tokenRequest, nil
+}