Repository: fluxcd/kustomize-controller

Issue:
SOPS multi-tenancy
Context:
* running on EKS, using IRSA and pod-identity-webhook
* multiple tenants should be onboarded onto the cluster
* we're enforcing `spec.serviceAccountName` 
* one tenant should not be able to decrypt secrets of other tenants

Problem:
Right now, `kustomize-controller` uses a local set of credentials that are defined by environment variables. These credentials allow the controller to make API calls against AWS. 
I want to onboard multiple tenants and they should not be able to decrypt the secrets of other tenants. However, since they share the same set of credentials they can decrypt secrets of other tenants.

With GPG we have a better tenant isolation due to the fact that `Kind=Kustomization` is namespaced and it stores a local reference to a gpg private key. (i.e. one gpg-key per namespace).

Proposed solutions:

(1) use multiple flux instances
This is meh for obvious reasons. Question: can i have multiple `kustomize-controllers` ? Would you support having sth. similar to `ingressClassName` to shard responsibilities between controllers?

(2) use serviceAccount/TokenRequest API 
Since the Kustomization refers to an `spec.serviceAccountName` we could implement sth. similar to the service account token volume projection API in Kubelet ([docs](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-token-volume-projection)).
We can feed those tokens into the aws sdk to do a `sts:assumeRoleWithWebIdentity` this needs to be implemented in the sops repo, of course.
With that we basicially mimic the behavior of the eks-pod-identity-webhook but have the ability to assume multiple roles within one pod. I'm not sure yet what implications that has to elevate the privileges to use the token request api.

Discussion:
@stefanprodan: @moolen the current implementation assumes that each tenant uses OpenPGP or Age encryption keys as Kubernetes secrets are subject to RBAC and so does Kustomization objects. A tenant has no means of accessing another tenant secrets due to secretRef being a local reference, and we also isolate the keys for each reconciliation inside kustomize-controller container.

For KMS access with IRSA, ideally option 2 should be implemented in SOPS then in Flux, I don't think running one controller instance per tenant is a good solution.  @travisgroth: Before seeing this issue, I opened https://github.com/fluxcd/flux2/issues/1478.  Not sure if that should move to this repo but it seems related to the ask here.  Would something like that support your use case @moolen?

