Browse Source

feat(gcp): issue-5790: optional GCP service account email for WIF impersonation (#6273)

* updates GCP STS endpoints to use configured UniverseDomain

Signed-off-by: Bharath B <bhb@redhat.com>

* issue-5790: feat(gcp): optional GCP service account email for WIF impersonation

Signed-off-by: Bharath B <bhb@redhat.com>

---------

Signed-off-by: Bharath B <bhb@redhat.com>
Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Bharath B 2 months ago
parent
commit
801018ac6d

+ 12 - 0
apis/externalsecrets/v1/secretstore_gcpsm_types.go

@@ -117,6 +117,18 @@ type GCPWorkloadIdentityFederation struct {
 	// URL is having the expected value.
 	// +kubebuilder:validation:Optional
 	ExternalTokenEndpoint string `json:"externalTokenEndpoint,omitempty"`
+
+	// GCPServiceAccountEmail is the email of the Google Cloud service account to impersonate
+	// after Workload Identity Federation. Use this to grant access through the service account's
+	// IAM bindings (for example roles/secretmanager.secretAccessor). When set, it overrides
+	// service_account_impersonation_url in the external account JSON from credConfig;
+	// when serviceAccountRef is set, it also overrides the "iam.gke.io/gcp-service-account" annotation
+	// on that ServiceAccount.
+	// +kubebuilder:example:="my-gsa@my-project.iam.gserviceaccount.com"
+	// +kubebuilder:validation:Pattern:=^.*@.*\.iam\.gserviceaccount\.com$
+	// +kubebuilder:validation:MinLength:=1
+	// +kubebuilder:validation:Optional
+	GCPServiceAccountEmail string `json:"gcpServiceAccountEmail,omitempty"`
 }
 
 // ConfigMapReference holds the details of a configmap.

+ 12 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -2156,6 +2156,18 @@ spec:
                                   credential_source.url in the provided credConfig. This field is merely to double-check the external token source
                                   URL is having the expected value.
                                 type: string
+                              gcpServiceAccountEmail:
+                                description: |-
+                                  GCPServiceAccountEmail is the email of the Google Cloud service account to impersonate
+                                  after Workload Identity Federation. Use this to grant access through the service account's
+                                  IAM bindings (for example roles/secretmanager.secretAccessor). When set, it overrides
+                                  service_account_impersonation_url in the external account JSON from credConfig;
+                                  when serviceAccountRef is set, it also overrides the "iam.gke.io/gcp-service-account" annotation
+                                  on that ServiceAccount.
+                                example: my-gsa@my-project.iam.gserviceaccount.com
+                                minLength: 1
+                                pattern: ^.*@.*\.iam\.gserviceaccount\.com$
+                                type: string
                               serviceAccountRef:
                                 description: |-
                                   serviceAccountRef is the reference to the kubernetes ServiceAccount to be used for obtaining the tokens,

+ 12 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -2156,6 +2156,18 @@ spec:
                                   credential_source.url in the provided credConfig. This field is merely to double-check the external token source
                                   URL is having the expected value.
                                 type: string
+                              gcpServiceAccountEmail:
+                                description: |-
+                                  GCPServiceAccountEmail is the email of the Google Cloud service account to impersonate
+                                  after Workload Identity Federation. Use this to grant access through the service account's
+                                  IAM bindings (for example roles/secretmanager.secretAccessor). When set, it overrides
+                                  service_account_impersonation_url in the external account JSON from credConfig;
+                                  when serviceAccountRef is set, it also overrides the "iam.gke.io/gcp-service-account" annotation
+                                  on that ServiceAccount.
+                                example: my-gsa@my-project.iam.gserviceaccount.com
+                                minLength: 1
+                                pattern: ^.*@.*\.iam\.gserviceaccount\.com$
+                                type: string
                               serviceAccountRef:
                                 description: |-
                                   serviceAccountRef is the reference to the kubernetes ServiceAccount to be used for obtaining the tokens,

+ 12 - 0
config/crds/bases/generators.external-secrets.io_clustergenerators.yaml

@@ -603,6 +603,18 @@ spec:
                                   credential_source.url in the provided credConfig. This field is merely to double-check the external token source
                                   URL is having the expected value.
                                 type: string
+                              gcpServiceAccountEmail:
+                                description: |-
+                                  GCPServiceAccountEmail is the email of the Google Cloud service account to impersonate
+                                  after Workload Identity Federation. Use this to grant access through the service account's
+                                  IAM bindings (for example roles/secretmanager.secretAccessor). When set, it overrides
+                                  service_account_impersonation_url in the external account JSON from credConfig;
+                                  when serviceAccountRef is set, it also overrides the "iam.gke.io/gcp-service-account" annotation
+                                  on that ServiceAccount.
+                                example: my-gsa@my-project.iam.gserviceaccount.com
+                                minLength: 1
+                                pattern: ^.*@.*\.iam\.gserviceaccount\.com$
+                                type: string
                               serviceAccountRef:
                                 description: |-
                                   serviceAccountRef is the reference to the kubernetes ServiceAccount to be used for obtaining the tokens,

+ 12 - 0
config/crds/bases/generators.external-secrets.io_gcraccesstokens.yaml

@@ -213,6 +213,18 @@ spec:
                           credential_source.url in the provided credConfig. This field is merely to double-check the external token source
                           URL is having the expected value.
                         type: string
+                      gcpServiceAccountEmail:
+                        description: |-
+                          GCPServiceAccountEmail is the email of the Google Cloud service account to impersonate
+                          after Workload Identity Federation. Use this to grant access through the service account's
+                          IAM bindings (for example roles/secretmanager.secretAccessor). When set, it overrides
+                          service_account_impersonation_url in the external account JSON from credConfig;
+                          when serviceAccountRef is set, it also overrides the "iam.gke.io/gcp-service-account" annotation
+                          on that ServiceAccount.
+                        example: my-gsa@my-project.iam.gserviceaccount.com
+                        minLength: 1
+                        pattern: ^.*@.*\.iam\.gserviceaccount\.com$
+                        type: string
                       serviceAccountRef:
                         description: |-
                           serviceAccountRef is the reference to the kubernetes ServiceAccount to be used for obtaining the tokens,

+ 48 - 0
deploy/crds/bundle.yaml

@@ -4256,6 +4256,18 @@ spec:
                                     credential_source.url in the provided credConfig. This field is merely to double-check the external token source
                                     URL is having the expected value.
                                   type: string
+                                gcpServiceAccountEmail:
+                                  description: |-
+                                    GCPServiceAccountEmail is the email of the Google Cloud service account to impersonate
+                                    after Workload Identity Federation. Use this to grant access through the service account's
+                                    IAM bindings (for example roles/secretmanager.secretAccessor). When set, it overrides
+                                    service_account_impersonation_url in the external account JSON from credConfig;
+                                    when serviceAccountRef is set, it also overrides the "iam.gke.io/gcp-service-account" annotation
+                                    on that ServiceAccount.
+                                  example: my-gsa@my-project.iam.gserviceaccount.com
+                                  minLength: 1
+                                  pattern: ^.*@.*\.iam\.gserviceaccount\.com$
+                                  type: string
                                 serviceAccountRef:
                                   description: |-
                                     serviceAccountRef is the reference to the kubernetes ServiceAccount to be used for obtaining the tokens,
@@ -16497,6 +16509,18 @@ spec:
                                     credential_source.url in the provided credConfig. This field is merely to double-check the external token source
                                     URL is having the expected value.
                                   type: string
+                                gcpServiceAccountEmail:
+                                  description: |-
+                                    GCPServiceAccountEmail is the email of the Google Cloud service account to impersonate
+                                    after Workload Identity Federation. Use this to grant access through the service account's
+                                    IAM bindings (for example roles/secretmanager.secretAccessor). When set, it overrides
+                                    service_account_impersonation_url in the external account JSON from credConfig;
+                                    when serviceAccountRef is set, it also overrides the "iam.gke.io/gcp-service-account" annotation
+                                    on that ServiceAccount.
+                                  example: my-gsa@my-project.iam.gserviceaccount.com
+                                  minLength: 1
+                                  pattern: ^.*@.*\.iam\.gserviceaccount\.com$
+                                  type: string
                                 serviceAccountRef:
                                   description: |-
                                     serviceAccountRef is the reference to the kubernetes ServiceAccount to be used for obtaining the tokens,
@@ -25619,6 +25643,18 @@ spec:
                                     credential_source.url in the provided credConfig. This field is merely to double-check the external token source
                                     URL is having the expected value.
                                   type: string
+                                gcpServiceAccountEmail:
+                                  description: |-
+                                    GCPServiceAccountEmail is the email of the Google Cloud service account to impersonate
+                                    after Workload Identity Federation. Use this to grant access through the service account's
+                                    IAM bindings (for example roles/secretmanager.secretAccessor). When set, it overrides
+                                    service_account_impersonation_url in the external account JSON from credConfig;
+                                    when serviceAccountRef is set, it also overrides the "iam.gke.io/gcp-service-account" annotation
+                                    on that ServiceAccount.
+                                  example: my-gsa@my-project.iam.gserviceaccount.com
+                                  minLength: 1
+                                  pattern: ^.*@.*\.iam\.gserviceaccount\.com$
+                                  type: string
                                 serviceAccountRef:
                                   description: |-
                                     serviceAccountRef is the reference to the kubernetes ServiceAccount to be used for obtaining the tokens,
@@ -27772,6 +27808,18 @@ spec:
                             credential_source.url in the provided credConfig. This field is merely to double-check the external token source
                             URL is having the expected value.
                           type: string
+                        gcpServiceAccountEmail:
+                          description: |-
+                            GCPServiceAccountEmail is the email of the Google Cloud service account to impersonate
+                            after Workload Identity Federation. Use this to grant access through the service account's
+                            IAM bindings (for example roles/secretmanager.secretAccessor). When set, it overrides
+                            service_account_impersonation_url in the external account JSON from credConfig;
+                            when serviceAccountRef is set, it also overrides the "iam.gke.io/gcp-service-account" annotation
+                            on that ServiceAccount.
+                          example: my-gsa@my-project.iam.gserviceaccount.com
+                          minLength: 1
+                          pattern: ^.*@.*\.iam\.gserviceaccount\.com$
+                          type: string
                         serviceAccountRef:
                           description: |-
                             serviceAccountRef is the reference to the kubernetes ServiceAccount to be used for obtaining the tokens,

+ 16 - 0
docs/api/spec.md

@@ -5772,6 +5772,22 @@ credential_source.url in the provided credConfig. This field is merely to double
 URL is having the expected value.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>gcpServiceAccountEmail</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>GCPServiceAccountEmail is the email of the Google Cloud service account to impersonate
+after Workload Identity Federation. Use this to grant access through the service account&rsquo;s
+IAM bindings (for example roles/secretmanager.secretAccessor). When set, it overrides
+service_account_impersonation_url in the external account JSON from credConfig;
+when serviceAccountRef is set, it also overrides the &ldquo;iam.gke.io/gcp-service-account&rdquo; annotation
+on that ServiceAccount.</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1.GcpIDTokenAuthCredentials">GcpIDTokenAuthCredentials

+ 6 - 4
providers/v1/gcp/secretmanager/workload_identity.go

@@ -75,11 +75,13 @@ var (
 	// workloadIdentityRequestedTokenType is the requested type for OAuth 2.0 access token.
 	workloadIdentityRequestedTokenType = "urn:ietf:params:oauth:token-type:access_token"
 
-	// workloadIdentityTokenURL is the token service endpoint.
-	workloadIdentityTokenURL = "https://sts.googleapis.com/v1/token"
+	// workloadIdentityTokenURLFormat is the token service endpoint format. When the UniverseDomain is not set
+	// in the GCP credentials config, defaultUniverseDomain will be substituted.
+	workloadIdentityTokenURLFormat = "https://sts.%s/v1/token"
 
-	// workloadIdentityTokenInfoURL is the STS introspection service endpoint.
-	workloadIdentityTokenInfoURL = "https://sts.googleapis.com/v1/introspect"
+	// workloadIdentityTokenInfoURLFormat is the STS introspection service endpoint format. When the UniverseDomain is not set
+	// in the GCP credentials config, defaultUniverseDomain will be substituted.
+	workloadIdentityTokenInfoURLFormat = "https://sts.%s/v1/introspect"
 )
 
 // workloadIdentity holds all clients and generators needed

+ 33 - 17
providers/v1/gcp/secretmanager/workload_identity_federation.go

@@ -106,10 +106,16 @@ type serviceAccountImpersonationInfo struct {
 }
 
 var (
+	gcpSTSTokenURLRegex                 = regexp.MustCompile(`^https://sts\.[^/\s]+/v1/token$`)
+	gcpSTSTokenInfoURLRegex             = regexp.MustCompile(`^https://sts\.[^/\s]+/v1/introspect$`)
 	awsSTSTokenURLRegex                 = regexp.MustCompile(`^http://(metadata\.google\.internal|169\.254\.169\.254|\[fd00:ec2::254\])/latest/meta-data/iam/security-credentials$`)
 	awsRegionURLRegex                   = regexp.MustCompile(`^http://(metadata\.google\.internal|169\.254\.169\.254|\[fd00:ec2::254\])/latest/meta-data/placement/availability-zone$`)
 	awsSessionTokenURLRegex             = regexp.MustCompile(`^http://(metadata\.google\.internal|169\.254\.169\.254|\[fd00:ec2::254\])/latest/api/token$`)
-	serviceAccountImpersonationURLRegex = regexp.MustCompile(`^https://iamcredentials\.googleapis\.com/v1/projects/-/serviceAccounts/(\S+):generateAccessToken$`)
+	serviceAccountImpersonationURLRegex = regexp.MustCompile(
+		`^https://iamcredentials\.(?:[a-z0-9-]+\.)*googleapis\.com` +
+			`/v1/projects/[^/]+/serviceAccounts/` +
+			`[a-z0-9-]+@[a-z0-9-]+\.iam\.gserviceaccount\.com:generateAccessToken$`,
+	)
 )
 
 const (
@@ -172,13 +178,20 @@ func (w *workloadIdentityFederation) TokenSource(ctx context.Context) (oauth2.To
 	return externalaccount.NewTokenSource(ctx, *config)
 }
 
-func (w *workloadIdentityFederation) getGCPServiceAccountFromAnnotation(ctx context.Context, cfg *externalaccount.Config) error {
+// updateServiceAccountImpersonationURL sets cfg.ServiceAccountImpersonationURL for GCP service account
+// impersonation after the external account config is merged. When GCPServiceAccountEmail is set on the
+// spec, it wins over service_account_impersonation_url from credConfig and over the "iam.gke.io/gcp-service-account"
+// annotation on the referenced Kubernetes ServiceAccount. Otherwise, if serviceAccountRef is set, the
+// ServiceAccount is loaded and the gcp-service-account annotation is applied when present.
+func (w *workloadIdentityFederation) updateServiceAccountImpersonationURL(ctx context.Context, cfg *externalaccount.Config) error {
+	if w.config.GCPServiceAccountEmail != "" {
+		cfg.ServiceAccountImpersonationURL = fmt.Sprintf(workloadIdentityFederationServiceAccountImpersonationURLFormat, w.config.GCPServiceAccountEmail)
+		return nil
+	}
+
 	if w.config.ServiceAccountRef == nil {
 		return nil
 	}
-	// look up the service account and check if it has a well-known GCP WI annotation.
-	// If so, use that GCP service account for impersonation.
-	// Required if you grant secret access to a GCP service account instead of direct resource access.
 	ns := w.namespace
 	if w.isClusterKind && w.config.ServiceAccountRef.Namespace != nil {
 		ns = *w.config.ServiceAccountRef.Namespace
@@ -244,7 +257,7 @@ func (w *workloadIdentityFederation) generateExternalAccountConfig(ctx context.C
 	if err := w.updateExternalAccountConfigWithAWSCredentialsSupplier(ctx, config); err != nil {
 		return nil, err
 	}
-	if err := w.getGCPServiceAccountFromAnnotation(ctx, config); err != nil {
+	if err := w.updateServiceAccountImpersonationURL(ctx, config); err != nil {
 		return nil, err
 	}
 	w.updateExternalAccountConfigWithDefaultValues(config)
@@ -294,14 +307,14 @@ func (w *workloadIdentityFederation) updateExternalAccountConfigWithDefaultValue
 	if config.SubjectTokenType == "" {
 		config.SubjectTokenType = workloadIdentitySubjectTokenType
 	}
+	if config.UniverseDomain == "" {
+		config.UniverseDomain = defaultUniverseDomain
+	}
 	if config.TokenURL == "" {
-		config.TokenURL = workloadIdentityTokenURL
+		config.TokenURL = fmt.Sprintf(workloadIdentityTokenURLFormat, config.UniverseDomain)
 	}
 	if config.TokenInfoURL == "" {
-		config.TokenInfoURL = workloadIdentityTokenInfoURL
-	}
-	if config.UniverseDomain == "" {
-		config.UniverseDomain = defaultUniverseDomain
+		config.TokenInfoURL = fmt.Sprintf(workloadIdentityTokenInfoURLFormat, config.UniverseDomain)
 	}
 }
 
@@ -384,10 +397,13 @@ func validateExternalAccountConfig(config *externalaccount.Config, wif *esv1.GCP
 	}
 	if config.ServiceAccountImpersonationURL != "" &&
 		!serviceAccountImpersonationURLRegex.MatchString(config.ServiceAccountImpersonationURL) {
-		errs = append(errs, fmt.Errorf("service_account_impersonation_url \"%s\" does not have expected value", config.ServiceAccountImpersonationURL))
+		errs = append(errs, fmt.Errorf("service_account_impersonation_url \"%s\" must match \"%s\"", config.ServiceAccountImpersonationURL, serviceAccountImpersonationURLRegex.String()))
+	}
+	if !gcpSTSTokenURLRegex.MatchString(config.TokenURL) {
+		errs = append(errs, fmt.Errorf("token_url \"%s\" must match \"%s\"", config.TokenURL, gcpSTSTokenURLRegex.String()))
 	}
-	if config.TokenURL != workloadIdentityTokenURL {
-		errs = append(errs, fmt.Errorf("token_url \"%s\" must match %s", config.TokenURL, workloadIdentityTokenURL))
+	if !gcpSTSTokenInfoURLRegex.MatchString(config.TokenInfoURL) {
+		errs = append(errs, fmt.Errorf("token_info_url \"%s\" must match \"%s\"", config.TokenInfoURL, gcpSTSTokenInfoURLRegex.String()))
 	}
 	if config.CredentialSource != nil {
 		errs = append(errs, validateCredConfigCredentialSource(config.CredentialSource, wif)...)
@@ -423,13 +439,13 @@ func validateCredConfigAWSCredentialSource(credSource *externalaccount.Credentia
 			errs = append(errs, fmt.Errorf("credential_source.environment_id \"%s\" must start with %s", credSource.EnvironmentID, awsEnvironmentIDPrefix))
 		}
 		if !awsSTSTokenURLRegex.MatchString(credSource.URL) {
-			errs = append(errs, fmt.Errorf("credential_source.aws.url \"%s\" does not have expected value", credSource.URL))
+			errs = append(errs, fmt.Errorf("credential_source.aws.url \"%s\" must match \"%s\"", credSource.URL, awsSTSTokenURLRegex.String()))
 		}
 		if !awsRegionURLRegex.MatchString(credSource.RegionURL) {
-			errs = append(errs, fmt.Errorf("credential_source.aws.region_url \"%s\" does not have expected value", credSource.RegionURL))
+			errs = append(errs, fmt.Errorf("credential_source.aws.region_url \"%s\" must match \"%s\"", credSource.RegionURL, awsRegionURLRegex.String()))
 		}
 		if credSource.IMDSv2SessionTokenURL != "" && !awsSessionTokenURLRegex.MatchString(credSource.IMDSv2SessionTokenURL) {
-			errs = append(errs, fmt.Errorf("credential_source.aws.imdsv2_session_token_url \"%s\" does not have expected value", credSource.IMDSv2SessionTokenURL))
+			errs = append(errs, fmt.Errorf("credential_source.aws.imdsv2_session_token_url \"%s\" must match \"%s\"", credSource.IMDSv2SessionTokenURL, awsSessionTokenURLRegex.String()))
 		}
 	}
 	return errs

+ 301 - 23
providers/v1/gcp/secretmanager/workload_identity_federation_test.go

@@ -19,6 +19,7 @@ package secretmanager
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -41,6 +42,7 @@ type workloadIdentityFederationTest struct {
 	genSAToken             func(context.Context, []string, string, string) (*authv1.TokenRequest, error)
 	expectError            string
 	expectTokenSource      bool
+	assertImpersonation    bool
 	expectImpersonationURL string
 }
 
@@ -51,6 +53,8 @@ const (
 	testAudience                       = "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/test-pool/providers/test-provider"
 	testServiceAccountImpersonationURL = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@test.iam.gserviceaccount.com:generateAccessToken"
 	testGCPServiceAccountEmail         = "test@test.iam.gserviceaccount.com"
+	testOverrideGCPServiceAccountEmail = "override@override.iam.gserviceaccount.com"
+	testFromCredGCPServiceAccountEmail = "from-cred@cred.iam.gserviceaccount.com"
 	testSAToken                        = "test-sa-token"
 	testAwsRegion                      = "us-west-2"
 	// below values taken from https://docs.aws.amazon.com/sdkref/latest/guide/feature-static-credentials.html
@@ -70,7 +74,11 @@ const (
 )
 
 var (
-	testNamespace = "external-secrets-tests"
+	testNamespace                = "external-secrets-tests"
+	testTokenURL                 = fmt.Sprintf(workloadIdentityTokenURLFormat, defaultUniverseDomain)
+	testTokenInfoURL             = fmt.Sprintf(workloadIdentityTokenInfoURLFormat, defaultUniverseDomain)
+	testOverrideImpersonationURL = fmt.Sprintf(workloadIdentityFederationServiceAccountImpersonationURLFormat, testOverrideGCPServiceAccountEmail)
+	testFromCredImpersonationURL = fmt.Sprintf(workloadIdentityFederationServiceAccountImpersonationURLFormat, testFromCredGCPServiceAccountEmail)
 )
 
 func createValidK8sExternalAccountConfig(audience string) string {
@@ -78,11 +86,44 @@ func createValidK8sExternalAccountConfig(audience string) string {
 		"type":               externalAccountCredentialType,
 		"audience":           audience,
 		"subject_token_type": workloadIdentitySubjectTokenType,
-		"token_url":          workloadIdentityTokenURL,
+		"token_url":          testTokenURL,
+		"credential_source": map[string]any{
+			"file": "/var/run/secrets/oidc_token",
+		},
+		"token_info_url": testTokenInfoURL,
+	}
+	data, _ := json.Marshal(config)
+	return string(data)
+}
+
+// createValidK8sExternalAccountConfigOmittedSTSEndpoints omits token_url, token_info_url, and universe_domain
+// in the external_account JSON to test URL derivation using default universe domain value.
+func createValidK8sExternalAccountConfigOmittedSTSEndpoints(audience string) string {
+	config := map[string]any{
+		"type":               externalAccountCredentialType,
+		"audience":           audience,
+		"subject_token_type": workloadIdentitySubjectTokenType,
+		"credential_source": map[string]any{
+			"file": "/var/run/secrets/oidc_token",
+		},
+	}
+	data, _ := json.Marshal(config)
+	return string(data)
+}
+
+// createK8sExternalAccountConfigWithImpersonation includes service_account_impersonation_url in the
+// external_account JSON to test precedence against GCPServiceAccountEmail and annotations.
+func createK8sExternalAccountConfigWithImpersonation(audience, serviceAccountImpersonationURL string) string {
+	config := map[string]any{
+		"type":                              externalAccountCredentialType,
+		"audience":                          audience,
+		"subject_token_type":                workloadIdentitySubjectTokenType,
+		"token_url":                         testTokenURL,
+		"token_info_url":                    testTokenInfoURL,
+		"service_account_impersonation_url": serviceAccountImpersonationURL,
 		"credential_source": map[string]any{
 			"file": "/var/run/secrets/oidc_token",
 		},
-		"token_info_url": workloadIdentityTokenInfoURL,
 	}
 	data, _ := json.Marshal(config)
 	return string(data)
@@ -93,7 +134,8 @@ func createValidAWSExternalAccountConfig(audience string) string {
 		"type":                              externalAccountCredentialType,
 		"audience":                          audience,
 		"subject_token_type":                workloadIdentitySubjectTokenType,
-		"token_url":                         workloadIdentityTokenURL,
+		"token_url":                         testTokenURL,
+		"token_info_url":                    testTokenInfoURL,
 		"service_account_impersonation_url": testServiceAccountImpersonationURL,
 		"credential_source": map[string]any{
 			"environment_id":           "aws1",
@@ -120,11 +162,11 @@ func createInvalidK8sExternalAccountConfigWithUnallowedTokenFilePath(audience st
 		"type":               externalAccountCredentialType,
 		"audience":           audience,
 		"subject_token_type": workloadIdentitySubjectTokenType,
-		"token_url":          workloadIdentityTokenURL,
+		"token_url":          testTokenURL,
 		"credential_source": map[string]any{
 			"file": autoMountedServiceAccountTokenPath,
 		},
-		"token_info_url": workloadIdentityTokenInfoURL,
+		"token_info_url": testTokenInfoURL,
 	}
 	data, _ := json.Marshal(config)
 	return string(data)
@@ -139,13 +181,13 @@ func createInvalidK8sExternalAccountConfigWithUnallowedTokenURL(audience string)
 		"credential_source": map[string]any{
 			"file": "/var/run/secrets/oidc_token",
 		},
-		"token_info_url": workloadIdentityTokenInfoURL,
+		"token_info_url": testTokenInfoURL,
 	}
 	data, _ := json.Marshal(config)
 	return string(data)
 }
 
-func defaultSATokenGenerator(ctx context.Context, idPool []string, namespace, name string) (*authv1.TokenRequest, error) {
+func defaultSATokenGenerator(_ context.Context, _ []string, _, _ string) (*authv1.TokenRequest, error) {
 	return &authv1.TokenRequest{
 		Status: authv1.TokenRequestStatus{
 			Token: testSAToken,
@@ -192,6 +234,28 @@ func TestWorkloadIdentityFederation(t *testing.T) {
 			},
 			expectTokenSource: true,
 		},
+		{
+			name: "successful kubernetes cred config with omitted token_url and token_info_url",
+			wifConfig: &esv1.GCPWorkloadIdentityFederation{
+				CredConfig: &esv1.ConfigMapReference{
+					Name:      testConfigMapName,
+					Key:       testConfigMapKey,
+					Namespace: testNamespace,
+				},
+			},
+			kubeObjects: []client.Object{
+				&corev1.ConfigMap{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      testConfigMapName,
+						Namespace: testNamespace,
+					},
+					Data: map[string]string{
+						testConfigMapKey: createValidK8sExternalAccountConfigOmittedSTSEndpoints(testAudience),
+					},
+				},
+			},
+			expectTokenSource: true,
+		},
 		{
 			name: "cred configmap configured non-existent key",
 			wifConfig: &esv1.GCPWorkloadIdentityFederation{
@@ -256,7 +320,7 @@ func TestWorkloadIdentityFederation(t *testing.T) {
 					},
 				},
 			},
-			expectError: "invalid external_account config\ntoken_url \"https://example.com\" must match https://sts.googleapis.com/v1/token",
+			expectError: "invalid external_account config\ntoken_url \"https://example.com\" must match \"^https://sts\\.[^/\\s]+/v1/token$\"",
 		},
 		{
 			name: "successful AWS federation with security credentials",
@@ -423,9 +487,86 @@ func TestWorkloadIdentityFederation(t *testing.T) {
 					},
 				}, nil
 			},
+			assertImpersonation:    true,
 			expectImpersonationURL: testServiceAccountImpersonationURL,
 			expectTokenSource:      true,
 		},
+		{
+			name: "kubernetes service account without gcp annotation has no service account impersonation URL",
+			wifConfig: &esv1.GCPWorkloadIdentityFederation{
+				Audience: testAudience,
+				ServiceAccountRef: &esmeta.ServiceAccountSelector{
+					Name:      testServiceAccount,
+					Namespace: &testNamespace,
+					Audiences: []string{testAudience},
+				},
+			},
+			kubeObjects: []client.Object{
+				&corev1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      testServiceAccount,
+						Namespace: testNamespace,
+					},
+				},
+			},
+			genSAToken:             defaultSATokenGenerator,
+			assertImpersonation:    true,
+			expectImpersonationURL: "",
+			expectTokenSource:      true,
+		},
+		{
+			name: "GCPServiceAccountEmail on spec overrides service_account_impersonation_url in credConfig JSON",
+			wifConfig: &esv1.GCPWorkloadIdentityFederation{
+				Audience:               testAudience,
+				GCPServiceAccountEmail: testOverrideGCPServiceAccountEmail,
+				CredConfig: &esv1.ConfigMapReference{
+					Name:      testConfigMapName,
+					Key:       testConfigMapKey,
+					Namespace: testNamespace,
+				},
+			},
+			kubeObjects: []client.Object{
+				&corev1.ConfigMap{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      testConfigMapName,
+						Namespace: testNamespace,
+					},
+					Data: map[string]string{
+						testConfigMapKey: createK8sExternalAccountConfigWithImpersonation(testAudience, testFromCredImpersonationURL),
+					},
+				},
+			},
+			assertImpersonation:    true,
+			expectImpersonationURL: testOverrideImpersonationURL,
+			expectTokenSource:      true,
+		},
+		{
+			name: "GCPServiceAccountEmail on spec overrides ServiceAccount annotation",
+			wifConfig: &esv1.GCPWorkloadIdentityFederation{
+				Audience:               testAudience,
+				GCPServiceAccountEmail: testOverrideGCPServiceAccountEmail,
+				ServiceAccountRef: &esmeta.ServiceAccountSelector{
+					Name:      testServiceAccount,
+					Namespace: &testNamespace,
+					Audiences: []string{testAudience},
+				},
+			},
+			kubeObjects: []client.Object{
+				&corev1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      testServiceAccount,
+						Namespace: testNamespace,
+						Annotations: map[string]string{
+							gcpSAAnnotation: testGCPServiceAccountEmail,
+						},
+					},
+				},
+			},
+			genSAToken:             defaultSATokenGenerator,
+			assertImpersonation:    true,
+			expectImpersonationURL: testOverrideImpersonationURL,
+			expectTokenSource:      true,
+		},
 		{
 			name: "valid AWS credentials secret",
 			wifConfig: &esv1.GCPWorkloadIdentityFederation{
@@ -587,8 +728,8 @@ func TestWorkloadIdentityFederation(t *testing.T) {
 				namespace:        testNamespace,
 			}
 
-			if tc.expectImpersonationURL != "" {
-				cfg, cfgErr := wif.generateExternalAccountConfig(context.Background(), nil)
+			if tc.assertImpersonation {
+				cfg, cfgErr := wif.readCredConfig(context.Background())
 				assert.NoError(t, cfgErr)
 				assert.Equal(t, tc.expectImpersonationURL, cfg.ServiceAccountImpersonationURL)
 			}
@@ -620,7 +761,44 @@ func TestValidateCredConfig(t *testing.T) {
 			config: &externalaccount.Config{
 				Audience:                       testAudience,
 				SubjectTokenType:               workloadIdentitySubjectTokenType,
-				TokenURL:                       workloadIdentityTokenURL,
+				TokenURL:                       testTokenURL,
+				TokenInfoURL:                   testTokenInfoURL,
+				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
+				CredentialSource: &externalaccount.CredentialSource{
+					File: autoMountedServiceAccountTokenPath,
+				},
+			},
+			wif: &esv1.GCPWorkloadIdentityFederation{
+				CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
+			},
+			expectError: "",
+		},
+		{
+			name: "valid kubernetes provider with empty universe domain",
+			config: &externalaccount.Config{
+				Audience:                       testAudience,
+				SubjectTokenType:               workloadIdentitySubjectTokenType,
+				TokenURL:                       testTokenURL,
+				TokenInfoURL:                   testTokenInfoURL,
+				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
+				CredentialSource: &externalaccount.CredentialSource{
+					File: autoMountedServiceAccountTokenPath,
+				},
+				UniverseDomain: "",
+			},
+			wif: &esv1.GCPWorkloadIdentityFederation{
+				CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
+			},
+			expectError: "",
+		},
+		{
+			name: "valid non-default universe STS URLs",
+			config: &externalaccount.Config{
+				Audience:                       testAudience,
+				SubjectTokenType:               workloadIdentitySubjectTokenType,
+				UniverseDomain:                 "partner.example",
+				TokenURL:                       fmt.Sprintf(workloadIdentityTokenURLFormat, "partner.example"),
+				TokenInfoURL:                   fmt.Sprintf(workloadIdentityTokenInfoURLFormat, "partner.example"),
 				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
 				CredentialSource: &externalaccount.CredentialSource{
 					File: autoMountedServiceAccountTokenPath,
@@ -636,7 +814,8 @@ func TestValidateCredConfig(t *testing.T) {
 			config: &externalaccount.Config{
 				Audience:                       testAudience,
 				SubjectTokenType:               workloadIdentitySubjectTokenType,
-				TokenURL:                       workloadIdentityTokenURL,
+				TokenURL:                       testTokenURL,
+				TokenInfoURL:                   testTokenInfoURL,
 				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
 				CredentialSource: &externalaccount.CredentialSource{
 					EnvironmentID:         "aws1",
@@ -655,7 +834,8 @@ func TestValidateCredConfig(t *testing.T) {
 			config: &externalaccount.Config{
 				Audience:                       testAudience,
 				SubjectTokenType:               workloadIdentitySubjectTokenType,
-				TokenURL:                       workloadIdentityTokenURL,
+				TokenURL:                       testTokenURL,
+				TokenInfoURL:                   testTokenInfoURL,
 				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
 				CredentialSource: &externalaccount.CredentialSource{
 					EnvironmentID:         "aws1",
@@ -673,31 +853,47 @@ func TestValidateCredConfig(t *testing.T) {
 			name: "invalid service account impersonation URL",
 			config: &externalaccount.Config{
 				Audience:                       testAudience,
-				TokenURL:                       workloadIdentityTokenURL,
+				TokenURL:                       testTokenURL,
+				TokenInfoURL:                   testTokenInfoURL,
 				ServiceAccountImpersonationURL: "https://invalid-url.com",
 			},
 			wif: &esv1.GCPWorkloadIdentityFederation{
 				CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
 			},
-			expectError: "invalid external_account config\nservice_account_impersonation_url \"https://invalid-url.com\" does not have expected value",
+			expectError: "invalid external_account config\nservice_account_impersonation_url \"https://invalid-url.com\" must match \"^https://iamcredentials\\.(?:[a-z0-9-]+\\.)*googleapis\\.com/v1/projects/[^/]+/serviceAccounts/[a-z0-9-]+@[a-z0-9-]+\\.iam\\.gserviceaccount\\.com:generateAccessToken$\"",
 		},
 		{
 			name: "invalid token URL",
 			config: &externalaccount.Config{
 				Audience:                       testAudience,
 				TokenURL:                       "https://invalid-token-url.com",
+				TokenInfoURL:                   testTokenInfoURL,
+				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
+			},
+			wif: &esv1.GCPWorkloadIdentityFederation{
+				CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
+			},
+			expectError: "invalid external_account config\ntoken_url \"https://invalid-token-url.com\" must match \"^https://sts\\.[^/\\s]+/v1/token$\"",
+		},
+		{
+			name: "invalid token info URL",
+			config: &externalaccount.Config{
+				Audience:                       testAudience,
+				TokenURL:                       testTokenURL,
+				TokenInfoURL:                   "https://example.com/v1/introspect",
 				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
 			},
 			wif: &esv1.GCPWorkloadIdentityFederation{
 				CredConfig: &esv1.ConfigMapReference{Name: testConfigMapName},
 			},
-			expectError: "invalid external_account config\ntoken_url \"https://invalid-token-url.com\" must match https://sts.googleapis.com/v1/token",
+			expectError: "invalid external_account config\ntoken_info_url \"https://example.com/v1/introspect\" must match \"^https://sts\\.[^/\\s]+/v1/introspect$\"",
 		},
 		{
 			name: "executable is configured",
 			config: &externalaccount.Config{
 				Audience:                       testAudience,
-				TokenURL:                       workloadIdentityTokenURL,
+				TokenURL:                       testTokenURL,
+				TokenInfoURL:                   testTokenInfoURL,
 				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
 				CredentialSource: &externalaccount.CredentialSource{
 					Executable: &externalaccount.ExecutableConfig{
@@ -713,7 +909,8 @@ func TestValidateCredConfig(t *testing.T) {
 		{
 			name: "invalid config - empty audience",
 			config: &externalaccount.Config{
-				TokenURL:                       workloadIdentityTokenURL,
+				TokenURL:                       testTokenURL,
+				TokenInfoURL:                   testTokenInfoURL,
 				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
 				CredentialSource: &externalaccount.CredentialSource{
 					File: "/var/run/secrets/token",
@@ -728,7 +925,8 @@ func TestValidateCredConfig(t *testing.T) {
 			name: "invalid config - invalid URL",
 			config: &externalaccount.Config{
 				Audience:                       testAudience,
-				TokenURL:                       workloadIdentityTokenURL,
+				TokenURL:                       testTokenURL,
+				TokenInfoURL:                   testTokenInfoURL,
 				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
 				CredentialSource: &externalaccount.CredentialSource{
 					URL: "https://example.com",
@@ -744,7 +942,8 @@ func TestValidateCredConfig(t *testing.T) {
 			name: "invalid config - invalid AWS config",
 			config: &externalaccount.Config{
 				Audience:                       testAudience,
-				TokenURL:                       workloadIdentityTokenURL,
+				TokenURL:                       testTokenURL,
+				TokenInfoURL:                   testTokenInfoURL,
 				ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
 				CredentialSource: &externalaccount.CredentialSource{
 					EnvironmentID:         "sample",
@@ -757,7 +956,7 @@ func TestValidateCredConfig(t *testing.T) {
 				CredConfig:            &esv1.ConfigMapReference{Name: testConfigMapName},
 				ExternalTokenEndpoint: "https://mismatch.com",
 			},
-			expectError: "invalid external_account config\ncredential_source.environment_id \"sample\" must start with aws\ncredential_source.aws.url \"https://aws-token.com\" does not have expected value\ncredential_source.aws.region_url \"https://region.com\" does not have expected value\ncredential_source.aws.imdsv2_session_token_url \"https://session-token.com\" does not have expected value",
+			expectError: "invalid external_account config\ncredential_source.environment_id \"sample\" must start with aws\ncredential_source.aws.url \"https://aws-token.com\" must match \"^http://(metadata\\.google\\.internal|169\\.254\\.169\\.254|\\[fd00:ec2::254\\])/latest/meta-data/iam/security-credentials$\"\ncredential_source.aws.region_url \"https://region.com\" must match \"^http://(metadata\\.google\\.internal|169\\.254\\.169\\.254|\\[fd00:ec2::254\\])/latest/meta-data/placement/availability-zone$\"\ncredential_source.aws.imdsv2_session_token_url \"https://session-token.com\" must match \"^http://(metadata\\.google\\.internal|169\\.254\\.169\\.254|\\[fd00:ec2::254\\])/latest/api/token$\"",
 		},
 	}
 
@@ -905,6 +1104,85 @@ func TestReadCredConfig(t *testing.T) {
 	}
 }
 
+// TestUpdateServiceAccountImpersonationURL covers ordering edge cases for impersonation that are not
+// asserted by TestWorkloadIdentityFederation: (1) cred JSON already set a URL and an empty spec does
+// not clear it, (2) annotation replaces a URL from cred, (3) no annotation keeps a URL from cred.
+// Flows also covered by WIF (email vs cred/annotation, annotation-only, missing SA) are tested there only.
+func TestUpdateServiceAccountImpersonationURL(t *testing.T) {
+	annotationURL := testServiceAccountImpersonationURL
+	credImpersonationURL := testFromCredImpersonationURL
+
+	baseSA := &corev1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      testServiceAccount,
+			Namespace: testNamespace,
+		},
+	}
+	saWithGcpAnnotation := baseSA.DeepCopy()
+	saWithGcpAnnotation.Annotations = map[string]string{
+		gcpSAAnnotation: testGCPServiceAccountEmail,
+	}
+
+	tests := []struct {
+		name       string
+		wif        *workloadIdentityFederation
+		initialURL string
+		wantURL    string
+	}{
+		{
+			name: "no gcp service account email and no service account ref leaves initial URL unchanged",
+			wif: &workloadIdentityFederation{
+				kubeClient: clientfake.NewClientBuilder().Build(),
+				config:     &esv1.GCPWorkloadIdentityFederation{},
+			},
+			initialURL: credImpersonationURL,
+			wantURL:    credImpersonationURL,
+		},
+		{
+			name: "kubernetes service account annotation overwrites initial impersonation URL from cred",
+			wif: &workloadIdentityFederation{
+				kubeClient: clientfake.NewClientBuilder().WithObjects(saWithGcpAnnotation).Build(),
+				config: &esv1.GCPWorkloadIdentityFederation{
+					ServiceAccountRef: &esmeta.ServiceAccountSelector{
+						Name:      testServiceAccount,
+						Namespace: &testNamespace,
+					},
+				},
+				namespace:     testNamespace,
+				isClusterKind: true,
+			},
+			initialURL: credImpersonationURL,
+			wantURL:    annotationURL,
+		},
+		{
+			name: "kubernetes service account without gcp annotation leaves initial impersonation URL from cred",
+			wif: &workloadIdentityFederation{
+				kubeClient: clientfake.NewClientBuilder().WithObjects(baseSA).Build(),
+				config: &esv1.GCPWorkloadIdentityFederation{
+					ServiceAccountRef: &esmeta.ServiceAccountSelector{
+						Name:      testServiceAccount,
+						Namespace: &testNamespace,
+					},
+				},
+				namespace:     testNamespace,
+				isClusterKind: true,
+			},
+			initialURL: credImpersonationURL,
+			wantURL:    credImpersonationURL,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			cfg := &externalaccount.Config{ServiceAccountImpersonationURL: tc.initialURL}
+			err := tc.wif.updateServiceAccountImpersonationURL(context.Background(), cfg)
+			assert.NoError(t, err)
+			assert.Equal(t, tc.wantURL, cfg.ServiceAccountImpersonationURL)
+		})
+	}
+}
+
 func TestGenerateExternalAccountConfig(t *testing.T) {
 	wif := &esv1.GCPWorkloadIdentityFederation{
 		CredConfig: &esv1.ConfigMapReference{
@@ -950,7 +1228,7 @@ func TestGenerateExternalAccountConfig(t *testing.T) {
 		Type:                           externalAccountCredentialType,
 		Audience:                       testAudience,
 		SubjectTokenType:               workloadIdentitySubjectTokenType,
-		TokenURLExternal:               workloadIdentityTokenURL,
+		TokenURLExternal:               testTokenURL,
 		ServiceAccountImpersonationURL: testServiceAccountImpersonationURL,
 	}
 

+ 1 - 0
tests/__snapshot__/clustergenerator-v1alpha1.yaml

@@ -87,6 +87,7 @@ spec:
             name: string
             namespace: string
           externalTokenEndpoint: string
+          gcpServiceAccountEmail: "my-gsa@my-project.iam.gserviceaccount.com"
           serviceAccountRef:
             audiences: [] # minItems 0 of type string
             name: string

+ 1 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -327,6 +327,7 @@ spec:
             name: string
             namespace: string
           externalTokenEndpoint: string
+          gcpServiceAccountEmail: "my-gsa@my-project.iam.gserviceaccount.com"
           serviceAccountRef:
             audiences: [] # minItems 0 of type string
             name: string

+ 1 - 0
tests/__snapshot__/gcraccesstoken-v1alpha1.yaml

@@ -28,6 +28,7 @@ spec:
         name: string
         namespace: string
       externalTokenEndpoint: string
+      gcpServiceAccountEmail: "my-gsa@my-project.iam.gserviceaccount.com"
       serviceAccountRef:
         audiences: [] # minItems 0 of type string
         name: string

+ 1 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -327,6 +327,7 @@ spec:
             name: string
             namespace: string
           externalTokenEndpoint: string
+          gcpServiceAccountEmail: "my-gsa@my-project.iam.gserviceaccount.com"
           serviceAccountRef:
             audiences: [] # minItems 0 of type string
             name: string