FWIW, I think currently you can run a `kustomize-controller` per namespace by setting `--watch-all-namespaces=false` for multi-tenant use cases.  I'd prefer something with less operational overhead when you're primarily worried about segmenting secrets access.@dquagebeur: We would like to use Azure Keyvault instead of pgp keys. What do you think about this proposal:
Integrate Azure Keyvault SDK in the keyservice to encrypt/decrypt using the azure crendentials stored in a secret from the current namespace (behaviour similar to current pgp implementation).
Is it compatible with how SOPS works with AKV ? (I don't know much about SOPS)@dquagebeur: Hi @stefanprodan , could you give us your feedback ? does it worth to invest in this direction ?@dquagebeur: I have made an implementation, could you give me your feedback and if it could be merged : [commit](https://github.com/dquagebeur/kustomize-controller/commit/6750532e1eabded2bd412bc1bfe9e91190e716f8)
Rules:
- if a secretRef contains a secret named *.json, the json is written to tmp folder (like .asc for pgp)
- if a secret is encrypted with an azureKeyvaultKey, I'm checking if an azure_kv.json exist, if yes I use Azure SDK and the json as credentials to decrypt, if no I let the default decrypt workflow...
 @stefanprodan: Hey @dquagebeur I see some issues your implementation, for example using `os.Setenv` which affects all tenants, if we can get rid of it then this looks good to me. Please open a PR so others can comment.

Solution patch:
diff --git a/api/v1/kustomization_types.go b/api/v1/kustomization_types.go
index 97302cf..b4b9bca 100644
--- a/api/v1/kustomization_types.go
+++ b/api/v1/kustomization_types.go
@@ -205,7 +205,18 @@ type Decryption struct {
 	// +required
 	Provider string `json:"provider"`
 
+	// ServiceAccountName is the name of the service account used to
+	// authenticate with KMS services from cloud providers. If a
+	// static credential for a given cloud provider is defined
+	// inside the Secret referenced by SecretRef, that static
+	// credential takes priority.
+	// +optional
+	ServiceAccountName string `json:"serviceAccountName,omitempty"`
+
 	// The secret name containing the private OpenPGP keys used for decryption.
+	// A static credential for a cloud provider defined inside the Secret
+	// takes priority to secret-less authentication with the ServiceAccountName
+	// field.
 	// +optional
 	SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
 }
diff --git a/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml b/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml
index 1140b60..9526ee4 100644
--- a/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml
+++ b/config/crd/bases/kustomize.toolkit.fluxcd.io_kustomizations.yaml
@@ -86,8 +86,11 @@ spec:
                     - sops
                     type: string
                   secretRef:
-                    description: The secret name containing the private OpenPGP keys
-                      used for decryption.
+                    description: |-
+                      The secret name containing the private OpenPGP keys used for decryption.
+                      A static credential for a cloud provider defined inside the Secret
+                      takes priority to secret-less authentication with the ServiceAccountName
+                      field.
                     properties:
                       name:
                         description: Name of the referent.
@@ -95,6 +98,14 @@ spec:
                     required:
                     - name
                     type: object
+                  serviceAccountName:
+                    description: |-
+                      ServiceAccountName is the name of the service account used to
+                      authenticate with KMS services from cloud providers. If a
+                      static credential for a given cloud provider is defined
+                      inside the Secret referenced by SecretRef, that static
+                      credential takes priority.
+                    type: string
                 required:
                 - provider
                 type: object
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 52f2cb3..2faa307 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -21,6 +21,12 @@ rules:
   verbs:
   - create
   - patch
+- apiGroups:
+  - ""
+  resources:
+  - serviceaccounts/token
+  verbs:
+  - create
 - apiGroups:
   - kustomize.toolkit.fluxcd.io
   resources:
diff --git a/docs/api/v1/kustomize.md b/docs/api/v1/kustomize.md
index 9539564..325ba38 100644
--- a/docs/api/v1/kustomize.md
+++ b/docs/api/v1/kustomize.md
@@ -574,6 +574,22 @@ string
 </tr>
 <tr>
 <td>
+<code>serviceAccountName</code><br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>ServiceAccountName is the name of the service account used to
+authenticate with KMS services from cloud providers. If a
+static credential for a given cloud provider is defined
+inside the Secret referenced by SecretRef, that static
+credential takes priority.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>secretRef</code><br>
 <em>
 <a href="https://godoc.org/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
@@ -583,7 +599,10 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
 </td>
 <td>
 <em>(Optional)</em>
-<p>The secret name containing the private OpenPGP keys used for decryption.</p>
+<p>The secret name containing the private OpenPGP keys used for decryption.
+A static credential for a cloud provider defined inside the Secret
+takes priority to secret-less authentication with the ServiceAccountName
+field.</p>
 </td>
 </tr>
 </tbody>
diff --git a/docs/spec/v1/kustomizations.md b/docs/spec/v1/kustomizations.md
index 893052f..2c594e5 100644
--- a/docs/spec/v1/kustomizations.md
+++ b/docs/spec/v1/kustomizations.md
@@ -823,33 +823,46 @@ For more information, see [remote clusters/Cluster-API](#remote-clusterscluster-
 
 ### Decryption
 
-`.spec.decryption` is an optional field to specify the configuration to decrypt
-Secrets, ConfigMaps and patches that are a part of the Kustomization.
+Storing Secrets in Git repositories in plain text or base64 is unsafe,
+regardless of the visibility or access restrictions of the repository.
 
-Since Secrets are either plain text or `base64` encoded, it's unsafe to store
-them in plain text in a public or private Git repository. In order to store
-them safely, you can use [Mozilla SOPS](https://github.com/mozilla/sops) and
-encrypt your Kubernetes Secret data with [age](https://age-encryption.org/v1/)
-and/or [OpenPGP](https://www.openpgp.org) keys, or with provider implementations
-like Azure Key Vault, GCP KMS or Hashicorp Vault.
+In order to store Secrets safely in Git repositorioes you can use an
+encryption provider and the optional field `.spec.decryption` to
+configure decryption for Secrets that are a part of the Kustomization.
 
-Also, you may want to encrypt some parts of resources as well. In order to do that,
-you may encrypt patches as well.
+The only supported encryption provider is [SOPS](https://getsops.io/).
+With SOPS you can encrypt your secrets with [age](https://github.com/FiloSottile/age)
+or [OpenPGP](https://www.openpgp.org) keys, or with keys from Key Management Services
+(KMS), like AWS KMS, Azure Key Vault, GCP KMS or Hashicorp Vault.
 
 **Note:** You must leave `metadata`, `kind` or `apiVersion` in plain text.
-An easy way to do this is to limit encrypted keys by appending `--encrypted-regex '^(data|stringData)$'`
-to your `sops --encrypt` command.
+An easy way to do this is limiting the encrypted keys with the flag
+`--encrypted-regex '^(data|stringData)$'` in your `sops encrypt` command.
 
-It has two fields:
+The `.spec.decryption` field has the following subfields:
 
 - `.provider`: The secrets decryption provider to be used. This field is required and
   the only supported value is `sops`.
-- `.secretRef.name`: The name of the secret that contains the keys to be used for
-  decryption. This field can be omitted when using the
-  [global decryption](#controller-global-decryption) option.
+- `.secretRef.name`: The name of the secret that contains the keys or cloud provider
+  static credentials for KMS services to be used for decryption.
+- `.serviceAccountName`: The name of the service account used for
+  secret-less authentication with KMS services from cloud providers.
+  See the [workload identity](/flux/installation/configuration/workload-identity/) docs
+  for how to configure a cloud provider identity for this service account.
+
+If a static credential for a given cloud provider is defined inside the secret
+referenced by `.secretRef`, that static credential takes priority over secret-less
+authentication for that provider. If no static credentials are defined for a given
+cloud provider inside the secret, secret-less authentication is attempted for that
+provider.
+
+If `.serviceAccountName` is specified for secret-less authentication,
+it takes priority over [controller global decryption](#controller-global-decryption)
+for all cloud providers.
+
+Example:
 
 ```yaml
----
 apiVersion: kustomize.toolkit.fluxcd.io/v1
 kind: Kustomization
 metadata:
@@ -863,13 +876,11 @@ spec:
     name: repository-with-secrets
   decryption:
     provider: sops
+    serviceAccountName: sops-identity
     secretRef:
-      name: sops-keys
+      name: sops-keys-and-credentials
 ```
 
-**Note:** For information on Secrets decryption at a controller level, please
-refer to [controller global decryption](#controller-global-decryption).
-
 The Secret's `.data` section is expected to contain entries with decryption
 keys (for age and OpenPGP), or credentials (for any of the supported provider
 implementations). The controller identifies the type of the entry by the suffix
@@ -880,7 +891,7 @@ of the key (e.g. `.agekey`), or a fixed key (e.g. `sops.vault-token`).
 apiVersion: v1
 kind: Secret
 metadata:
-  name: sops-keys
+  name: sops-keys-and-credentials
   namespace: default
 data:
   # Exemplary age private key
@@ -937,9 +948,9 @@ metadata:
   namespace: default
 data:
   sops.aws-kms: |
-        aws_access_key_id: some-access-key-id
-        aws_secret_access_key: some-aws-secret-access-key
-        aws_session_token: some-aws-session-token # this field is optional
+    aws_access_key_id: some-access-key-id
+    aws_secret_access_key: some-aws-secret-access-key
+    aws_session_token: some-aws-session-token # this field is optional
 ```
 
 #### Azure Key Vault Secret entry
@@ -1408,6 +1419,8 @@ it is possible to specify global decryption settings on the
 kustomize-controller Pod. When the controller fails to find credentials on the
 Kustomization object itself, it will fall back to these defaults.
 
+See also the [workload identity](/flux/installation/configuration/workload-identity/) docs.
+
 #### AWS KMS
 
 While making use of the [IAM OIDC provider](https://eksctl.io/usage/iamserviceaccounts/)
diff --git a/go.mod b/go.mod
index 269f5f7..284ece7 100644
--- a/go.mod
+++ b/go.mod
@@ -9,10 +9,12 @@ replace github.com/fluxcd/kustomize-controller/api => ./api
 replace github.com/opencontainers/go-digest => github.com/opencontainers/go-digest v1.0.1-0.20220411205349-bde1400a84be
 
 require (
+	cloud.google.com/go/kms v1.21.2
 	filippo.io/age v1.2.1
 	github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0
+	github.com/aws/aws-sdk-go-v2 v1.36.3
 	github.com/aws/aws-sdk-go-v2/credentials v1.17.67
 	github.com/cyphar/filepath-securejoin v0.4.1
 	github.com/dimchansky/utfbom v1.1.1
@@ -22,6 +24,8 @@ require (
 	github.com/fluxcd/pkg/apis/event v0.17.0
 	github.com/fluxcd/pkg/apis/kustomize v1.10.0
 	github.com/fluxcd/pkg/apis/meta v1.11.0
+	github.com/fluxcd/pkg/auth v0.12.0
+	github.com/fluxcd/pkg/cache v0.9.0
 	github.com/fluxcd/pkg/http/fetch v0.16.0
 	github.com/fluxcd/pkg/kustomize v1.17.0
 	github.com/fluxcd/pkg/runtime v0.59.0
@@ -36,6 +40,7 @@ require (
 	github.com/ory/dockertest/v3 v3.12.0
 	github.com/spf13/pflag v1.0.6
 	golang.org/x/net v0.39.0
+	golang.org/x/oauth2 v0.29.0
 	k8s.io/api v0.33.0
 	k8s.io/apimachinery v0.33.0
 	k8s.io/client-go v0.33.0
@@ -61,7 +66,6 @@ require (
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 	cloud.google.com/go/compute/metadata v0.6.0 // indirect
 	cloud.google.com/go/iam v1.5.2 // indirect
-	cloud.google.com/go/kms v1.21.2 // indirect
 	cloud.google.com/go/longrunning v0.6.7 // indirect
 	cloud.google.com/go/monitoring v1.24.2 // indirect
 	cloud.google.com/go/storage v1.51.0 // indirect
@@ -80,7 +84,6 @@ require (
 	github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
 	github.com/ProtonMail/go-crypto v1.2.0 // indirect
 	github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
-	github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
 	github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
@@ -89,6 +92,7 @@ require (
 	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
@@ -112,6 +116,7 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/docker/cli v28.1.1+incompatible // indirect
 	github.com/docker/docker v28.1.1+incompatible // indirect
+	github.com/docker/docker-credential-helpers v0.8.2 // indirect
 	github.com/docker/go-connections v0.5.0 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
 	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
@@ -144,6 +149,7 @@ require (
 	github.com/google/cel-go v0.23.2 // indirect
 	github.com/google/gnostic-models v0.6.9 // indirect
 	github.com/google/go-cmp v0.7.0 // indirect
+	github.com/google/go-containerregistry v0.20.3 // indirect
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
 	github.com/google/uuid v1.6.0 // indirect
@@ -222,7 +228,6 @@ require (
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/crypto v0.37.0 // indirect
 	golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
-	golang.org/x/oauth2 v0.29.0 // indirect
 	golang.org/x/sync v0.13.0 // indirect
 	golang.org/x/sys v0.32.0 // indirect
 	golang.org/x/term v0.31.0 // indirect
diff --git a/go.sum b/go.sum
index 74a1382..49f56da 100644
--- a/go.sum
+++ b/go.sum
@@ -91,6 +91,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
 github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3 h1:YyH8Hk73bYzdbvf6S8NF5z/fb/1stpiMnFSfL6jSfRA=
+github.com/aws/aws-sdk-go-v2/service/ecr v1.43.3/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
 github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
@@ -129,6 +131,8 @@ github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73l
 github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
 github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
 github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
+github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
+github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
 github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -148,6 +152,8 @@ github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5M
 github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I=
 github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
+github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
 github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
 github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
 github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -182,6 +188,10 @@ github.com/fluxcd/pkg/apis/kustomize v1.10.0 h1:47EeSzkQvlQZdH92vHMe2lK2iR8aOSEJ
 github.com/fluxcd/pkg/apis/kustomize v1.10.0/go.mod h1:UsqMV4sqNa1Yg0pmTsdkHRJr7bafBOENIJoAN+3ezaQ=
 github.com/fluxcd/pkg/apis/meta v1.11.0 h1:h8q95k6ZEK1HCfsLkt8Np3i6ktb6ZzcWJ6hg++oc9w0=
 github.com/fluxcd/pkg/apis/meta v1.11.0/go.mod h1:+son1Va60x2eiDcTwd7lcctbI6C+K3gM7R+ULmEq1SI=
+github.com/fluxcd/pkg/auth v0.12.0 h1:35o0ziYMLZVgJwNvJBGsv/wd903B2fMagcrnm1ptUjc=
+github.com/fluxcd/pkg/auth v0.12.0/go.mod h1:gQD2VT5OhIR1E8ZTEsTaho3bDQZidr9P10smH/awcew=
+github.com/fluxcd/pkg/cache v0.9.0 h1:EGKfOLMG3fOwWnH/4Axl5xd425mxoQbZzlZoLfd8PDk=
+github.com/fluxcd/pkg/cache v0.9.0/go.mod h1:jMwabjWfsC5lW8hE7NM3wtGNwSJ38Javx6EKbEi7INU=
 github.com/fluxcd/pkg/envsubst v1.4.0 h1:pYsb6wrmXOSfHXuXQHaaBBMt3LumhgCb8SMdBNAwV/U=
 github.com/fluxcd/pkg/envsubst v1.4.0/go.mod h1:zSDFO3Wawi+vI2NPxsMQp+EkIsz/85MNg/s1Wzmqt+s=
 github.com/fluxcd/pkg/http/fetch v0.16.0 h1:XzhBTSK5HNdAPEnEGMJHwtoN2LfqQ9QFDsu3DGzl908=
@@ -254,6 +264,8 @@ github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcb
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
+github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
diff --git a/internal/cache/operations.go b/internal/cache/operations.go
new file mode 100644
index 0000000..cabfbf8
--- /dev/null
+++ b/internal/cache/operations.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2025 The Flux authors
+
+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 intcache
+
+const (
+	OperationDecryptWithAWS   = "decrypt_with_aws"
+	OperationDecryptWithAzure = "decrypt_with_azure"
+	OperationDecryptWithGCP   = "decrypt_with_gcp"
+)
+
+var AllOperations = []string{
+	OperationDecryptWithAWS,
+	OperationDecryptWithAzure,
+	OperationDecryptWithGCP,
+}
diff --git a/internal/controller/kustomization_controller.go b/internal/controller/kustomization_controller.go
index d1f33bf..7f905d0 100644
--- a/internal/controller/kustomization_controller.go
+++ b/internal/controller/kustomization_controller.go
@@ -27,8 +27,6 @@ import (
 	"time"
 
 	securejoin "github.com/cyphar/filepath-securejoin"
-	"github.com/fluxcd/pkg/ssa/normalize"
-	ssautil "github.com/fluxcd/pkg/ssa/utils"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	apimeta "k8s.io/apimachinery/pkg/api/meta"
@@ -54,6 +52,7 @@ import (
 	apiacl "github.com/fluxcd/pkg/apis/acl"
 	eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
 	"github.com/fluxcd/pkg/apis/meta"
+	"github.com/fluxcd/pkg/cache"
 	"github.com/fluxcd/pkg/http/fetch"
 	generator "github.com/fluxcd/pkg/kustomize"
 	"github.com/fluxcd/pkg/runtime/acl"
@@ -66,11 +65,14 @@ import (
 	"github.com/fluxcd/pkg/runtime/predicates"
 	"github.com/fluxcd/pkg/runtime/statusreaders"
 	"github.com/fluxcd/pkg/ssa"
+	"github.com/fluxcd/pkg/ssa/normalize"
+	ssautil "github.com/fluxcd/pkg/ssa/utils"
 	"github.com/fluxcd/pkg/tar"
 	sourcev1 "github.com/fluxcd/source-controller/api/v1"
 	sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
 
 	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
+	intcache "github.com/fluxcd/kustomize-controller/internal/cache"
 	"github.com/fluxcd/kustomize-controller/internal/decryptor"
 	"github.com/fluxcd/kustomize-controller/internal/inventory"
 )
@@ -81,6 +83,7 @@ import (
 // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets;ocirepositories;gitrepositories,verbs=get;list;watch
 // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets/status;ocirepositories/status;gitrepositories/status,verbs=get
 // +kubebuilder:rbac:groups="",resources=configmaps;secrets;serviceaccounts,verbs=get;list;watch
+// +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
 // +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
 
 // KustomizationReconciler reconciles a Kustomization object
@@ -106,6 +109,7 @@ type KustomizationReconciler struct {
 	DisallowedFieldManagers []string
 	StrictSubstitutions     bool
 	GroupChangeLog          bool
+	TokenCache              *cache.TokenCache
 }
 
 // KustomizationReconcilerOptions contains options for the KustomizationReconciler.
@@ -626,17 +630,20 @@ func (r *KustomizationReconciler) generate(obj unstructured.Unstructured,
 func (r *KustomizationReconciler) build(ctx context.Context,
 	obj *kustomizev1.Kustomization, u unstructured.Unstructured,
 	workDir, dirPath string) ([]byte, error) {
-	dec, cleanup, err := decryptor.NewTempDecryptor(workDir, r.Client, obj)
+	dec, cleanup, err := decryptor.NewTempDecryptor(workDir, r.Client, obj, r.TokenCache)
 	if err != nil {
 		return nil, err
 	}
 	defer cleanup()
 
-	// Import decryption keys
+	// Import keys and static credentials for decryption.
 	if err := dec.ImportKeys(ctx); err != nil {
 		return nil, err
 	}
 
+	// Set options for secret-less authentication with cloud providers for decryption.
+	dec.SetAuthOptions(ctx)
+
 	// Decrypt Kustomize EnvSources files before build
 	if err = dec.DecryptSources(dirPath); err != nil {
 		return nil, fmt.Errorf("error decrypting sources: %w", err)
@@ -1090,6 +1097,12 @@ func (r *KustomizationReconciler) finalize(ctx context.Context,
 
 	// Remove our finalizer from the list and update it
 	controllerutil.RemoveFinalizer(obj, kustomizev1.KustomizationFinalizer)
+
+	// Cleanup caches.
+	for _, op := range intcache.AllOperations {
+		r.TokenCache.DeleteEventsForObject(kustomizev1.KustomizationKind, obj.GetName(), obj.GetNamespace(), op)
+	}
+
 	// Stop reconciliation as the object is being deleted
 	return ctrl.Result{}, nil
 }
diff --git a/internal/decryptor/decryptor.go b/internal/decryptor/decryptor.go
index 491445a..970a9d5 100644
--- a/internal/decryptor/decryptor.go
+++ b/internal/decryptor/decryptor.go
@@ -29,17 +29,25 @@ import (
 	"sync"
 	"time"
 
+	gcpkmsapi "cloud.google.com/go/kms/apiv1"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
+	awssdk "github.com/aws/aws-sdk-go-v2/aws"
 	securejoin "github.com/cyphar/filepath-securejoin"
+	"github.com/fluxcd/pkg/auth"
+	"github.com/fluxcd/pkg/auth/aws"
+	"github.com/fluxcd/pkg/auth/azure"
+	"github.com/fluxcd/pkg/auth/gcp"
+	"github.com/fluxcd/pkg/cache"
 	"github.com/getsops/sops/v3"
 	"github.com/getsops/sops/v3/aes"
 	"github.com/getsops/sops/v3/age"
-	"github.com/getsops/sops/v3/azkv"
 	"github.com/getsops/sops/v3/cmd/sops/common"
 	"github.com/getsops/sops/v3/cmd/sops/formats"
 	"github.com/getsops/sops/v3/config"
 	"github.com/getsops/sops/v3/keyservice"
-	awskms "github.com/getsops/sops/v3/kms"
 	"github.com/getsops/sops/v3/pgp"
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/google"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -51,6 +59,7 @@ import (
 	"sigs.k8s.io/yaml"
 
 	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
+	intcache "github.com/fluxcd/kustomize-controller/internal/cache"
 	intawskms "github.com/fluxcd/kustomize-controller/internal/sops/awskms"
 	intazkv "github.com/fluxcd/kustomize-controller/internal/sops/azkv"
 	intkeyservice "github.com/fluxcd/kustomize-controller/internal/sops/keyservice"
@@ -127,6 +136,8 @@ type Decryptor struct {
 	// injected into most resources, causing the integrity check to fail.
 	// Mostly kept around for feature completeness and documentation purposes.
 	checkSopsMac bool
+	// tokenCache is the cache for token credentials.
+	tokenCache *cache.TokenCache
 
 	// gnuPGHome is the absolute path of the GnuPG home directory used to
 	// decrypt PGP data. When empty, the systems' GnuPG keyring is used.
@@ -137,15 +148,15 @@ type Decryptor struct {
 	// vaultToken is the Hashicorp Vault token used to authenticate towards
 	// any Vault server.
 	vaultToken string
-	// awsCredsProvider is the AWS credentials provider object used to authenticate
+	// awsCredentialsProvider is the AWS credentials provider object used to authenticate
 	// towards any AWS KMS.
-	awsCredsProvider *awskms.CredentialsProvider
-	// azureToken is the Azure credential token used to authenticate towards
+	awsCredentialsProvider func(region string) awssdk.CredentialsProvider
+	// azureTokenCredential is the Azure credential token used to authenticate towards
 	// any Azure Key Vault.
-	azureToken *azkv.TokenCredential
-	// gcpCredsJSON is the JSON credential file of the service account used to
-	// authenticate towards any GCP KMS.
-	gcpCredsJSON []byte
+	azureTokenCredential azcore.TokenCredential
+	// gcpTokenSource is the GCP token source used to authenticate towards
+	// any GCP KMS.
+	gcpTokenSource oauth2.TokenSource
 
 	// keyServices are the SOPS keyservice.KeyServiceClient's available to the
 	// decryptor.
@@ -155,25 +166,28 @@ type Decryptor struct {
 
 // NewDecryptor creates a new Decryptor for the given kustomization.
 // gnuPGHome can be empty, in which case the systems' keyring is used.
-func NewDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization, maxFileSize int64, gnuPGHome string) *Decryptor {
+func NewDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization,
+	maxFileSize int64, gnuPGHome string, tokenCache *cache.TokenCache) *Decryptor {
 	return &Decryptor{
 		root:          root,
 		client:        client,
 		kustomization: kustomization,
 		maxFileSize:   maxFileSize,
 		gnuPGHome:     pgp.GnuPGHome(gnuPGHome),
+		tokenCache:    tokenCache,
 	}
 }
 
 // NewTempDecryptor creates a new Decryptor, with a temporary GnuPG
 // home directory to Decryptor.ImportKeys() into.
-func NewTempDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization) (*Decryptor, func(), error) {
+func NewTempDecryptor(root string, client client.Client, kustomization *kustomizev1.Kustomization,
+	tokenCache *cache.TokenCache) (*Decryptor, func(), error) {
 	gnuPGHome, err := pgp.NewGnuPGHome()
 	if err != nil {
 		return nil, nil, fmt.Errorf("cannot create decryptor: %w", err)
 	}
 	cleanup := func() { _ = os.RemoveAll(gnuPGHome.String()) }
-	return NewDecryptor(root, client, kustomization, maxEncryptedFileSize, gnuPGHome.String()), cleanup, nil
+	return NewDecryptor(root, client, kustomization, maxEncryptedFileSize, gnuPGHome.String(), tokenCache), cleanup, nil
 }
 
 // IsEncryptedSecret checks if the given object is a Kubernetes Secret encrypted
@@ -228,7 +242,6 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error {
 					return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
 				}
 			case filepath.Ext(DecryptionVaultTokenFileName):
-				// Make sure we have the absolute name
 				if name == DecryptionVaultTokenFileName {
 					token := string(value)
 					token = strings.Trim(strings.TrimSpace(token), "\n")
@@ -240,10 +253,9 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error {
 					if err != nil {
 						return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
 					}
-					d.awsCredsProvider = awskms.NewCredentialsProvider(awsCreds)
+					d.awsCredentialsProvider = func(string) awssdk.CredentialsProvider { return awsCreds }
 				}
 			case filepath.Ext(DecryptionAzureAuthFile):
-				// Make sure we have the absolute name
 				if name == DecryptionAzureAuthFile {
 					conf := intazkv.AADConfig{}
 					if err = intazkv.LoadAADConfigFromBytes(value, &conf); err != nil {
@@ -253,11 +265,16 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error {
 					if err != nil {
 						return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
 					}
-					d.azureToken = azkv.NewTokenCredential(azureToken)
+					d.azureTokenCredential = azureToken
 				}
 			case filepath.Ext(DecryptionGCPCredsFile):
 				if name == DecryptionGCPCredsFile {
-					d.gcpCredsJSON = bytes.Trim(value, "\n")
+					creds, err := google.CredentialsFromJSON(ctx,
+						bytes.Trim(value, "\n"), gcpkmsapi.DefaultAuthScopes()...)
+					if err != nil {
+						return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err)
+					}
+					d.gcpTokenSource = creds.TokenSource
 				}
 			}
 		}
@@ -265,6 +282,63 @@ func (d *Decryptor) ImportKeys(ctx context.Context) error {
 	return nil
 }
 
+// SetAuthOptions sets the authentication options for secret-less authentication
+// with cloud providers.
+func (d *Decryptor) SetAuthOptions(ctx context.Context) {
+	if d.kustomization.Spec.Decryption == nil {
+		return
+	}
+
+	switch d.kustomization.Spec.Decryption.Provider {
+	case DecryptionProviderSOPS:
+		var opts []auth.Option
+
+		if d.kustomization.Spec.Decryption.ServiceAccountName != "" {
+			serviceAccount := types.NamespacedName{
+				Name:      d.kustomization.Spec.Decryption.ServiceAccountName,
+				Namespace: d.kustomization.GetNamespace(),
+			}
+			opts = append(opts, auth.WithServiceAccount(serviceAccount, d.client))
+		}
+
+		involvedObject := cache.InvolvedObject{
+			Kind:      kustomizev1.KustomizationKind,
+			Name:      d.kustomization.GetName(),
+			Namespace: d.kustomization.GetNamespace(),
+		}
+
+		if d.awsCredentialsProvider == nil {
+			awsOpts := opts
+			if d.tokenCache != nil {
+				involvedObject.Operation = intcache.OperationDecryptWithAWS
+				awsOpts = append(awsOpts, auth.WithCache(*d.tokenCache, involvedObject))
+			}
+			d.awsCredentialsProvider = func(region string) awssdk.CredentialsProvider {
+				awsOpts := append(awsOpts, auth.WithSTSRegion(region))
+				return aws.NewCredentialsProvider(ctx, awsOpts...)
+			}
+		}
+
+		if d.azureTokenCredential == nil {
+			azureOpts := opts
+			if d.tokenCache != nil {
+				involvedObject.Operation = intcache.OperationDecryptWithAzure
+				azureOpts = append(azureOpts, auth.WithCache(*d.tokenCache, involvedObject))
+			}
+			d.azureTokenCredential = azure.NewTokenCredential(ctx, azureOpts...)
+		}
+
+		if d.gcpTokenSource == nil {
+			gcpOpts := opts
+			if d.tokenCache != nil {
+				involvedObject.Operation = intcache.OperationDecryptWithGCP
+				gcpOpts = append(gcpOpts, auth.WithCache(*d.tokenCache, involvedObject))
+			}
+			d.gcpTokenSource = gcp.NewTokenSource(ctx, gcpOpts...)
+		}
+	}
+}
+
 // SopsDecryptWithFormat attempts to load a SOPS encrypted file using the store
 // for the input format, gathers the data key for it from the key service,
 // and then decrypts the file data with the retrieved data key.
@@ -582,12 +656,10 @@ func (d *Decryptor) loadKeyServiceServer() {
 		intkeyservice.WithGnuPGHome(d.gnuPGHome),
 		intkeyservice.WithVaultToken(d.vaultToken),
 		intkeyservice.WithAgeIdentities(d.ageIdentities),
-		intkeyservice.WithGCPCredsJSON(d.gcpCredsJSON),
-	}
-	if d.azureToken != nil {
-		serverOpts = append(serverOpts, intkeyservice.WithAzureToken{Token: d.azureToken})
+		intkeyservice.WithAWSCredentialsProvider{CredentialsProvider: d.awsCredentialsProvider},
+		intkeyservice.WithAzureTokenCredential{TokenCredential: d.azureTokenCredential},
+		intkeyservice.WithGCPTokenSource{TokenSource: d.gcpTokenSource},
 	}
-	serverOpts = append(serverOpts, intkeyservice.WithAWSKeys{CredsProvider: d.awsCredsProvider})
 	server := intkeyservice.NewServer(serverOpts...)
 	d.keyServices = append(make([]keyservice.KeyServiceClient, 0), keyservice.NewCustomLocalClient(server))
 }
diff --git a/internal/sops/awskms/region.go b/internal/sops/awskms/region.go
new file mode 100644
index 0000000..686b39b
--- /dev/null
+++ b/internal/sops/awskms/region.go
@@ -0,0 +1,27 @@
+/*
+Copyright 2025 The Flux authors
+
+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 awskms
+
+import (
+	"strings"
+)
+
+// GetRegionFromKMSARN extracts the region from a KMS ARN.
+func GetRegionFromKMSARN(arn string) string {
+	arn = strings.TrimPrefix(arn, "arn:aws:kms:")
+	return strings.SplitN(arn, ":", 2)[0]
+}
diff --git a/internal/sops/azkv/credentials.go b/internal/sops/azkv/credentials.go
deleted file mode 100644
index 5db2af1..0000000
--- a/internal/sops/azkv/credentials.go
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
-Copyright 2023 The Flux authors
-
-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 azkv
-
-import (
-	"errors"
-	"fmt"
-	"os"
-	"strings"
-
-	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
-	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
-)
-
-// DefaultTokenCredential is a modification of azidentity.NewDefaultAzureCredential,
-// specifically adapted to not shell out to the Azure CLI.
-//
-// It attempts to return an azcore.TokenCredential based on the following order:
-//
-//   - azidentity.NewEnvironmentCredential if environment variables AZURE_CLIENT_ID,
-//     AZURE_CLIENT_ID is set with either one of the following: (AZURE_CLIENT_SECRET)
-//     or (AZURE_CLIENT_CERTIFICATE_PATH and AZURE_CLIENT_CERTIFICATE_PATH) or
-//     (AZURE_USERNAME, AZURE_PASSWORD)
-//   - azidentity.WorkloadIdentityCredential if environment variable configuration
-//     (AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID)
-//     is set by the Azure workload identity webhook.
-//   - azidentity.ManagedIdentityCredential if only AZURE_CLIENT_ID env variable is set.
-func DefaultTokenCredential() (azcore.TokenCredential, error) {
-	var (
-		azureClientID           = "AZURE_CLIENT_ID"
-		azureFederatedTokenFile = "AZURE_FEDERATED_TOKEN_FILE"
-		azureAuthorityHost      = "AZURE_AUTHORITY_HOST"
-		azureTenantID           = "AZURE_TENANT_ID"
-	)
-
-	var errorMessages []string
-	options := &azidentity.DefaultAzureCredentialOptions{}
-
-	envCred, err := azidentity.NewEnvironmentCredential(&azidentity.EnvironmentCredentialOptions{
-		ClientOptions: options.ClientOptions, DisableInstanceDiscovery: options.DisableInstanceDiscovery},
-	)
-	if err == nil {
-		return envCred, nil
-	} else {
-		errorMessages = append(errorMessages, "EnvironmentCredential: "+err.Error())
-	}
-
-	// workload identity requires values for AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID
-	haveWorkloadConfig := false
-	clientID, haveClientID := os.LookupEnv(azureClientID)
-	if haveClientID {
-		if file, ok := os.LookupEnv(azureFederatedTokenFile); ok {
-			if _, ok := os.LookupEnv(azureAuthorityHost); ok {
-				if tenantID, ok := os.LookupEnv(azureTenantID); ok {
-					haveWorkloadConfig = true
-					workloadCred, err := azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{
-						ClientID:                 clientID,
-						TenantID:                 tenantID,
-						TokenFilePath:            file,
-						ClientOptions:            options.ClientOptions,
-						DisableInstanceDiscovery: options.DisableInstanceDiscovery,
-					})
-					if err == nil {
-						return workloadCred, nil
-					} else {
-						errorMessages = append(errorMessages, "Workload Identity"+": "+err.Error())
-					}
-				}
-			}
-		}
-	}
-	if !haveWorkloadConfig {
-		err := errors.New("missing environment variables for workload identity. Check webhook and pod configuration")
-		errorMessages = append(errorMessages, fmt.Sprintf("Workload Identity: %s", err))
-	}
-
-	o := &azidentity.ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions}
-	if haveClientID {
-		o.ID = azidentity.ClientID(clientID)
-	}
-	miCred, err := azidentity.NewManagedIdentityCredential(o)
-	if err == nil {
-		return miCred, nil
-	} else {
-		errorMessages = append(errorMessages, "ManagedIdentity"+": "+err.Error())
-	}
-
-	return nil, errors.New(strings.Join(errorMessages, "\n"))
-}
diff --git a/internal/sops/keyservice/options.go b/internal/sops/keyservice/options.go
index 99ac411..91221cb 100644
--- a/internal/sops/keyservice/options.go
+++ b/internal/sops/keyservice/options.go
@@ -18,6 +18,8 @@ package keyservice
 
 import (
 	extage "filippo.io/age"
+	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
+	awssdk "github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/getsops/sops/v3/age"
 	"github.com/getsops/sops/v3/azkv"
 	"github.com/getsops/sops/v3/gcpkms"
@@ -25,6 +27,9 @@ import (
 	"github.com/getsops/sops/v3/keyservice"
 	awskms "github.com/getsops/sops/v3/kms"
 	"github.com/getsops/sops/v3/pgp"
+	"golang.org/x/oauth2"
+
+	intawskms "github.com/fluxcd/kustomize-controller/internal/sops/awskms"
 )
 
 // ServerOption is some configuration that modifies the Server.
@@ -57,33 +62,38 @@ func (o WithAgeIdentities) ApplyToServer(s *Server) {
 	s.ageIdentities = age.ParsedIdentities(o)
 }
 
-// WithAWSKeys configures the AWS credentials on the Server
-type WithAWSKeys struct {
-	CredsProvider *awskms.CredentialsProvider
+// WithAWSCredentialsProvider configures the AWS credentials on the Server
+type WithAWSCredentialsProvider struct {
+	CredentialsProvider func(region string) awssdk.CredentialsProvider
 }
 
 // ApplyToServer applies this configuration to the given Server.
-func (o WithAWSKeys) ApplyToServer(s *Server) {
-	s.awsCredsProvider = o.CredsProvider
+func (o WithAWSCredentialsProvider) ApplyToServer(s *Server) {
+	s.awsCredentialsProvider = func(arn string) *awskms.CredentialsProvider {
+		region := intawskms.GetRegionFromKMSARN(arn)
+		cp := o.CredentialsProvider(region)
+		return awskms.NewCredentialsProvider(cp)
+	}
 }
 
-// WithGCPCredsJSON configures the GCP service account credentials JSON on the
-// Server.
-type WithGCPCredsJSON []byte
+// WithGCPTokenSource configures the GCP token source on the Server.
+type WithGCPTokenSource struct {
+	TokenSource oauth2.TokenSource
+}
 
 // ApplyToServer applies this configuration to the given Server.
-func (o WithGCPCredsJSON) ApplyToServer(s *Server) {
-	s.gcpCredsJSON = gcpkms.CredentialJSON(o)
+func (o WithGCPTokenSource) ApplyToServer(s *Server) {
+	s.gcpTokenSource = gcpkms.NewTokenSource(o.TokenSource)
 }
 
-// WithAzureToken configures the Azure credential token on the Server.
-type WithAzureToken struct {
-	Token *azkv.TokenCredential
+// WithAzureTokenCredential configures the Azure credential token on the Server.
+type WithAzureTokenCredential struct {
+	TokenCredential azcore.TokenCredential
 }
 
 // ApplyToServer applies this configuration to the given Server.
-func (o WithAzureToken) ApplyToServer(s *Server) {
-	s.azureToken = o.Token
+func (o WithAzureTokenCredential) ApplyToServer(s *Server) {
+	s.azureTokenCredential = azkv.NewTokenCredential(o.TokenCredential)
 }
 
 // WithDefaultServer configures the fallback default server on the Server.
diff --git a/internal/sops/keyservice/server.go b/internal/sops/keyservice/server.go
index 8911bfc..5e4e5ac 100644
--- a/internal/sops/keyservice/server.go
+++ b/internal/sops/keyservice/server.go
@@ -28,8 +28,6 @@ import (
 	"github.com/getsops/sops/v3/logging"
 	"github.com/getsops/sops/v3/pgp"
 	"golang.org/x/net/context"
-
-	intazkv "github.com/fluxcd/kustomize-controller/internal/sops/azkv"
 )
 
 // Server is a key service server that uses SOPS MasterKeys to fulfill
@@ -54,20 +52,19 @@ type Server struct {
 	// When empty, the request will be handled by defaultServer.
 	vaultToken hcvault.Token
 
-	// azureToken is the credential token used for Encrypt and Decrypt
+	// azureTokenCredential is the credential token used for Encrypt and Decrypt
 	// operations of Azure Key Vault requests.
 	// When nil, the request will be handled by defaultServer.
-	azureToken *azkv.TokenCredential
+	azureTokenCredential *azkv.TokenCredential
 
-	// awsCredsProvider is the Credentials object used for Encrypt and Decrypt
+	// awsCredentialsProvider is the Credentials object used for Encrypt and Decrypt
 	// operations of AWS KMS requests.
 	// When nil, the request will be handled by defaultServer.
-	awsCredsProvider *awskms.CredentialsProvider
+	awsCredentialsProvider func(arn string) *awskms.CredentialsProvider
 
-	// gcpCredsJSON is the JSON credentials used for Decrypt and Encrypt
-	// operations of GCP KMS requests. When nil, a default client with
-	// environmental runtime settings will be used.
-	gcpCredsJSON gcpkms.CredentialJSON
+	// gcpTokenSource is the token source used for Encrypt and Decrypt
+	// operations of GCP KMS requests.
+	gcpTokenSource gcpkms.TokenSource
 
 	// defaultServer is the fallback server, used to handle any request that
 	// is not eligible to be handled by this Server.
@@ -296,9 +293,7 @@ func (ks *Server) decryptWithHCVault(key *keyservice.VaultKey, ciphertext []byte
 
 func (ks *Server) encryptWithAWSKMS(key *keyservice.KmsKey, plaintext []byte) ([]byte, error) {
 	awsKey := kmsKeyToMasterKey(key)
-	if ks.awsCredsProvider != nil {
-		ks.awsCredsProvider.ApplyToMasterKey(&awsKey)
-	}
+	ks.awsCredentialsProvider(key.Arn).ApplyToMasterKey(&awsKey)
 	if err := awsKey.Encrypt(plaintext); err != nil {
 		return nil, err
 	}
@@ -308,9 +303,7 @@ func (ks *Server) encryptWithAWSKMS(key *keyservice.KmsKey, plaintext []byte) ([
 func (ks *Server) decryptWithAWSKMS(key *keyservice.KmsKey, cipherText []byte) ([]byte, error) {
 	awsKey := kmsKeyToMasterKey(key)
 	awsKey.EncryptedKey = string(cipherText)
-	if ks.awsCredsProvider != nil {
-		ks.awsCredsProvider.ApplyToMasterKey(&awsKey)
-	}
+	ks.awsCredentialsProvider(key.Arn).ApplyToMasterKey(&awsKey)
 	return awsKey.Decrypt()
 }
 
@@ -320,17 +313,7 @@ func (ks *Server) encryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, pla
 		Name:     key.Name,
 		Version:  key.Version,
 	}
-	if ks.azureToken == nil {
-		// Ensure we use the default token credential if none is provided
-		// _without_ shelling out to `az`.
-		defaultToken, err := intazkv.DefaultTokenCredential()
-		if err != nil {
-			return nil, fmt.Errorf("failed to get Azure token credential to encrypt data: %w", err)
-		}
-		azkv.NewTokenCredential(defaultToken).ApplyToMasterKey(&azureKey)
-	} else {
-		ks.azureToken.ApplyToMasterKey(&azureKey)
-	}
+	ks.azureTokenCredential.ApplyToMasterKey(&azureKey)
 	if err := azureKey.Encrypt(plaintext); err != nil {
 		return nil, err
 	}
@@ -343,17 +326,7 @@ func (ks *Server) decryptWithAzureKeyVault(key *keyservice.AzureKeyVaultKey, cip
 		Name:     key.Name,
 		Version:  key.Version,
 	}
-	if ks.azureToken == nil {
-		// Ensure we use the default token credential if none is provided
-		// _without_ shelling out to `az`.
-		defaultToken, err := intazkv.DefaultTokenCredential()
-		if err != nil {
-			return nil, fmt.Errorf("failed to get Azure token credential to decrypt data: %w", err)
-		}
-		azkv.NewTokenCredential(defaultToken).ApplyToMasterKey(&azureKey)
-	} else {
-		ks.azureToken.ApplyToMasterKey(&azureKey)
-	}
+	ks.azureTokenCredential.ApplyToMasterKey(&azureKey)
 	azureKey.EncryptedKey = string(ciphertext)
 	plaintext, err := azureKey.Decrypt()
 	return plaintext, err
@@ -363,7 +336,7 @@ func (ks *Server) encryptWithGCPKMS(key *keyservice.GcpKmsKey, plaintext []byte)
 	gcpKey := gcpkms.MasterKey{
 		ResourceID: key.ResourceId,
 	}
-	ks.gcpCredsJSON.ApplyToMasterKey(&gcpKey)
+	ks.gcpTokenSource.ApplyToMasterKey(&gcpKey)
 	if err := gcpKey.Encrypt(plaintext); err != nil {
 		return nil, err
 	}
@@ -374,7 +347,7 @@ func (ks *Server) decryptWithGCPKMS(key *keyservice.GcpKmsKey, ciphertext []byte
 	gcpKey := gcpkms.MasterKey{
 		ResourceID: key.ResourceId,
 	}
-	ks.gcpCredsJSON.ApplyToMasterKey(&gcpKey)
+	ks.gcpTokenSource.ApplyToMasterKey(&gcpKey)
 	gcpKey.EncryptedKey = string(ciphertext)
 	plaintext, err := gcpKey.Decrypt()
 	return plaintext, err
diff --git a/main.go b/main.go
index c0f5651..dadb7ef 100644
--- a/main.go
+++ b/main.go
@@ -32,10 +32,12 @@ import (
 	ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
 	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
 	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/config"
+	ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics"
 	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
 
 	"github.com/fluxcd/cli-utils/pkg/kstatus/polling/clusterreader"
 	"github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine"
+	pkgcache "github.com/fluxcd/pkg/cache"
 	"github.com/fluxcd/pkg/runtime/acl"
 	runtimeClient "github.com/fluxcd/pkg/runtime/client"
 	runtimeCtrl "github.com/fluxcd/pkg/runtime/controller"
@@ -73,6 +75,10 @@ func init() {
 }
 
 func main() {
+	const (
+		tokenCacheDefaultMaxSize = 100
+	)
+
 	var (
 		metricsAddr             string
 		eventsAddr              string
@@ -93,6 +99,7 @@ func main() {
 		defaultServiceAccount   string
 		featureGates            feathelper.FeatureGates
 		disallowedFieldManagers []string
+		tokenCacheOptions       pkgcache.TokenFlags
 	)
 
 	flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
@@ -116,6 +123,7 @@ func main() {
 	featureGates.BindFlags(flag.CommandLine)
 	watchOptions.BindFlags(flag.CommandLine)
 	intervalJitterOptions.BindFlags(flag.CommandLine)
+	tokenCacheOptions.BindFlags(flag.CommandLine, tokenCacheDefaultMaxSize)
 
 	flag.Parse()
 
@@ -240,6 +248,19 @@ func main() {
 		os.Exit(1)
 	}
 
+	var tokenCache *pkgcache.TokenCache
+	if tokenCacheOptions.MaxSize > 0 {
+		var err error
+		tokenCache, err = pkgcache.NewTokenCache(tokenCacheOptions.MaxSize,
+			pkgcache.WithMaxDuration(tokenCacheOptions.MaxDuration),
+			pkgcache.WithMetricsRegisterer(ctrlmetrics.Registry),
+			pkgcache.WithMetricsPrefix("gotk_token_"))
+		if err != nil {
+			setupLog.Error(err, "unable to create token cache")
+			os.Exit(1)
+		}
+	}
+
 	if err = (&controller.KustomizationReconciler{
 		ControllerName:          controllerName,
 		DefaultServiceAccount:   defaultServiceAccount,
@@ -257,6 +278,7 @@ func main() {
 		DisallowedFieldManagers: disallowedFieldManagers,
 		StrictSubstitutions:     strictSubstitutions,
 		GroupChangeLog:          groupChangeLog,
+		TokenCache:              tokenCache,
 	}).SetupWithManager(ctx, mgr, controller.KustomizationReconcilerOptions{
 		DependencyRequeueInterval: requeueDependency,
 		HTTPRetry:                 httpRetry,



Test patch:
diff --git a/internal/decryptor/decryptor_test.go b/internal/decryptor/decryptor_test.go
index ac1dd42..fde695d 100644
--- a/internal/decryptor/decryptor_test.go
+++ b/internal/decryptor/decryptor_test.go
@@ -210,7 +210,7 @@ aws_session_token: test-token`),
 				},
 			},
 			inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
-				g.Expect(decryptor.awsCredsProvider).ToNot(BeNil())
+				g.Expect(decryptor.awsCredentialsProvider).ToNot(BeNil())
 			},
 		},
 		{
@@ -233,7 +233,7 @@ aws_session_token: test-token`),
 				},
 			},
 			inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
-				g.Expect(decryptor.gcpCredsJSON).ToNot(BeNil())
+				g.Expect(decryptor.gcpTokenSource).ToNot(BeNil())
 			},
 		},
 		{
@@ -256,7 +256,7 @@ clientSecret: some-client-secret`),
 				},
 			},
 			inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
-				g.Expect(decryptor.azureToken).ToNot(BeNil())
+				g.Expect(decryptor.azureTokenCredential).ToNot(BeNil())
 			},
 		},
 		{
@@ -278,7 +278,7 @@ clientSecret: some-client-secret`),
 			},
 			wantErr: true,
 			inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
-				g.Expect(decryptor.azureToken).To(BeNil())
+				g.Expect(decryptor.azureTokenCredential).To(BeNil())
 			},
 		},
 		{
@@ -300,7 +300,7 @@ clientSecret: some-client-secret`),
 			},
 			wantErr: true,
 			inspectFunc: func(g *GomegaWithT, decryptor *Decryptor) {
-				g.Expect(decryptor.azureToken).To(BeNil())
+				g.Expect(decryptor.azureTokenCredential).To(BeNil())
 			},
 		},
 		{
@@ -376,7 +376,7 @@ clientSecret: some-client-secret`),
 				},
 			}
 
-			d, cleanup, err := NewTempDecryptor("", cb.Build(), &kustomization)
+			d, cleanup, err := NewTempDecryptor("", cb.Build(), &kustomization, nil)
 			g.Expect(err).ToNot(HaveOccurred())
 			t.Cleanup(cleanup)
 
@@ -393,6 +393,60 @@ clientSecret: some-client-secret`),
 	}
 }
 
+func TestDecryptor_SetAuthOptions(t *testing.T) {
+	t.Run("nil decryption settings", func(t *testing.T) {
+		g := NewWithT(t)
+
+		d := &Decryptor{
+			kustomization: &kustomizev1.Kustomization{},
+		}
+
+		d.SetAuthOptions(context.Background())
+
+		g.Expect(d.awsCredentialsProvider).To(BeNil())
+		g.Expect(d.azureTokenCredential).To(BeNil())
+		g.Expect(d.gcpTokenSource).To(BeNil())
+	})
+
+	t.Run("non-sops provider", func(t *testing.T) {
+		g := NewWithT(t)
+
+		d := &Decryptor{
+			kustomization: &kustomizev1.Kustomization{
+				Spec: kustomizev1.KustomizationSpec{
+					Decryption: &kustomizev1.Decryption{},
+				},
+			},
+		}
+
+		d.SetAuthOptions(context.Background())
+
+		g.Expect(d.awsCredentialsProvider).To(BeNil())
+		g.Expect(d.azureTokenCredential).To(BeNil())
+		g.Expect(d.gcpTokenSource).To(BeNil())
+	})
+
+	t.Run("sops provider", func(t *testing.T) {
+		g := NewWithT(t)
+
+		d := &Decryptor{
+			kustomization: &kustomizev1.Kustomization{
+				Spec: kustomizev1.KustomizationSpec{
+					Decryption: &kustomizev1.Decryption{
+						Provider: DecryptionProviderSOPS,
+					},
+				},
+			},
+		}
+
+		d.SetAuthOptions(context.Background())
+
+		g.Expect(d.awsCredentialsProvider).NotTo(BeNil())
+		g.Expect(d.azureTokenCredential).NotTo(BeNil())
+		g.Expect(d.gcpTokenSource).NotTo(BeNil())
+	})
+}
+
 func TestDecryptor_SopsDecryptWithFormat(t *testing.T) {
 	t.Run("decrypt INI to INI", func(t *testing.T) {
 		g := NewWithT(t)
@@ -551,7 +605,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
 			Provider: DecryptionProviderSOPS,
 		}
 
-		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus)
+		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil)
 		g.Expect(err).ToNot(HaveOccurred())
 		t.Cleanup(cleanup)
 
@@ -592,7 +646,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
 			Provider: DecryptionProviderSOPS,
 		}
 
-		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus)
+		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil)
 		g.Expect(err).ToNot(HaveOccurred())
 		t.Cleanup(cleanup)
 
@@ -627,7 +681,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
 			Provider: DecryptionProviderSOPS,
 		}
 
-		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus)
+		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil)
 		g.Expect(err).ToNot(HaveOccurred())
 		t.Cleanup(cleanup)
 
@@ -662,7 +716,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
 			Provider: DecryptionProviderSOPS,
 		}
 
-		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus)
+		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil)
 		g.Expect(err).ToNot(HaveOccurred())
 		t.Cleanup(cleanup)
 
@@ -711,7 +765,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
 	t.Run("nil resource", func(t *testing.T) {
 		g := NewWithT(t)
 
-		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy())
+		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy(), nil)
 		g.Expect(err).ToNot(HaveOccurred())
 		t.Cleanup(cleanup)
 
@@ -723,7 +777,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
 	t.Run("no decryption spec", func(t *testing.T) {
 		g := NewWithT(t)
 
-		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy())
+		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kustomization.DeepCopy(), nil)
 		g.Expect(err).ToNot(HaveOccurred())
 		t.Cleanup(cleanup)
 
@@ -739,7 +793,7 @@ func TestDecryptor_DecryptResource(t *testing.T) {
 		kus.Spec.Decryption = &kustomizev1.Decryption{
 			Provider: "not-supported",
 		}
-		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus)
+		d, cleanup, err := NewTempDecryptor("", fake.NewClientBuilder().Build(), kus, nil)
 		g.Expect(err).ToNot(HaveOccurred())
 		t.Cleanup(cleanup)
 
diff --git a/internal/sops/awskms/region_test.go b/internal/sops/awskms/region_test.go
new file mode 100644
index 0000000..98a587d
--- /dev/null
+++ b/internal/sops/awskms/region_test.go
@@ -0,0 +1,34 @@
+/*
+Copyright 2025 The Flux authors
+
+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 awskms_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/gomega"
+
+	"github.com/fluxcd/kustomize-controller/internal/sops/awskms"
+)
+
+func TestGetRegionFromKMSARN(t *testing.T) {
+	g := NewWithT(t)
+
+	arn := "arn:aws:kms:us-east-1:211125720409:key/mrk-3179bb7e88bc42ffb1a27d5038ceea25"
+
+	region := awskms.GetRegionFromKMSARN(arn)
+	g.Expect(region).To(Equal("us-east-1"))
+}
diff --git a/internal/sops/keyservice/server_test.go b/internal/sops/keyservice/server_test.go
index 8ccd408..996f6f5 100644
--- a/internal/sops/keyservice/server_test.go
+++ b/internal/sops/keyservice/server_test.go
@@ -21,7 +21,9 @@ import (
 	"os"
 	"testing"
 
+	gcpkmsapi "cloud.google.com/go/kms/apiv1"
 	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
+	"github.com/aws/aws-sdk-go-v2/aws"
 	"github.com/aws/aws-sdk-go-v2/credentials"
 	"github.com/getsops/sops/v3/age"
 	"github.com/getsops/sops/v3/azkv"
@@ -32,6 +34,7 @@ import (
 	"github.com/getsops/sops/v3/pgp"
 	. "github.com/onsi/gomega"
 	"golang.org/x/net/context"
+	"golang.org/x/oauth2/google"
 )
 
 func TestServer_EncryptDecrypt_PGP(t *testing.T) {
@@ -151,8 +154,8 @@ func TestServer_EncryptDecrypt_HCVault_Fallback(t *testing.T) {
 
 func TestServer_EncryptDecrypt_awskms(t *testing.T) {
 	g := NewWithT(t)
-	s := NewServer(WithAWSKeys{
-		CredsProvider: awskms.NewCredentialsProvider(credentials.StaticCredentialsProvider{}),
+	s := NewServer(WithAWSCredentialsProvider{
+		CredentialsProvider: func(region string) aws.CredentialsProvider { return credentials.StaticCredentialsProvider{} },
 	})
 
 	key := KeyFromMasterKey(awskms.NewMasterKeyFromArn("arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48", nil, ""))
@@ -174,7 +177,7 @@ func TestServer_EncryptDecrypt_azkv(t *testing.T) {
 
 	identity, err := azidentity.NewDefaultAzureCredential(nil)
 	g.Expect(err).ToNot(HaveOccurred())
-	s := NewServer(WithAzureToken{Token: azkv.NewTokenCredential(identity)})
+	s := NewServer(WithAzureTokenCredential{TokenCredential: identity})
 
 	key := KeyFromMasterKey(azkv.NewMasterKey("", "", ""))
 	_, err = s.Encrypt(context.TODO(), &keyservice.EncryptRequest{
@@ -194,24 +197,24 @@ func TestServer_EncryptDecrypt_azkv(t *testing.T) {
 func TestServer_EncryptDecrypt_gcpkms(t *testing.T) {
 	g := NewWithT(t)
 
-	creds := `{ "client_id": "<client-id>.apps.googleusercontent.com",
- 		"client_secret": "<secret>",
-		"type": "authorized_user"}`
-	s := NewServer(WithGCPCredsJSON([]byte(creds)))
+	creds, err := google.CredentialsFromJSON(context.Background(),
+		[]byte(`{"type":"service_account"}`), gcpkmsapi.DefaultAuthScopes()...)
+	g.Expect(err).ToNot(HaveOccurred())
+	s := NewServer(WithGCPTokenSource{TokenSource: creds.TokenSource})
 
 	resourceID := "projects/test-flux/locations/global/keyRings/test-flux/cryptoKeys/sops"
 	key := KeyFromMasterKey(gcpkms.NewMasterKeyFromResourceID(resourceID))
-	_, err := s.Encrypt(context.TODO(), &keyservice.EncryptRequest{
+	_, err = s.Encrypt(context.TODO(), &keyservice.EncryptRequest{
 		Key: &key,
 	})
 	g.Expect(err).To(HaveOccurred())
-	g.Expect(err.Error()).To(ContainSubstring("cannot create GCP KMS service"))
+	g.Expect(err.Error()).To(ContainSubstring("failed to encrypt sops data key with GCP KMS key"))
 
 	_, err = s.Decrypt(context.TODO(), &keyservice.DecryptRequest{
 		Key: &key,
 	})
 	g.Expect(err).To(HaveOccurred())
-	g.Expect(err.Error()).To(ContainSubstring("cannot create GCP KMS service"))
+	g.Expect(err.Error()).To(ContainSubstring("failed to decrypt sops data key with GCP KMS key"))
 
 }