Browse Source

Merge pull request #1040 from AndreyZamyslov/yandex-certificate-manager

Support for Yandex Certificate Manager
paul-the-alien[bot] 4 years ago
parent
commit
3de2cc8bee
30 changed files with 2549 additions and 656 deletions
  1. 4 0
      apis/externalsecrets/v1beta1/secretstore_types.go
  2. 43 0
      apis/externalsecrets/v1beta1/secretstore_yandexcertificatemanager_types.go
  3. 58 0
      apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
  4. 58 0
      config/crds/bases/external-secrets.io_clustersecretstores.yaml
  5. 58 0
      config/crds/bases/external-secrets.io_secretstores.yaml
  6. 86 0
      deploy/crds/bundle.yaml
  7. 95 0
      docs/provider-yandex-certificate-manager.md
  8. 130 0
      docs/spec.md
  9. 1 0
      hack/api-docs/mkdocs.yml
  10. 1 0
      pkg/provider/register/register.go
  11. 80 0
      pkg/provider/yandex/certificatemanager/certificatemanager.go
  12. 717 0
      pkg/provider/yandex/certificatemanager/certificatemanager_test.go
  13. 85 0
      pkg/provider/yandex/certificatemanager/certificatemanagersecretgetter.go
  14. 25 0
      pkg/provider/yandex/certificatemanager/client/client.go
  15. 136 0
      pkg/provider/yandex/certificatemanager/client/fakeclient.go
  16. 57 0
      pkg/provider/yandex/certificatemanager/client/grpcclient.go
  17. 22 0
      pkg/provider/yandex/common/clock/clock.go
  18. 32 0
      pkg/provider/yandex/common/clock/fakeclock.go
  19. 27 0
      pkg/provider/yandex/common/clock/realclock.go
  20. 260 0
      pkg/provider/yandex/common/provider.go
  21. 43 58
      pkg/provider/yandex/lockbox/client/grpc/grpc.go
  22. 24 0
      pkg/provider/yandex/common/secretgetter.go
  23. 51 0
      pkg/provider/yandex/common/secretsclient.go
  24. 3 17
      pkg/provider/yandex/lockbox/client/client.go
  25. 0 152
      pkg/provider/yandex/lockbox/client/fake/fake.go
  26. 136 0
      pkg/provider/yandex/lockbox/client/fakeclient.go
  27. 58 0
      pkg/provider/yandex/lockbox/client/grpcclient.go
  28. 24 286
      pkg/provider/yandex/lockbox/lockbox.go
  29. 122 143
      pkg/provider/yandex/lockbox/lockbox_test.go
  30. 113 0
      pkg/provider/yandex/lockbox/lockboxsecretgetter.go

+ 4 - 0
apis/externalsecrets/v1beta1/secretstore_types.go

@@ -70,6 +70,10 @@ type SecretStoreProvider struct {
 	// +optional
 	IBM *IBMProvider `json:"ibm,omitempty"`
 
+	// YandexCertificateManager configures this store to sync secrets using Yandex Certificate Manager provider
+	// +optional
+	YandexCertificateManager *YandexCertificateManagerProvider `json:"yandexcertificatemanager,omitempty"`
+
 	// YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
 	// +optional
 	YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`

+ 43 - 0
apis/externalsecrets/v1beta1/secretstore_yandexcertificatemanager_types.go

@@ -0,0 +1,43 @@
+/*
+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 v1beta1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type YandexCertificateManagerAuth struct {
+	// The authorized key used for authentication
+	// +optional
+	AuthorizedKey esmeta.SecretKeySelector `json:"authorizedKeySecretRef,omitempty"`
+}
+
+type YandexCertificateManagerCAProvider struct {
+	Certificate esmeta.SecretKeySelector `json:"certSecretRef,omitempty"`
+}
+
+// YandexCertificateManagerProvider Configures a store to sync secrets using the Yandex Certificate Manager provider.
+type YandexCertificateManagerProvider struct {
+	// Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+	// +optional
+	APIEndpoint string `json:"apiEndpoint,omitempty"`
+
+	// Auth defines the information necessary to authenticate against Yandex Certificate Manager
+	Auth YandexCertificateManagerAuth `json:"auth"`
+
+	// The provider for the CA bundle to use to validate Yandex.Cloud server certificate.
+	// +optional
+	CAProvider *YandexCertificateManagerCAProvider `json:"caProvider,omitempty"`
+}

+ 58 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -1384,6 +1384,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(IBMProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.YandexCertificateManager != nil {
+		in, out := &in.YandexCertificateManager, &out.YandexCertificateManager
+		*out = new(YandexCertificateManagerProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.YandexLockbox != nil {
 		in, out := &in.YandexLockbox, &out.YandexLockbox
 		*out = new(YandexLockboxProvider)
@@ -1970,6 +1975,59 @@ func (in *WebhookSecret) DeepCopy() *WebhookSecret {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *YandexCertificateManagerAuth) DeepCopyInto(out *YandexCertificateManagerAuth) {
+	*out = *in
+	in.AuthorizedKey.DeepCopyInto(&out.AuthorizedKey)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexCertificateManagerAuth.
+func (in *YandexCertificateManagerAuth) DeepCopy() *YandexCertificateManagerAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexCertificateManagerAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *YandexCertificateManagerCAProvider) DeepCopyInto(out *YandexCertificateManagerCAProvider) {
+	*out = *in
+	in.Certificate.DeepCopyInto(&out.Certificate)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexCertificateManagerCAProvider.
+func (in *YandexCertificateManagerCAProvider) DeepCopy() *YandexCertificateManagerCAProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexCertificateManagerCAProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *YandexCertificateManagerProvider) DeepCopyInto(out *YandexCertificateManagerProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+	if in.CAProvider != nil {
+		in, out := &in.CAProvider, &out.CAProvider
+		*out = new(YandexCertificateManagerCAProvider)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexCertificateManagerProvider.
+func (in *YandexCertificateManagerProvider) DeepCopy() *YandexCertificateManagerProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexCertificateManagerProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
 	*out = *in
 	in.AuthorizedKey.DeepCopyInto(&out.AuthorizedKey)

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

@@ -2749,6 +2749,64 @@ spec:
                     - result
                     - url
                     type: object
+                  yandexcertificatemanager:
+                    description: YandexCertificateManager configures this store to
+                      sync secrets using Yandex Certificate Manager provider
+                    properties:
+                      apiEndpoint:
+                        description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+                        type: string
+                      auth:
+                        description: Auth defines the information necessary to authenticate
+                          against Yandex Certificate Manager
+                        properties:
+                          authorizedKeySecretRef:
+                            description: The authorized key used for authentication
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                      caProvider:
+                        description: The provider for the CA bundle to use to validate
+                          Yandex.Cloud server certificate.
+                        properties:
+                          certSecretRef:
+                            description: A reference to a specific 'key' within a
+                              Secret resource, In some instances, `key` is a required
+                              field.
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                    required:
+                    - auth
+                    type: object
                   yandexlockbox:
                     description: YandexLockbox configures this store to sync secrets
                       using Yandex Lockbox provider

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

@@ -2752,6 +2752,64 @@ spec:
                     - result
                     - url
                     type: object
+                  yandexcertificatemanager:
+                    description: YandexCertificateManager configures this store to
+                      sync secrets using Yandex Certificate Manager provider
+                    properties:
+                      apiEndpoint:
+                        description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+                        type: string
+                      auth:
+                        description: Auth defines the information necessary to authenticate
+                          against Yandex Certificate Manager
+                        properties:
+                          authorizedKeySecretRef:
+                            description: The authorized key used for authentication
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                      caProvider:
+                        description: The provider for the CA bundle to use to validate
+                          Yandex.Cloud server certificate.
+                        properties:
+                          certSecretRef:
+                            description: A reference to a specific 'key' within a
+                              Secret resource, In some instances, `key` is a required
+                              field.
+                            properties:
+                              key:
+                                description: The key of the entry in the Secret resource's
+                                  `data` field to be used. Some instances of this
+                                  field may be defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: Namespace of the resource being referred
+                                  to. Ignored if referent is not cluster-scoped. cluster-scoped
+                                  defaults to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                    required:
+                    - auth
+                    type: object
                   yandexlockbox:
                     description: YandexLockbox configures this store to sync secrets
                       using Yandex Lockbox provider

+ 86 - 0
deploy/crds/bundle.yaml

@@ -2351,6 +2351,49 @@ spec:
                         - result
                         - url
                       type: object
+                    yandexcertificatemanager:
+                      description: YandexCertificateManager configures this store to sync secrets using Yandex Certificate Manager provider
+                      properties:
+                        apiEndpoint:
+                          description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+                          type: string
+                        auth:
+                          description: Auth defines the information necessary to authenticate against Yandex Certificate Manager
+                          properties:
+                            authorizedKeySecretRef:
+                              description: The authorized key used for authentication
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                        caProvider:
+                          description: The provider for the CA bundle to use to validate Yandex.Cloud server certificate.
+                          properties:
+                            certSecretRef:
+                              description: A reference to a specific 'key' within a Secret resource, In some instances, `key` is a required field.
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                      required:
+                        - auth
+                      type: object
                     yandexlockbox:
                       description: YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
                       properties:
@@ -4986,6 +5029,49 @@ spec:
                         - result
                         - url
                       type: object
+                    yandexcertificatemanager:
+                      description: YandexCertificateManager configures this store to sync secrets using Yandex Certificate Manager provider
+                      properties:
+                        apiEndpoint:
+                          description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
+                          type: string
+                        auth:
+                          description: Auth defines the information necessary to authenticate against Yandex Certificate Manager
+                          properties:
+                            authorizedKeySecretRef:
+                              description: The authorized key used for authentication
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                        caProvider:
+                          description: The provider for the CA bundle to use to validate Yandex.Cloud server certificate.
+                          properties:
+                            certSecretRef:
+                              description: A reference to a specific 'key' within a Secret resource, In some instances, `key` is a required field.
+                              properties:
+                                key:
+                                  description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                      required:
+                        - auth
+                      type: object
                     yandexlockbox:
                       description: YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
                       properties:

+ 95 - 0
docs/provider-yandex-certificate-manager.md

@@ -0,0 +1,95 @@
+## Yandex Certificate Manager
+
+External Secrets Operator integrates with [Yandex Certificate Manager](https://cloud.yandex.com/docs/certificate-manager/)
+for secret management.
+
+### Prerequisites
+* [External Secrets Operator installed](../guides-getting-started/#installing-with-helm)
+* [Yandex.Cloud CLI installed](https://cloud.yandex.com/docs/cli/quickstart)
+
+### Authentication
+At the moment, [authorized key](https://cloud.yandex.com/docs/iam/concepts/authorization/key) authentication is only supported:
+
+* Create a [service account](https://cloud.yandex.com/docs/iam/concepts/users/service-accounts) in Yandex.Cloud:
+```bash
+yc iam service-account create --name eso-service-account
+```
+* Create an authorized key for the service account and save it to `authorized-key.json` file:
+```bash
+yc iam key create \
+  --service-account-name eso-service-account \
+  --output authorized-key.json
+```
+* Create a k8s secret containing the authorized key saved above:
+```bash
+kubectl create secret generic yc-auth --from-file=authorized-key=authorized-key.json
+```
+* Create a [SecretStore](../api-secretstore/) pointing to `yc-auth` k8s secret:
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: secret-store
+spec:
+  provider:
+    yandexcertificatemanager:
+      auth:
+        authorizedKeySecretRef:
+          name: yc-auth
+          key: authorized-key
+```
+
+**NOTE:** In case of a `ClusterSecretStore`, Be sure to provide `namespace` in all `authorizedKeySecretRef` with the namespace where the secret resides.
+
+### Creating external secret
+To make External Secrets Operator sync a k8s secret with a Certificate Manager certificate:
+
+* Create a Certificate Manager certificate (follow
+  [the instructions](https://cloud.yandex.com/en-ru/docs/certificate-manager/operations/)), if not already created.
+* Assign the [`certificate-manager.certificates.downloader`](https://cloud.yandex.com/en-ru/docs/certificate-manager/security/#roles-list) role
+  for accessing the certificate content to the service account used for authentication (`*****` is the certificate ID):
+```bash
+yc cm certificate add-access-binding \
+  --id ***** \
+  --service-account-name eso-service-account \
+  --role certificate-manager.certificates.downloader
+```
+Run the following command to ensure that the correct access binding has been added:
+```bash
+yc cm certificate list-access-bindings --id *****
+```
+* Create an [ExternalSecret](../api-externalsecret/) pointing to `secret-store` and the certificate in Certificate Manager:
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: external-secret
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: secret-store
+    kind: SecretStore
+  target:
+    name: k8s-secret # the target k8s secret name
+    template:
+      type: kubernetes.io/tls
+  data:
+    - secretKey: tls.crt # the target k8s secret key
+      remoteRef:
+        key: ***** # the certificate ID
+        property: chain
+    - secretKey: tls.key # the target k8s secret key
+      remoteRef:
+        key: ***** # the certificate ID
+        property: privateKey
+```
+The following property values are possible:
+    * `chain` – to fetch PEM-encoded certificate chain
+    * `privateKey` – to fetch PEM-encoded private key
+    * `chainAndPrivateKey` or missing property – to fetch both chain and private key
+
+The operator will fetch the Yandex Certificate Manager certificate and inject it as a `Kind=Secret`
+```yaml
+kubectl get secret k8s-secret -ojson | jq '."data"."tls\.crt"' -r | base64 --decode
+kubectl get secret k8s-secret -ojson | jq '."data"."tls\.key"' -r | base64 --decode
+```

+ 130 - 0
docs/spec.md

@@ -3350,6 +3350,20 @@ IBMProvider
 </tr>
 <tr>
 <td>
+<code>yandexcertificatemanager</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.YandexCertificateManagerProvider">
+YandexCertificateManagerProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>YandexCertificateManager configures this store to sync secrets using Yandex Certificate Manager provider</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>yandexlockbox</code></br>
 <em>
 <a href="#external-secrets.io/v1beta1.YandexLockboxProvider">
@@ -5004,6 +5018,122 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1beta1.YandexCertificateManagerAuth">YandexCertificateManagerAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.YandexCertificateManagerProvider">YandexCertificateManagerProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>authorizedKeySecretRef</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The authorized key used for authentication</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.YandexCertificateManagerCAProvider">YandexCertificateManagerCAProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.YandexCertificateManagerProvider">YandexCertificateManagerProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>certSecretRef</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.YandexCertificateManagerProvider">YandexCertificateManagerProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>YandexCertificateManagerProvider Configures a store to sync secrets using the Yandex Certificate Manager provider.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>apiEndpoint</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Yandex.Cloud API endpoint (e.g. &lsquo;api.cloud.yandex.net:443&rsquo;)</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.YandexCertificateManagerAuth">
+YandexCertificateManagerAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth defines the information necessary to authenticate against Yandex Certificate Manager</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>caProvider</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.YandexCertificateManagerCAProvider">
+YandexCertificateManagerCAProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The provider for the CA bundle to use to validate Yandex.Cloud server certificate.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.YandexLockboxAuth">YandexLockboxAuth
 </h3>
 <p>

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -60,6 +60,7 @@ nav:
     - Akeyless: provider-akeyless.md
     - HashiCorp Vault: provider-hashicorp-vault.md
     - Yandex:
+        - Certificate Manager: provider-yandex-certificate-manager.md
         - Lockbox: provider-yandex-lockbox.md
     - Gitlab:
       - Gitlab Project Variables: provider-gitlab-project-variables.md

+ 1 - 0
pkg/provider/register/register.go

@@ -31,5 +31,6 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/senhasegura"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/webhook"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/certificatemanager"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
 )

+ 80 - 0
pkg/provider/yandex/certificatemanager/certificatemanager.go

@@ -0,0 +1,80 @@
+/*
+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 certificatemanager
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	ctrl "sigs.k8s.io/controller-runtime"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/certificatemanager/client"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+)
+
+var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("certificatemanager")
+
+func adaptInput(store esv1beta1.GenericStore) (*common.SecretsClientInput, error) {
+	storeSpec := store.GetSpec()
+	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexCertificateManager == nil {
+		return nil, fmt.Errorf("received invalid Yandex Certificate Manager SecretStore resource")
+	}
+	storeSpecYandexCertificateManager := storeSpec.Provider.YandexCertificateManager
+
+	if storeSpecYandexCertificateManager.Auth.AuthorizedKey.Name == "" {
+		return nil, fmt.Errorf("invalid Yandex Certificate Manager SecretStore resource: missing AuthorizedKey Name")
+	}
+
+	var caCertificate *esmeta.SecretKeySelector
+	if storeSpecYandexCertificateManager.CAProvider != nil {
+		caCertificate = &storeSpecYandexCertificateManager.CAProvider.Certificate
+	}
+
+	return &common.SecretsClientInput{
+		APIEndpoint:   storeSpecYandexCertificateManager.APIEndpoint,
+		AuthorizedKey: storeSpecYandexCertificateManager.Auth.AuthorizedKey,
+		CACertificate: caCertificate,
+	}, nil
+}
+
+func newSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (common.SecretGetter, error) {
+	grpcClient, err := client.NewGrpcCertificateManagerClient(ctx, apiEndpoint, authorizedKey, caCertificate)
+	if err != nil {
+		return nil, err
+	}
+	return newCertificateManagerSecretGetter(grpcClient)
+}
+
+func init() {
+	provider := common.InitYandexCloudProvider(
+		log,
+		clock.NewRealClock(),
+		adaptInput,
+		newSecretGetter,
+		common.NewIamToken,
+		time.Hour,
+	)
+
+	esv1beta1.Register(
+		provider,
+		&esv1beta1.SecretStoreProvider{
+			YandexCertificateManager: &esv1beta1.YandexCertificateManagerProvider{},
+		},
+	)
+}

+ 717 - 0
pkg/provider/yandex/certificatemanager/certificatemanager_test.go

@@ -0,0 +1,717 @@
+/*
+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 certificatemanager
+
+import (
+	"context"
+	"encoding/json"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/google/uuid"
+	tassert "github.com/stretchr/testify/assert"
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/certificatemanager/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/certificatemanager/client"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+)
+
+const (
+	errMissingKey                    = "invalid Yandex Certificate Manager SecretStore resource: missing AuthorizedKey Name"
+	errSecretPayloadPermissionDenied = "unable to request certificate content to get secret: permission denied"
+	errSecretPayloadNotFound         = "unable to request certificate content to get secret: certificate not found"
+)
+
+func TestNewClient(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+
+	store := &esv1beta1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				YandexCertificateManager: &esv1beta1.YandexCertificateManagerProvider{},
+			},
+		},
+	}
+	provider, err := esv1beta1.GetProvider(store)
+	tassert.Nil(t, err)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	secretClient, err := provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, errMissingKey)
+	tassert.Nil(t, secretClient)
+
+	store.Spec.Provider.YandexCertificateManager.Auth = esv1beta1.YandexCertificateManagerAuth{}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, errMissingKey)
+	tassert.Nil(t, secretClient)
+
+	store.Spec.Provider.YandexCertificateManager.Auth.AuthorizedKey = esmeta.SecretKeySelector{}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, errMissingKey)
+	tassert.Nil(t, secretClient)
+
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	store.Spec.Provider.YandexCertificateManager.Auth.AuthorizedKey.Name = authorizedKeySecretName
+	store.Spec.Provider.YandexCertificateManager.Auth.AuthorizedKey.Key = authorizedKeySecretKey
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "could not fetch AuthorizedKey secret: secrets \"authorizedKeySecretName\" not found")
+	tassert.Nil(t, secretClient)
+
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, newFakeAuthorizedKey()))
+	tassert.Nil(t, err)
+
+	const caCertificateSecretName = "caCertificateSecretName"
+	const caCertificateSecretKey = "caCertificateSecretKey"
+	store.Spec.Provider.YandexCertificateManager.CAProvider = &esv1beta1.YandexCertificateManagerCAProvider{
+		Certificate: esmeta.SecretKeySelector{
+			Key:  caCertificateSecretKey,
+			Name: caCertificateSecretName,
+		},
+	}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "could not fetch CA certificate secret: secrets \"caCertificateSecretName\" not found")
+	tassert.Nil(t, secretClient)
+
+	err = createK8sSecret(ctx, t, k8sClient, namespace, caCertificateSecretName, caCertificateSecretKey, []byte("it-is-not-a-certificate"))
+	tassert.Nil(t, err)
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "failed to create Yandex.Cloud client: unable to read trusted CA certificates")
+	tassert.Nil(t, secretClient)
+}
+
+func TestGetSecretWithoutProperty(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate1 := uuid.NewString()
+	certificate2 := uuid.NewString()
+	privateKey := uuid.NewString()
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1, certificate2},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{certificate1, certificate2, privateKey}, "\n")),
+		strings.TrimSpace(string(data)),
+	)
+}
+
+func TestGetSecretWithProperty(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate1 := uuid.NewString()
+	certificate2 := uuid.NewString()
+	privateKey := uuid.NewString()
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1, certificate2},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+
+	chainData, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: chainProperty})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{certificate1, certificate2}, "\n")),
+		strings.TrimSpace(string(chainData)),
+	)
+
+	privateKeyData, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: privateKeyProperty})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		strings.TrimSpace(privateKey),
+		strings.TrimSpace(string(privateKeyData)),
+	)
+
+	chainAndPrivateKeyData, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: chainAndPrivateKeyProperty})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{certificate1, certificate2, privateKey}, "\n")),
+		strings.TrimSpace(string(chainAndPrivateKeyData)),
+	)
+}
+
+func TestGetSecretByVersionID(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	oldCertificate1 := uuid.NewString()
+	oldCertificate2 := uuid.NewString()
+	oldPrivateKey := uuid.NewString()
+	certificateID, oldVersionID := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{oldCertificate1, oldCertificate2},
+		PrivateKey:       oldPrivateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: oldVersionID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{oldCertificate1, oldCertificate2, oldPrivateKey}, "\n")),
+		strings.TrimSpace(string(data)),
+	)
+
+	newCertificate1 := uuid.NewString()
+	newCertificate2 := uuid.NewString()
+	newPrivateKey := uuid.NewString()
+	newVersionID := fakeCertificateManagerServer.AddVersion(certificateID, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{newCertificate1, newCertificate2},
+		PrivateKey:       newPrivateKey,
+	})
+
+	data, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: oldVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{oldCertificate1, oldCertificate2, oldPrivateKey}, "\n")),
+		strings.TrimSpace(string(data)),
+	)
+
+	data, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: newVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		strings.TrimSpace(strings.Join([]string{newCertificate1, newCertificate2, newPrivateKey}, "\n")),
+		strings.TrimSpace(string(data)),
+	)
+}
+
+func TestGetSecretUnauthorized(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKeyA := newFakeAuthorizedKey()
+	authorizedKeyB := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKeyA, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{uuid.NewString()},
+		PrivateKey:       uuid.NewString(),
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKeyB))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID})
+	tassert.EqualError(t, err, errSecretPayloadPermissionDenied)
+}
+
+func TestGetSecretNotFound(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
+	tassert.EqualError(t, err, errSecretPayloadNotFound)
+
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{uuid.NewString()},
+		PrivateKey:       uuid.NewString(),
+	})
+	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: "no-version-with-this-id"})
+	tassert.EqualError(t, err, "unable to request certificate content to get secret: version not found")
+}
+
+func TestGetSecretWithTwoNamespaces(t *testing.T) {
+	ctx := context.Background()
+	namespace1 := uuid.NewString()
+	namespace2 := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate1 := uuid.NewString()
+	privateKey1 := uuid.NewString()
+	certificateID1, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey1, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1},
+		PrivateKey:       privateKey1,
+	})
+	certificate2 := uuid.NewString()
+	privateKey2 := uuid.NewString()
+	certificateID2, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey2, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate2},
+		PrivateKey:       privateKey2,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey1))
+	tassert.Nil(t, err)
+	err = createK8sSecret(ctx, t, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey2))
+	tassert.Nil(t, err)
+	store1 := newYandexCertificateManagerSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
+	store2 := newYandexCertificateManagerSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient1, err := provider.NewClient(ctx, store1, k8sClient, namespace1)
+	tassert.Nil(t, err)
+	secretsClient2, err := provider.NewClient(ctx, store2, k8sClient, namespace2)
+	tassert.Nil(t, err)
+
+	data, err := secretsClient1.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID1, Property: privateKeyProperty})
+	tassert.Equal(t, privateKey1, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+	data, err = secretsClient1.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID2, Property: privateKeyProperty})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, errSecretPayloadPermissionDenied)
+
+	data, err = secretsClient2.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID1, Property: privateKeyProperty})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, errSecretPayloadPermissionDenied)
+	data, err = secretsClient2.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID2, Property: privateKeyProperty})
+	tassert.Equal(t, privateKey2, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithTwoApiEndpoints(t *testing.T) {
+	ctx := context.Background()
+	apiEndpoint1 := uuid.NewString()
+	apiEndpoint2 := uuid.NewString()
+	namespace := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer1 := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate1 := uuid.NewString()
+	privateKey1 := uuid.NewString()
+	certificateID1, _ := fakeCertificateManagerServer1.CreateCertificate(authorizedKey1, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1},
+		PrivateKey:       privateKey1,
+	})
+	fakeCertificateManagerServer2 := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate2 := uuid.NewString()
+	privateKey2 := uuid.NewString()
+	certificateID2, _ := fakeCertificateManagerServer2.CreateCertificate(authorizedKey2, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate2},
+		PrivateKey:       privateKey2,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJSON(t, authorizedKey1))
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, toJSON(t, authorizedKey2))
+	tassert.Nil(t, err)
+
+	store1 := newYandexCertificateManagerSecretStore(apiEndpoint1, namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
+	store2 := newYandexCertificateManagerSecretStore(apiEndpoint2, namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
+
+	provider1 := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer1)
+	provider2 := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer2)
+
+	secretsClient1, err := provider1.NewClient(ctx, store1, k8sClient, namespace)
+	tassert.Nil(t, err)
+	secretsClient2, err := provider2.NewClient(ctx, store2, k8sClient, namespace)
+	tassert.Nil(t, err)
+
+	var data []byte
+
+	data, err = secretsClient1.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID1, Property: chainProperty})
+	tassert.Equal(t, certificate1, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+	data, err = secretsClient1.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID2, Property: chainProperty})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, errSecretPayloadNotFound)
+
+	data, err = secretsClient2.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID1, Property: chainProperty})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, errSecretPayloadNotFound)
+	data, err = secretsClient2.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID2, Property: chainProperty})
+	tassert.Equal(t, certificate2, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithIamTokenExpiration(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	tokenExpirationTime := time.Hour
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, tokenExpirationTime)
+	certificate := uuid.NewString()
+	privateKey := uuid.NewString()
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+
+	var data []byte
+
+	oldSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err = oldSecretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: privateKeyProperty})
+	tassert.Equal(t, privateKey, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+
+	fakeClock.AddDuration(2 * tokenExpirationTime)
+
+	data, err = oldSecretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: privateKeyProperty})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request certificate content to get secret: iam token expired")
+
+	newSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err = newSecretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Property: privateKeyProperty})
+	tassert.Equal(t, privateKey, strings.TrimSpace(string(data)))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithIamTokenCleanup(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	tokenExpirationDuration := time.Hour
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, tokenExpirationDuration)
+	certificateID1, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey1, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{uuid.NewString()},
+		PrivateKey:       uuid.NewString(),
+	})
+	certificateID2, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey2, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{uuid.NewString()},
+		PrivateKey:       uuid.NewString(),
+	})
+
+	var err error
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJSON(t, authorizedKey1))
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, toJSON(t, authorizedKey2))
+	tassert.Nil(t, err)
+
+	store1 := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
+	store2 := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
+
+	// Access secretID1 with authorizedKey1, IAM token for authorizedKey1 should be cached
+	secretsClient, err := provider.NewClient(ctx, store1, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID1})
+	tassert.Nil(t, err)
+
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
+
+	fakeClock.AddDuration(tokenExpirationDuration * 2)
+
+	// Access secretID2 with authorizedKey2, IAM token for authorizedKey2 should be cached
+	secretsClient, err = provider.NewClient(ctx, store2, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID2})
+	tassert.Nil(t, err)
+
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
+
+	fakeClock.AddDuration(tokenExpirationDuration)
+
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
+
+	provider.CleanUpIamTokenMap()
+
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
+
+	fakeClock.AddDuration(tokenExpirationDuration)
+
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
+
+	provider.CleanUpIamTokenMap()
+
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
+}
+
+func TestGetSecretMap(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate1 := uuid.NewString()
+	certificate2 := uuid.NewString()
+	privateKey := uuid.NewString()
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1, certificate2},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			chainProperty:      []byte(strings.Join([]string{certificate1, certificate2}, "\n")),
+			privateKeyProperty: []byte(privateKey),
+		},
+		data,
+	)
+}
+
+func TestGetSecretMapByVersionID(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	fakeClock := clock.NewFakeClock()
+	fakeCertificateManagerServer := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	oldCertificate := uuid.NewString()
+	oldPrivateKey := uuid.NewString()
+	certificateID, oldVersionID := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{oldCertificate},
+		PrivateKey:       oldPrivateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
+	tassert.Nil(t, err)
+	store := newYandexCertificateManagerSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newCertificateManagerProvider(fakeClock, fakeCertificateManagerServer)
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: oldVersionID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			chainProperty:      []byte(oldCertificate),
+			privateKeyProperty: []byte(oldPrivateKey),
+		},
+		data,
+	)
+
+	newCertificate := uuid.NewString()
+	newPrivateKey := uuid.NewString()
+	newVersionID := fakeCertificateManagerServer.AddVersion(certificateID, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{newCertificate},
+		PrivateKey:       newPrivateKey,
+	})
+
+	data, err = secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: oldVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			chainProperty:      []byte(oldCertificate),
+			privateKeyProperty: []byte(oldPrivateKey),
+		},
+		data,
+	)
+
+	data, err = secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: certificateID, Version: newVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			chainProperty:      []byte(newCertificate),
+			privateKeyProperty: []byte(newPrivateKey),
+		},
+		data,
+	)
+}
+
+// helper functions
+
+func newCertificateManagerProvider(clock clock.Clock, fakeCertificateManagerServer *client.FakeCertificateManagerServer) *common.YandexCloudProvider {
+	return common.InitYandexCloudProvider(
+		ctrl.Log.WithName("provider").WithName("yandex").WithName("certificatemanager"),
+		clock,
+		adaptInput,
+		func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (common.SecretGetter, error) {
+			return newCertificateManagerSecretGetter(client.NewFakeCertificateManagerClient(fakeCertificateManagerServer))
+		},
+		func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*common.IamToken, error) {
+			return fakeCertificateManagerServer.NewIamToken(authorizedKey), nil
+		},
+		0,
+	)
+}
+
+func newYandexCertificateManagerSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1beta1.GenericStore {
+	return &esv1beta1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				YandexCertificateManager: &esv1beta1.YandexCertificateManagerProvider{
+					APIEndpoint: apiEndpoint,
+					Auth: esv1beta1.YandexCertificateManagerAuth{
+						AuthorizedKey: esmeta.SecretKeySelector{
+							Name: authorizedKeySecretName,
+							Key:  authorizedKeySecretKey,
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func toJSON(t *testing.T, v interface{}) []byte {
+	jsonBytes, err := json.Marshal(v)
+	tassert.Nil(t, err)
+	return jsonBytes
+}
+
+func createK8sSecret(ctx context.Context, t *testing.T, k8sClient k8sclient.Client, namespace, secretName, secretKey string, secretValue []byte) error {
+	err := k8sClient.Create(ctx, &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+			Name:      secretName,
+		},
+		Data: map[string][]byte{secretKey: secretValue},
+	})
+	tassert.Nil(t, err)
+	return nil
+}
+
+func newFakeAuthorizedKey() *iamkey.Key {
+	uniqueLabel := uuid.NewString()
+	return &iamkey.Key{
+		Id: uniqueLabel,
+		Subject: &iamkey.Key_ServiceAccountId{
+			ServiceAccountId: uniqueLabel,
+		},
+		PrivateKey: uniqueLabel,
+	}
+}

+ 85 - 0
pkg/provider/yandex/certificatemanager/certificatemanagersecretgetter.go

@@ -0,0 +1,85 @@
+/*
+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 certificatemanager
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/certificatemanager/client"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+)
+
+const (
+	chainProperty              = "chain"
+	privateKeyProperty         = "privateKey"
+	chainAndPrivateKeyProperty = "chainAndPrivateKey"
+)
+
+// Implementation of common.SecretGetter.
+type certificateManagerSecretGetter struct {
+	certificateManagerClient client.CertificateManagerClient
+}
+
+func newCertificateManagerSecretGetter(certificateManagerClient client.CertificateManagerClient) (common.SecretGetter, error) {
+	return &certificateManagerSecretGetter{
+		certificateManagerClient: certificateManagerClient,
+	}, nil
+}
+
+func (g *certificateManagerSecretGetter) GetSecret(ctx context.Context, iamToken, resourceID, versionID, property string) ([]byte, error) {
+	response, err := g.certificateManagerClient.GetCertificateContent(ctx, iamToken, resourceID, versionID)
+	if err != nil {
+		return nil, fmt.Errorf("unable to request certificate content to get secret: %w", err)
+	}
+
+	chain := trimAndJoin(response.CertificateChain...)
+	privateKey := trimAndJoin(response.PrivateKey)
+
+	switch property {
+	case "", chainAndPrivateKeyProperty:
+		return []byte(trimAndJoin(chain, privateKey)), nil
+	case chainProperty:
+		return []byte(chain), nil
+	case privateKeyProperty:
+		return []byte(privateKey), nil
+	default:
+		return nil, fmt.Errorf("unsupported property '%s'", property)
+	}
+}
+
+func (g *certificateManagerSecretGetter) GetSecretMap(ctx context.Context, iamToken, resourceID, versionID string) (map[string][]byte, error) {
+	response, err := g.certificateManagerClient.GetCertificateContent(ctx, iamToken, resourceID, versionID)
+	if err != nil {
+		return nil, fmt.Errorf("unable to request certificate content to get secret map: %w", err)
+	}
+
+	chain := strings.Join(response.CertificateChain, "\n")
+	privateKey := response.PrivateKey
+
+	return map[string][]byte{
+		chainProperty:      []byte(chain),
+		privateKeyProperty: []byte(privateKey),
+	}, nil
+}
+
+func trimAndJoin(elems ...string) string {
+	var sb strings.Builder
+	for _, elem := range elems {
+		sb.WriteString(strings.TrimSpace(elem))
+		sb.WriteRune('\n')
+	}
+	return sb.String()
+}

+ 25 - 0
pkg/provider/yandex/certificatemanager/client/client.go

@@ -0,0 +1,25 @@
+/*
+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 client
+
+import (
+	"context"
+
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/certificatemanager/v1"
+)
+
+// Requests the content of the given certificate from Certificate Manager.
+type CertificateManagerClient interface {
+	GetCertificateContent(ctx context.Context, iamToken, certificateID, versionID string) (*api.GetCertificateContentResponse, error)
+}

+ 136 - 0
pkg/provider/yandex/certificatemanager/client/fakeclient.go

@@ -0,0 +1,136 @@
+/*
+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 client
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/google/uuid"
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/certificatemanager/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+)
+
+// Fake implementation of CertificateManagerClient.
+type fakeCertificateManagerClient struct {
+	fakeCertificateManagerServer *FakeCertificateManagerServer
+}
+
+func NewFakeCertificateManagerClient(fakeCertificateManagerServer *FakeCertificateManagerServer) CertificateManagerClient {
+	return &fakeCertificateManagerClient{fakeCertificateManagerServer}
+}
+
+func (c *fakeCertificateManagerClient) GetCertificateContent(ctx context.Context, iamToken, certificateID, versionID string) (*api.GetCertificateContentResponse, error) {
+	return c.fakeCertificateManagerServer.getCertificateContent(iamToken, certificateID, versionID)
+}
+
+// Fakes Yandex Certificate Manager service backend.
+type FakeCertificateManagerServer struct {
+	certificateMap map[certificateKey]certificateValue // certificate specific data
+	versionMap     map[versionKey]versionValue         // version specific data
+	tokenMap       map[tokenKey]tokenValue             // token specific data
+
+	tokenExpirationDuration time.Duration
+	clock                   clock.Clock
+}
+
+type certificateKey struct {
+	certificateID string
+}
+
+type certificateValue struct {
+	expectedAuthorizedKey *iamkey.Key // authorized key expected to access the certificate
+}
+
+type versionKey struct {
+	certificateID string
+	versionID     string
+}
+
+type versionValue struct {
+	content *api.GetCertificateContentResponse
+}
+
+type tokenKey struct {
+	token string
+}
+
+type tokenValue struct {
+	authorizedKey *iamkey.Key
+	expiresAt     time.Time
+}
+
+func NewFakeCertificateManagerServer(clock clock.Clock, tokenExpirationDuration time.Duration) *FakeCertificateManagerServer {
+	return &FakeCertificateManagerServer{
+		certificateMap:          make(map[certificateKey]certificateValue),
+		versionMap:              make(map[versionKey]versionValue),
+		tokenMap:                make(map[tokenKey]tokenValue),
+		tokenExpirationDuration: tokenExpirationDuration,
+		clock:                   clock,
+	}
+}
+
+func (s *FakeCertificateManagerServer) CreateCertificate(authorizedKey *iamkey.Key, content *api.GetCertificateContentResponse) (string, string) {
+	certificateID := uuid.NewString()
+	versionID := uuid.NewString()
+
+	s.certificateMap[certificateKey{certificateID}] = certificateValue{authorizedKey}
+	s.versionMap[versionKey{certificateID, ""}] = versionValue{content} // empty versionID corresponds to the latest version
+	s.versionMap[versionKey{certificateID, versionID}] = versionValue{content}
+
+	return certificateID, versionID
+}
+
+func (s *FakeCertificateManagerServer) AddVersion(certificateID string, content *api.GetCertificateContentResponse) string {
+	versionID := uuid.NewString()
+
+	s.versionMap[versionKey{certificateID, ""}] = versionValue{content} // empty versionID corresponds to the latest version
+	s.versionMap[versionKey{certificateID, versionID}] = versionValue{content}
+
+	return versionID
+}
+
+func (s *FakeCertificateManagerServer) NewIamToken(authorizedKey *iamkey.Key) *common.IamToken {
+	token := uuid.NewString()
+	expiresAt := s.clock.CurrentTime().Add(s.tokenExpirationDuration)
+	s.tokenMap[tokenKey{token}] = tokenValue{authorizedKey, expiresAt}
+	return &common.IamToken{Token: token, ExpiresAt: expiresAt}
+}
+
+func (s *FakeCertificateManagerServer) getCertificateContent(iamToken, certificateID, versionID string) (*api.GetCertificateContentResponse, error) {
+	if _, ok := s.certificateMap[certificateKey{certificateID}]; !ok {
+		return nil, fmt.Errorf("certificate not found")
+	}
+	if _, ok := s.versionMap[versionKey{certificateID, versionID}]; !ok {
+		return nil, fmt.Errorf("version not found")
+	}
+	if _, ok := s.tokenMap[tokenKey{iamToken}]; !ok {
+		return nil, fmt.Errorf("unauthenticated")
+	}
+
+	if s.tokenMap[tokenKey{iamToken}].expiresAt.Before(s.clock.CurrentTime()) {
+		return nil, fmt.Errorf("iam token expired")
+	}
+	if !cmp.Equal(s.tokenMap[tokenKey{iamToken}].authorizedKey, s.certificateMap[certificateKey{certificateID}].expectedAuthorizedKey, cmpopts.IgnoreUnexported(iamkey.Key{})) {
+		return nil, fmt.Errorf("permission denied")
+	}
+
+	return s.versionMap[versionKey{certificateID, versionID}].content, nil
+}

+ 57 - 0
pkg/provider/yandex/certificatemanager/client/grpcclient.go

@@ -0,0 +1,57 @@
+/*
+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 client
+
+import (
+	"context"
+
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/certificatemanager/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	"google.golang.org/grpc"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+)
+
+// Real/gRPC implementation of CertificateManagerClient.
+type grpcCertificateManagerClient struct {
+	certificateContentServiceClient api.CertificateContentServiceClient
+}
+
+func NewGrpcCertificateManagerClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (CertificateManagerClient, error) {
+	conn, err := common.NewGrpcConnection(
+		ctx,
+		apiEndpoint,
+		"certificate-manager-data", // taken from https://api.cloud.yandex.net/endpoints
+		authorizedKey,
+		caCertificate,
+	)
+	if err != nil {
+		return nil, err
+	}
+	return &grpcCertificateManagerClient{api.NewCertificateContentServiceClient(conn)}, nil
+}
+
+func (c *grpcCertificateManagerClient) GetCertificateContent(ctx context.Context, iamToken, certificateID, versionID string) (*api.GetCertificateContentResponse, error) {
+	response, err := c.certificateContentServiceClient.Get(
+		ctx,
+		&api.GetCertificateContentRequest{
+			CertificateId: certificateID,
+		},
+		grpc.PerRPCCredentials(common.PerRPCCredentials{IamToken: iamToken}),
+	)
+	if err != nil {
+		return nil, err
+	}
+	return response, nil
+}

+ 22 - 0
pkg/provider/yandex/common/clock/clock.go

@@ -0,0 +1,22 @@
+/*
+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 clock
+
+import (
+	"time"
+)
+
+type Clock interface {
+	CurrentTime() time.Time
+}

+ 32 - 0
pkg/provider/yandex/common/clock/fakeclock.go

@@ -0,0 +1,32 @@
+/*
+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 clock
+
+import "time"
+
+type FakeClock struct {
+	now time.Time
+}
+
+func NewFakeClock() *FakeClock {
+	return &FakeClock{time.Time{}}
+}
+
+func (c *FakeClock) CurrentTime() time.Time {
+	return c.now
+}
+
+func (c *FakeClock) AddDuration(duration time.Duration) {
+	c.now = c.now.Add(duration)
+}

+ 27 - 0
pkg/provider/yandex/common/clock/realclock.go

@@ -0,0 +1,27 @@
+/*
+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 clock
+
+import "time"
+
+type RealClock struct {
+}
+
+func NewRealClock() *RealClock {
+	return &RealClock{}
+}
+
+func (c *RealClock) CurrentTime() time.Time {
+	return time.Now()
+}

+ 260 - 0
pkg/provider/yandex/common/provider.go

@@ -0,0 +1,260 @@
+/*
+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 common
+
+import (
+	"context"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/go-logr/logr"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	clock2 "github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+)
+
+const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.Provider = &YandexCloudProvider{}
+
+// Implementation of v1beta1.Provider.
+type YandexCloudProvider struct {
+	logger              logr.Logger
+	clock               clock2.Clock
+	adaptInputFunc      AdaptInputFunc
+	newSecretGetterFunc NewSecretGetterFunc
+	newIamTokenFunc     NewIamTokenFunc
+
+	secretGetteMap       map[string]SecretGetter // apiEndpoint -> SecretGetter
+	secretGetterMapMutex sync.Mutex
+	iamTokenMap          map[iamTokenKey]*IamToken
+	iamTokenMapMutex     sync.Mutex
+}
+
+type iamTokenKey struct {
+	authorizedKeyID  string
+	serviceAccountID string
+	privateKeyHash   string
+}
+
+func InitYandexCloudProvider(
+	logger logr.Logger,
+	clock clock2.Clock,
+	adaptInputFunc AdaptInputFunc,
+	newSecretGetterFunc NewSecretGetterFunc,
+	newIamTokenFunc NewIamTokenFunc,
+	iamTokenCleanupDelay time.Duration,
+) *YandexCloudProvider {
+	provider := &YandexCloudProvider{
+		logger:              logger,
+		clock:               clock,
+		adaptInputFunc:      adaptInputFunc,
+		newSecretGetterFunc: newSecretGetterFunc,
+		newIamTokenFunc:     newIamTokenFunc,
+		secretGetteMap:      make(map[string]SecretGetter),
+		iamTokenMap:         make(map[iamTokenKey]*IamToken),
+	}
+
+	if iamTokenCleanupDelay > 0 {
+		go func() {
+			for {
+				time.Sleep(iamTokenCleanupDelay)
+				provider.CleanUpIamTokenMap()
+			}
+		}()
+	}
+
+	return provider
+}
+
+type AdaptInputFunc func(store esv1beta1.GenericStore) (*SecretsClientInput, error)
+type NewSecretGetterFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error)
+type NewIamTokenFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error)
+
+type IamToken struct {
+	Token     string
+	ExpiresAt time.Time
+}
+
+type SecretsClientInput struct {
+	APIEndpoint   string
+	AuthorizedKey esmeta.SecretKeySelector
+	CACertificate *esmeta.SecretKeySelector
+}
+
+// NewClient constructs a Yandex.Cloud Provider.
+func (p *YandexCloudProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	input, err := p.adaptInputFunc(store)
+	if err != nil {
+		return nil, err
+	}
+
+	objectKey := types.NamespacedName{
+		Name:      input.AuthorizedKey.Name,
+		Namespace: namespace,
+	}
+
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
+		if input.AuthorizedKey.Namespace == nil {
+			return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
+		}
+		objectKey.Namespace = *input.AuthorizedKey.Namespace
+	}
+
+	authorizedKeySecret := &corev1.Secret{}
+	err = kube.Get(ctx, objectKey, authorizedKeySecret)
+	if err != nil {
+		return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
+	}
+
+	authorizedKeySecretData := authorizedKeySecret.Data[input.AuthorizedKey.Key]
+	if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
+		return nil, fmt.Errorf("missing AuthorizedKey")
+	}
+
+	var authorizedKey iamkey.Key
+	err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
+	if err != nil {
+		return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
+	}
+
+	var caCertificateData []byte
+
+	if input.CACertificate != nil {
+		certObjectKey := types.NamespacedName{
+			Name:      input.CACertificate.Name,
+			Namespace: namespace,
+		}
+
+		if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
+			if input.CACertificate.Namespace == nil {
+				return nil, fmt.Errorf("invalid ClusterSecretStore: missing CA certificate Namespace")
+			}
+			certObjectKey.Namespace = *input.CACertificate.Namespace
+		}
+
+		caCertificateSecret := &corev1.Secret{}
+		err := kube.Get(ctx, certObjectKey, caCertificateSecret)
+		if err != nil {
+			return nil, fmt.Errorf("could not fetch CA certificate secret: %w", err)
+		}
+
+		caCertificateData = caCertificateSecret.Data[input.CACertificate.Key]
+		if (caCertificateData == nil) || (len(caCertificateData) == 0) {
+			return nil, fmt.Errorf("missing CA Certificate")
+		}
+	}
+
+	secretGetter, err := p.getOrCreateSecretGetter(ctx, input.APIEndpoint, &authorizedKey, caCertificateData)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create Yandex.Cloud client: %w", err)
+	}
+
+	iamToken, err := p.getOrCreateIamToken(ctx, input.APIEndpoint, &authorizedKey, caCertificateData)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create IAM token: %w", err)
+	}
+
+	return &yandexCloudSecretsClient{secretGetter, iamToken.Token}, nil
+}
+
+func (p *YandexCloudProvider) getOrCreateSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error) {
+	p.secretGetterMapMutex.Lock()
+	defer p.secretGetterMapMutex.Unlock()
+
+	if _, ok := p.secretGetteMap[apiEndpoint]; !ok {
+		p.logger.Info("creating SecretGetter", "apiEndpoint", apiEndpoint)
+
+		secretGetter, err := p.newSecretGetterFunc(ctx, apiEndpoint, authorizedKey, caCertificate)
+		if err != nil {
+			return nil, err
+		}
+		p.secretGetteMap[apiEndpoint] = secretGetter
+	}
+	return p.secretGetteMap[apiEndpoint], nil
+}
+
+func (p *YandexCloudProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error) {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	iamTokenKey := buildIamTokenKey(authorizedKey)
+	if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
+		p.logger.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
+
+		iamToken, err := p.newIamTokenFunc(ctx, apiEndpoint, authorizedKey, caCertificate)
+		if err != nil {
+			return nil, err
+		}
+
+		p.logger.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
+
+		p.iamTokenMap[iamTokenKey] = iamToken
+	}
+	return p.iamTokenMap[iamTokenKey], nil
+}
+
+func (p *YandexCloudProvider) isIamTokenUsable(iamToken *IamToken) bool {
+	now := p.clock.CurrentTime()
+	return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
+}
+
+func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
+	privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
+	return iamTokenKey{
+		authorizedKey.GetId(),
+		authorizedKey.GetServiceAccountId(),
+		hex.EncodeToString(privateKeyHash[:]),
+	}
+}
+
+// Used for testing.
+func (p *YandexCloudProvider) IsIamTokenCached(authorizedKey *iamkey.Key) bool {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	_, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
+	return ok
+}
+
+func (p *YandexCloudProvider) CleanUpIamTokenMap() {
+	p.iamTokenMapMutex.Lock()
+	defer p.iamTokenMapMutex.Unlock()
+
+	for key, value := range p.iamTokenMap {
+		if p.clock.CurrentTime().After(value.ExpiresAt) {
+			p.logger.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
+			delete(p.iamTokenMap, key)
+		}
+	}
+}
+
+func (p *YandexCloudProvider) ValidateStore(store esv1beta1.GenericStore) error {
+	_, err := p.adaptInputFunc(store) // adaptInputFunc validates the input store
+	if err != nil {
+		return err
+	}
+	return nil
+}

+ 43 - 58
pkg/provider/yandex/lockbox/client/grpc/grpc.go

@@ -11,7 +11,7 @@ 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 grpc
+package common
 
 import (
 	"context"
@@ -21,22 +21,27 @@ import (
 	"time"
 
 	"github.com/yandex-cloud/go-genproto/yandex/cloud/endpoint"
-	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
 	ycsdk "github.com/yandex-cloud/go-sdk"
 	"github.com/yandex-cloud/go-sdk/iamkey"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/keepalive"
-
-	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
 )
 
-// Implementation of YandexCloudCreator.
-type YandexCloudCreator struct {
-}
+// Creates a connection to the given Yandex.Cloud API endpoint.
+func NewGrpcConnection(
+	ctx context.Context,
+	apiEndpoint string,
+	apiEndpointID string, // an ID from https://api.cloud.yandex.net/endpoints
+	authorizedKey *iamkey.Key,
+	caCertificate []byte,
+) (*grpc.ClientConn, error) {
+	tlsConfig, err := tlsConfig(caCertificate)
+	if err != nil {
+		return nil, err
+	}
 
-func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (client.LockboxClient, error) {
-	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
+	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey, tlsConfig)
 	if err != nil {
 		return nil, err
 	}
@@ -44,26 +49,15 @@ func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoi
 		_ = closeSDK(ctx, sdk)
 	}()
 
-	payloadAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
-		ApiEndpointId: "lockbox-payload", // the ID from https://api.cloud.yandex.net/endpoints
+	serviceAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
+		ApiEndpointId: apiEndpointID,
 	})
 	if err != nil {
 		return nil, err
 	}
 
-	tlsConfig := tls.Config{MinVersion: tls.VersionTLS12}
-
-	if caCertificate != nil {
-		caCertPool := x509.NewCertPool()
-		ok := caCertPool.AppendCertsFromPEM(caCertificate)
-		if !ok {
-			return nil, errors.New("unable to read certificate from PEM file")
-		}
-		tlsConfig.RootCAs = caCertPool
-	}
-
-	conn, err := grpc.Dial(payloadAPIEndpoint.Address,
-		grpc.WithTransportCredentials(credentials.NewTLS(&tlsConfig)),
+	return grpc.Dial(serviceAPIEndpoint.Address,
+		grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
 		grpc.WithKeepaliveParams(keepalive.ClientParameters{
 			Time:                time.Second * 30,
 			Timeout:             time.Second * 10,
@@ -71,15 +65,16 @@ func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoi
 		}),
 		grpc.WithUserAgent("external-secrets"),
 	)
+}
+
+// Exchanges the given authorized key to an IAM token.
+func NewIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error) {
+	tlsConfig, err := tlsConfig(caCertificate)
 	if err != nil {
 		return nil, err
 	}
 
-	return &LockboxClient{lockbox.NewPayloadServiceClient(conn)}, nil
-}
-
-func (lb *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
-	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
+	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey, tlsConfig)
 	if err != nil {
 		return nil, err
 	}
@@ -92,14 +87,23 @@ func (lb *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint st
 		return nil, err
 	}
 
-	return &client.IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
+	return &IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
 }
 
-func (lb *YandexCloudCreator) Now() time.Time {
-	return time.Now()
+func tlsConfig(caCertificate []byte) (*tls.Config, error) {
+	tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
+	if caCertificate != nil {
+		caCertPool := x509.NewCertPool()
+		ok := caCertPool.AppendCertsFromPEM(caCertificate)
+		if !ok {
+			return nil, errors.New("unable to read trusted CA certificates")
+		}
+		tlsConfig.RootCAs = caCertPool
+	}
+	return tlsConfig, nil
 }
 
-func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*ycsdk.SDK, error) {
+func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, tlsConfig *tls.Config) (*ycsdk.SDK, error) {
 	creds, err := ycsdk.ServiceAccountKey(authorizedKey)
 	if err != nil {
 		return nil, err
@@ -108,6 +112,7 @@ func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key
 	sdk, err := ycsdk.Build(ctx, ycsdk.Config{
 		Credentials: creds,
 		Endpoint:    apiEndpoint,
+		TLSConfig:   tlsConfig,
 	})
 	if err != nil {
 		return nil, err
@@ -120,34 +125,14 @@ func closeSDK(ctx context.Context, sdk *ycsdk.SDK) error {
 	return sdk.Shutdown(ctx)
 }
 
-// Implementation of LockboxClient.
-type LockboxClient struct {
-	lockboxPayloadClient lockbox.PayloadServiceClient
-}
-
-func (lc *LockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
-	payload, err := lc.lockboxPayloadClient.Get(
-		ctx,
-		&lockbox.GetPayloadRequest{
-			SecretId:  secretID,
-			VersionId: versionID,
-		},
-		grpc.PerRPCCredentials(perRPCCredentials{iamToken: iamToken}),
-	)
-	if err != nil {
-		return nil, err
-	}
-	return payload.Entries, nil
-}
-
-type perRPCCredentials struct {
-	iamToken string
+type PerRPCCredentials struct {
+	IamToken string
 }
 
-func (t perRPCCredentials) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
-	return map[string]string{"Authorization": "Bearer " + t.iamToken}, nil
+func (t PerRPCCredentials) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
+	return map[string]string{"Authorization": "Bearer " + t.IamToken}, nil
 }
 
-func (perRPCCredentials) RequireTransportSecurity() bool {
+func (PerRPCCredentials) RequireTransportSecurity() bool {
 	return true
 }

+ 24 - 0
pkg/provider/yandex/common/secretgetter.go

@@ -0,0 +1,24 @@
+/*
+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 common
+
+import (
+	"context"
+)
+
+// Adapts the secrets received from a remote Yandex.Cloud service for the format expected by v1beta1.SecretsClient.
+type SecretGetter interface {
+	GetSecret(ctx context.Context, iamToken, resourceID, versionID, property string) ([]byte, error)
+	GetSecretMap(ctx context.Context, iamToken, resourceID, versionID string) (map[string][]byte, error)
+}

+ 51 - 0
pkg/provider/yandex/common/secretsclient.go

@@ -0,0 +1,51 @@
+/*
+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 common
+
+import (
+	"context"
+	"fmt"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.SecretsClient = &yandexCloudSecretsClient{}
+
+// Implementation of v1beta1.SecretsClient.
+type yandexCloudSecretsClient struct {
+	secretGetter SecretGetter
+	iamToken     string
+}
+
+func (c *yandexCloudSecretsClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	// TO be implemented
+	return nil, fmt.Errorf("GetAllSecrets not supported")
+}
+
+func (c *yandexCloudSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	return c.secretGetter.GetSecret(ctx, c.iamToken, ref.Key, ref.Version, ref.Property)
+}
+
+func (c *yandexCloudSecretsClient) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	return c.secretGetter.GetSecretMap(ctx, c.iamToken, ref.Key, ref.Version)
+}
+
+func (c *yandexCloudSecretsClient) Close(ctx context.Context) error {
+	return nil
+}
+
+func (c *yandexCloudSecretsClient) Validate() (esv1beta1.ValidationResult, error) {
+	return esv1beta1.ValidationResultReady, nil
+}

+ 3 - 17
pkg/provider/yandex/lockbox/client/client.go

@@ -15,25 +15,11 @@ package client
 
 import (
 	"context"
-	"time"
 
-	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
-	"github.com/yandex-cloud/go-sdk/iamkey"
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
 )
 
-// Creates Lockbox clients and Yandex.Cloud IAM tokens.
-type YandexCloudCreator interface {
-	CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (LockboxClient, error)
-	CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*IamToken, error)
-	Now() time.Time
-}
-
-type IamToken struct {
-	Token     string
-	ExpiresAt time.Time
-}
-
-// Responsible for accessing Lockbox secrets.
+// Requests the payload of the given secret from Lockbox.
 type LockboxClient interface {
-	GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error)
+	GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*api.Payload_Entry, error)
 }

+ 0 - 152
pkg/provider/yandex/lockbox/client/fake/fake.go

@@ -1,152 +0,0 @@
-/*
-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 fake
-
-import (
-	"context"
-	"fmt"
-	"time"
-
-	"github.com/google/go-cmp/cmp"
-	"github.com/google/go-cmp/cmp/cmpopts"
-	"github.com/google/uuid"
-	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
-	"github.com/yandex-cloud/go-sdk/iamkey"
-
-	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
-)
-
-// Fake implementation of YandexCloudCreator.
-type YandexCloudCreator struct {
-	Backend *LockboxBackend
-}
-
-func (c *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (client.LockboxClient, error) {
-	return &LockboxClient{c.Backend}, nil
-}
-
-func (c *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
-	return c.Backend.getToken(authorizedKey)
-}
-
-func (c *YandexCloudCreator) Now() time.Time {
-	return c.Backend.now
-}
-
-// Fake implementation of LockboxClient.
-type LockboxClient struct {
-	fakeLockboxBackend *LockboxBackend
-}
-
-func (c *LockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
-	return c.fakeLockboxBackend.getEntries(iamToken, secretID, versionID)
-}
-
-// Fakes Yandex Lockbox service backend.
-type LockboxBackend struct {
-	secretMap  map[secretKey]secretValue   // secret specific data
-	versionMap map[versionKey]versionValue // version specific data
-	tokenMap   map[tokenKey]tokenValue     // token specific data
-
-	tokenExpirationDuration time.Duration
-	now                     time.Time // fakes the current time
-}
-
-type secretKey struct {
-	secretID string
-}
-
-type secretValue struct {
-	expectedAuthorizedKey *iamkey.Key // authorized key expected to access the secret
-}
-
-type versionKey struct {
-	secretID  string
-	versionID string
-}
-
-type versionValue struct {
-	entries []*lockbox.Payload_Entry
-}
-
-type tokenKey struct {
-	token string
-}
-
-type tokenValue struct {
-	authorizedKey *iamkey.Key
-	expiresAt     time.Time
-}
-
-func NewLockboxBackend(tokenExpirationDuration time.Duration) *LockboxBackend {
-	return &LockboxBackend{
-		secretMap:               make(map[secretKey]secretValue),
-		versionMap:              make(map[versionKey]versionValue),
-		tokenMap:                make(map[tokenKey]tokenValue),
-		tokenExpirationDuration: tokenExpirationDuration,
-		now:                     time.Time{},
-	}
-}
-
-func (lb *LockboxBackend) CreateSecret(authorizedKey *iamkey.Key, entries ...*lockbox.Payload_Entry) (string, string) {
-	secretID := uuid.NewString()
-	versionID := uuid.NewString()
-
-	lb.secretMap[secretKey{secretID}] = secretValue{authorizedKey}
-	lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
-	lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
-
-	return secretID, versionID
-}
-
-func (lb *LockboxBackend) AddVersion(secretID string, entries ...*lockbox.Payload_Entry) string {
-	versionID := uuid.NewString()
-
-	lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
-	lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
-
-	return versionID
-}
-
-func (lb *LockboxBackend) AdvanceClock(duration time.Duration) {
-	lb.now = lb.now.Add(duration)
-}
-
-func (lb *LockboxBackend) getToken(authorizedKey *iamkey.Key) (*client.IamToken, error) {
-	token := uuid.NewString()
-	expiresAt := lb.now.Add(lb.tokenExpirationDuration)
-	lb.tokenMap[tokenKey{token}] = tokenValue{authorizedKey, expiresAt}
-	return &client.IamToken{Token: token, ExpiresAt: expiresAt}, nil
-}
-
-func (lb *LockboxBackend) getEntries(iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
-	if _, ok := lb.secretMap[secretKey{secretID}]; !ok {
-		return nil, fmt.Errorf("secret not found")
-	}
-	if _, ok := lb.versionMap[versionKey{secretID, versionID}]; !ok {
-		return nil, fmt.Errorf("version not found")
-	}
-	if _, ok := lb.tokenMap[tokenKey{iamToken}]; !ok {
-		return nil, fmt.Errorf("unauthenticated")
-	}
-
-	if lb.tokenMap[tokenKey{iamToken}].expiresAt.Before(lb.now) {
-		return nil, fmt.Errorf("iam token expired")
-	}
-	if !cmp.Equal(lb.tokenMap[tokenKey{iamToken}].authorizedKey, lb.secretMap[secretKey{secretID}].expectedAuthorizedKey, cmpopts.IgnoreUnexported(iamkey.Key{})) {
-		return nil, fmt.Errorf("permission denied")
-	}
-
-	return lb.versionMap[versionKey{secretID, versionID}].entries, nil
-}

+ 136 - 0
pkg/provider/yandex/lockbox/client/fakeclient.go

@@ -0,0 +1,136 @@
+/*
+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 client
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/google/uuid"
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+)
+
+// Fake implementation of LockboxClient.
+type fakeLockboxClient struct {
+	fakeLockboxServer *FakeLockboxServer
+}
+
+func NewFakeLockboxClient(fakeLockboxServer *FakeLockboxServer) LockboxClient {
+	return &fakeLockboxClient{fakeLockboxServer}
+}
+
+func (c *fakeLockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*api.Payload_Entry, error) {
+	return c.fakeLockboxServer.getEntries(iamToken, secretID, versionID)
+}
+
+// Fakes Yandex Lockbox service backend.
+type FakeLockboxServer struct {
+	secretMap  map[secretKey]secretValue   // secret specific data
+	versionMap map[versionKey]versionValue // version specific data
+	tokenMap   map[tokenKey]tokenValue     // token specific data
+
+	tokenExpirationDuration time.Duration
+	clock                   clock.Clock
+}
+
+type secretKey struct {
+	secretID string
+}
+
+type secretValue struct {
+	expectedAuthorizedKey *iamkey.Key // authorized key expected to access the secret
+}
+
+type versionKey struct {
+	secretID  string
+	versionID string
+}
+
+type versionValue struct {
+	entries []*api.Payload_Entry
+}
+
+type tokenKey struct {
+	token string
+}
+
+type tokenValue struct {
+	authorizedKey *iamkey.Key
+	expiresAt     time.Time
+}
+
+func NewFakeLockboxServer(clock clock.Clock, tokenExpirationDuration time.Duration) *FakeLockboxServer {
+	return &FakeLockboxServer{
+		secretMap:               make(map[secretKey]secretValue),
+		versionMap:              make(map[versionKey]versionValue),
+		tokenMap:                make(map[tokenKey]tokenValue),
+		tokenExpirationDuration: tokenExpirationDuration,
+		clock:                   clock,
+	}
+}
+
+func (s *FakeLockboxServer) CreateSecret(authorizedKey *iamkey.Key, entries ...*api.Payload_Entry) (string, string) {
+	secretID := uuid.NewString()
+	versionID := uuid.NewString()
+
+	s.secretMap[secretKey{secretID}] = secretValue{authorizedKey}
+	s.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
+	s.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
+
+	return secretID, versionID
+}
+
+func (s *FakeLockboxServer) AddVersion(secretID string, entries ...*api.Payload_Entry) string {
+	versionID := uuid.NewString()
+
+	s.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
+	s.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
+
+	return versionID
+}
+
+func (s *FakeLockboxServer) NewIamToken(authorizedKey *iamkey.Key) *common.IamToken {
+	token := uuid.NewString()
+	expiresAt := s.clock.CurrentTime().Add(s.tokenExpirationDuration)
+	s.tokenMap[tokenKey{token}] = tokenValue{authorizedKey, expiresAt}
+	return &common.IamToken{Token: token, ExpiresAt: expiresAt}
+}
+
+func (s *FakeLockboxServer) getEntries(iamToken, secretID, versionID string) ([]*api.Payload_Entry, error) {
+	if _, ok := s.secretMap[secretKey{secretID}]; !ok {
+		return nil, fmt.Errorf("secret not found")
+	}
+	if _, ok := s.versionMap[versionKey{secretID, versionID}]; !ok {
+		return nil, fmt.Errorf("version not found")
+	}
+	if _, ok := s.tokenMap[tokenKey{iamToken}]; !ok {
+		return nil, fmt.Errorf("unauthenticated")
+	}
+
+	if s.tokenMap[tokenKey{iamToken}].expiresAt.Before(s.clock.CurrentTime()) {
+		return nil, fmt.Errorf("iam token expired")
+	}
+	if !cmp.Equal(s.tokenMap[tokenKey{iamToken}].authorizedKey, s.secretMap[secretKey{secretID}].expectedAuthorizedKey, cmpopts.IgnoreUnexported(iamkey.Key{})) {
+		return nil, fmt.Errorf("permission denied")
+	}
+
+	return s.versionMap[versionKey{secretID, versionID}].entries, nil
+}

+ 58 - 0
pkg/provider/yandex/lockbox/client/grpcclient.go

@@ -0,0 +1,58 @@
+/*
+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 client
+
+import (
+	"context"
+
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	"google.golang.org/grpc"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+)
+
+// Real/gRPC implementation of LockboxClient.
+type grpcLockboxClient struct {
+	lockboxPayloadClient api.PayloadServiceClient
+}
+
+func NewGrpcLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (LockboxClient, error) {
+	conn, err := common.NewGrpcConnection(
+		ctx,
+		apiEndpoint,
+		"lockbox-payload", // taken from https://api.cloud.yandex.net/endpoints
+		authorizedKey,
+		caCertificate,
+	)
+	if err != nil {
+		return nil, err
+	}
+	return &grpcLockboxClient{api.NewPayloadServiceClient(conn)}, nil
+}
+
+func (c *grpcLockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*api.Payload_Entry, error) {
+	payload, err := c.lockboxPayloadClient.Get(
+		ctx,
+		&api.GetPayloadRequest{
+			SecretId:  secretID,
+			VersionId: versionID,
+		},
+		grpc.PerRPCCredentials(common.PerRPCCredentials{IamToken: iamToken}),
+	)
+	if err != nil {
+		return nil, err
+	}
+	return payload.Entries, nil
+}

+ 24 - 286
pkg/provider/yandex/lockbox/lockbox.go

@@ -15,326 +15,64 @@ package lockbox
 
 import (
 	"context"
-	"crypto/sha256"
-	"encoding/hex"
-	"encoding/json"
 	"fmt"
-	"sync"
 	"time"
 
-	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
 	"github.com/yandex-cloud/go-sdk/iamkey"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
-	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
 	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
-	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/grpc"
 )
 
-const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
-const iamTokenCleanupDelay = 1 * time.Hour       // specifies how often cleanUpIamTokenMap() is performed
-
 var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox")
 
-type iamTokenKey struct {
-	authorizedKeyID  string
-	serviceAccountID string
-	privateKeyHash   string
-}
-
-// https://github.com/external-secrets/external-secrets/issues/644
-var _ esv1beta1.SecretsClient = &lockboxSecretsClient{}
-var _ esv1beta1.Provider = &lockboxProvider{}
-
-// lockboxProvider is a provider for Yandex Lockbox.
-type lockboxProvider struct {
-	yandexCloudCreator client.YandexCloudCreator
-
-	lockboxClientMap      map[string]client.LockboxClient // apiEndpoint -> LockboxClient
-	lockboxClientMapMutex sync.Mutex
-	iamTokenMap           map[iamTokenKey]*client.IamToken
-	iamTokenMapMutex      sync.Mutex
-}
-
-func newLockboxProvider(yandexCloudCreator client.YandexCloudCreator) *lockboxProvider {
-	return &lockboxProvider{
-		yandexCloudCreator: yandexCloudCreator,
-		lockboxClientMap:   make(map[string]client.LockboxClient),
-		iamTokenMap:        make(map[iamTokenKey]*client.IamToken),
-	}
-}
-
-// NewClient constructs a Yandex Lockbox Provider.
-func (p *lockboxProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+func adaptInput(store esv1beta1.GenericStore) (*common.SecretsClientInput, error) {
 	storeSpec := store.GetSpec()
 	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
 		return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
 	}
 	storeSpecYandexLockbox := storeSpec.Provider.YandexLockbox
 
-	authorizedKeySecretName := storeSpecYandexLockbox.Auth.AuthorizedKey.Name
-	if authorizedKeySecretName == "" {
+	if storeSpecYandexLockbox.Auth.AuthorizedKey.Name == "" {
 		return nil, fmt.Errorf("invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
 	}
-	objectKey := types.NamespacedName{
-		Name:      authorizedKeySecretName,
-		Namespace: namespace,
-	}
-
-	// only ClusterStore is allowed to set namespace (and then it's required)
-	if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
-		if storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace == nil {
-			return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
-		}
-		objectKey.Namespace = *storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace
-	}
-
-	authorizedKeySecret := &corev1.Secret{}
-	err := kube.Get(ctx, objectKey, authorizedKeySecret)
-	if err != nil {
-		return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
-	}
-
-	authorizedKeySecretData := authorizedKeySecret.Data[storeSpecYandexLockbox.Auth.AuthorizedKey.Key]
-	if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
-		return nil, fmt.Errorf("missing AuthorizedKey")
-	}
-
-	var authorizedKey iamkey.Key
-	err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
-	if err != nil {
-		return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
-	}
-
-	var caCertificateData []byte
 
+	var caCertificate *esmeta.SecretKeySelector
 	if storeSpecYandexLockbox.CAProvider != nil {
-		certObjectKey := types.NamespacedName{
-			Name:      storeSpecYandexLockbox.CAProvider.Certificate.Name,
-			Namespace: namespace,
-		}
-
-		if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
-			if storeSpecYandexLockbox.CAProvider.Certificate.Namespace == nil {
-				return nil, fmt.Errorf("invalid ClusterSecretStore: missing CA certificate Namespace")
-			}
-			certObjectKey.Namespace = *storeSpecYandexLockbox.CAProvider.Certificate.Namespace
-		}
-
-		caCertificateSecret := &corev1.Secret{}
-		err := kube.Get(ctx, certObjectKey, caCertificateSecret)
-		if err != nil {
-			return nil, fmt.Errorf("could not fetch CA certificate secret: %w", err)
-		}
-
-		caCertificateData = caCertificateSecret.Data[storeSpecYandexLockbox.CAProvider.Certificate.Key]
-		if (caCertificateData == nil) || (len(caCertificateData) == 0) {
-			return nil, fmt.Errorf("missing CA Certificate")
-		}
-	}
-
-	lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey, caCertificateData)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create Yandex Lockbox client: %w", err)
-	}
-
-	iamToken, err := p.getOrCreateIamToken(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
-	if err != nil {
-		return nil, fmt.Errorf("failed to create IAM token: %w", err)
-	}
-
-	return &lockboxSecretsClient{lockboxClient, iamToken.Token}, nil
-}
-
-func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (client.LockboxClient, error) {
-	p.lockboxClientMapMutex.Lock()
-	defer p.lockboxClientMapMutex.Unlock()
-
-	if _, ok := p.lockboxClientMap[apiEndpoint]; !ok {
-		log.Info("creating LockboxClient", "apiEndpoint", apiEndpoint)
-
-		lockboxClient, err := p.yandexCloudCreator.CreateLockboxClient(ctx, apiEndpoint, authorizedKey, caCertificate)
-		if err != nil {
-			return nil, err
-		}
-		p.lockboxClientMap[apiEndpoint] = lockboxClient
+		caCertificate = &storeSpecYandexLockbox.CAProvider.Certificate
 	}
-	return p.lockboxClientMap[apiEndpoint], nil
-}
-
-func (p *lockboxProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
-	p.iamTokenMapMutex.Lock()
-	defer p.iamTokenMapMutex.Unlock()
 
-	iamTokenKey := buildIamTokenKey(authorizedKey)
-	if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
-		log.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
-
-		iamToken, err := p.yandexCloudCreator.CreateIamToken(ctx, apiEndpoint, authorizedKey)
-		if err != nil {
-			return nil, err
-		}
-
-		log.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
-
-		p.iamTokenMap[iamTokenKey] = iamToken
-	}
-	return p.iamTokenMap[iamTokenKey], nil
+	return &common.SecretsClientInput{
+		APIEndpoint:   storeSpecYandexLockbox.APIEndpoint,
+		AuthorizedKey: storeSpecYandexLockbox.Auth.AuthorizedKey,
+		CACertificate: caCertificate,
+	}, nil
 }
 
-func (p *lockboxProvider) isIamTokenUsable(iamToken *client.IamToken) bool {
-	now := p.yandexCloudCreator.Now()
-	return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
-}
-
-func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
-	privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
-	return iamTokenKey{
-		authorizedKey.GetId(),
-		authorizedKey.GetServiceAccountId(),
-		hex.EncodeToString(privateKeyHash[:]),
-	}
-}
-
-// Used for testing.
-func (p *lockboxProvider) isIamTokenCached(authorizedKey *iamkey.Key) bool {
-	p.iamTokenMapMutex.Lock()
-	defer p.iamTokenMapMutex.Unlock()
-
-	_, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
-	return ok
-}
-
-func (p *lockboxProvider) cleanUpIamTokenMap() {
-	p.iamTokenMapMutex.Lock()
-	defer p.iamTokenMapMutex.Unlock()
-
-	for key, value := range p.iamTokenMap {
-		if p.yandexCloudCreator.Now().After(value.ExpiresAt) {
-			log.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
-			delete(p.iamTokenMap, key)
-		}
-	}
-}
-
-func (p *lockboxProvider) ValidateStore(store esv1beta1.GenericStore) error {
-	return nil
-}
-
-// lockboxSecretsClient is a secrets client for Yandex Lockbox.
-type lockboxSecretsClient struct {
-	lockboxClient client.LockboxClient
-	iamToken      string
-}
-
-// Empty GetAllSecrets.
-func (c *lockboxSecretsClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
-	// TO be implemented
-	return nil, fmt.Errorf("GetAllSecrets not implemented")
-}
-
-// GetSecret returns a single secret from the provider.
-func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
-	if err != nil {
-		return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
-	}
-
-	if ref.Property == "" {
-		keyToValue := make(map[string]interface{}, len(entries))
-		for _, entry := range entries {
-			value, err := getValueAsIs(entry)
-			if err != nil {
-				return nil, err
-			}
-			keyToValue[entry.Key] = value
-		}
-		out, err := json.Marshal(keyToValue)
-		if err != nil {
-			return nil, fmt.Errorf("failed to marshal secret: %w", err)
-		}
-		return out, nil
-	}
-
-	entry, err := findEntryByKey(entries, ref.Property)
+func newSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (common.SecretGetter, error) {
+	lockboxClient, err := client.NewGrpcLockboxClient(ctx, apiEndpoint, authorizedKey, caCertificate)
 	if err != nil {
 		return nil, err
 	}
-	return getValueAsBinary(entry)
-}
-
-// GetSecretMap returns multiple k/v pairs from the provider.
-func (c *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
-	entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
-	if err != nil {
-		return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
-	}
-
-	secretMap := make(map[string][]byte, len(entries))
-	for _, entry := range entries {
-		value, err := getValueAsBinary(entry)
-		if err != nil {
-			return nil, err
-		}
-		secretMap[entry.Key] = value
-	}
-	return secretMap, nil
-}
-
-func (c *lockboxSecretsClient) Close(ctx context.Context) error {
-	return nil
-}
-
-func (c *lockboxSecretsClient) Validate() (esv1beta1.ValidationResult, error) {
-	return esv1beta1.ValidationResultReady, nil
-}
-
-func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
-	switch entry.Value.(type) {
-	case *lockbox.Payload_Entry_TextValue:
-		return entry.GetTextValue(), nil
-	case *lockbox.Payload_Entry_BinaryValue:
-		return entry.GetBinaryValue(), nil
-	default:
-		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
-	}
-}
-
-func getValueAsBinary(entry *lockbox.Payload_Entry) ([]byte, error) {
-	switch entry.Value.(type) {
-	case *lockbox.Payload_Entry_TextValue:
-		return []byte(entry.GetTextValue()), nil
-	case *lockbox.Payload_Entry_BinaryValue:
-		return entry.GetBinaryValue(), nil
-	default:
-		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
-	}
-}
-
-func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payload_Entry, error) {
-	for i := range entries {
-		if entries[i].Key == key {
-			return entries[i], nil
-		}
-	}
-	return nil, fmt.Errorf("payload entry with key '%s' not found", key)
+	return newLockboxSecretGetter(lockboxClient)
 }
 
 func init() {
-	lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})
-
-	go func() {
-		for {
-			time.Sleep(iamTokenCleanupDelay)
-			lockboxProvider.cleanUpIamTokenMap()
-		}
-	}()
+	provider := common.InitYandexCloudProvider(
+		log,
+		clock.NewRealClock(),
+		adaptInput,
+		newSecretGetter,
+		common.NewIamToken,
+		time.Hour,
+	)
 
 	esv1beta1.Register(
-		lockboxProvider,
+		provider,
 		&esv1beta1.SecretStoreProvider{
 			YandexLockbox: &esv1beta1.YandexLockboxProvider{},
 		},

+ 122 - 143
pkg/provider/yandex/lockbox/lockbox_test.go

@@ -15,11 +15,8 @@ package lockbox
 
 import (
 	"context"
-	"crypto/x509"
-	"crypto/x509/pkix"
 	b64 "encoding/base64"
 	"encoding/json"
-	"math/big"
 	"testing"
 	"time"
 
@@ -29,12 +26,15 @@ import (
 	"github.com/yandex-cloud/go-sdk/iamkey"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"sigs.k8s.io/controller-runtime/pkg/client"
+	ctrl "sigs.k8s.io/controller-runtime"
+	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
 	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/fake"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
 )
 
 const (
@@ -83,7 +83,7 @@ func TestNewClient(t *testing.T) {
 	tassert.EqualError(t, err, "could not fetch AuthorizedKey secret: secrets \"authorizedKeySecretName\" not found")
 	tassert.Nil(t, secretClient)
 
-	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, newFakeAuthorizedKey())
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, newFakeAuthorizedKey()))
 	tassert.Nil(t, err)
 
 	const caCertificateSecretName = "caCertificateSecretName"
@@ -98,10 +98,10 @@ func TestNewClient(t *testing.T) {
 	tassert.EqualError(t, err, "could not fetch CA certificate secret: secrets \"caCertificateSecretName\" not found")
 	tassert.Nil(t, secretClient)
 
-	err = createK8sSecret(ctx, k8sClient, namespace, caCertificateSecretName, caCertificateSecretKey, newFakeCACertificate())
+	err = createK8sSecret(ctx, t, k8sClient, namespace, caCertificateSecretName, caCertificateSecretKey, []byte("it-is-not-a-certificate"))
 	tassert.Nil(t, err)
 	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
-	tassert.EqualError(t, err, "failed to create Yandex Lockbox client: private key parsing failed: invalid key: Key must be a PEM encoded PKCS1 or PKCS8 key")
+	tassert.EqualError(t, err, "failed to create Yandex.Cloud client: unable to read trusted CA certificates")
 	tassert.Nil(t, secretClient)
 }
 
@@ -110,10 +110,11 @@ func TestGetSecretForAllEntries(t *testing.T) {
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(k1, v1),
 		binaryEntry(k2, v2),
 	)
@@ -121,13 +122,11 @@ func TestGetSecretForAllEntries(t *testing.T) {
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
@@ -148,10 +147,11 @@ func TestGetSecretForTextEntry(t *testing.T) {
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(k1, v1),
 		binaryEntry(k2, v2),
 	)
@@ -159,13 +159,11 @@ func TestGetSecretForTextEntry(t *testing.T) {
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
@@ -179,10 +177,11 @@ func TestGetSecretForBinaryEntry(t *testing.T) {
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(k1, v1),
 		binaryEntry(k2, v2),
 	)
@@ -190,13 +189,11 @@ func TestGetSecretForBinaryEntry(t *testing.T) {
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k2})
@@ -210,22 +207,21 @@ func TestGetSecretByVersionID(t *testing.T) {
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	oldKey, oldVal := "oldKey", "oldVal"
-	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, oldVersionID := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(oldKey, oldVal),
 	)
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
@@ -234,7 +230,7 @@ func TestGetSecretByVersionID(t *testing.T) {
 	tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
 
 	newKey, newVal := "newKey", "newVal"
-	newVersionID := lockboxBackend.AddVersion(secretID,
+	newVersionID := fakeLockboxServer.AddVersion(secretID,
 		textEntry(newKey, newVal),
 	)
 
@@ -253,21 +249,20 @@ func TestGetSecretUnauthorized(t *testing.T) {
 	authorizedKeyA := newFakeAuthorizedKey()
 	authorizedKeyB := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKeyA,
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKeyA,
 		textEntry("k1", "v1"),
 	)
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKeyB)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKeyB))
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
@@ -279,24 +274,23 @@ func TestGetSecretNotFound(t *testing.T) {
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
 	tassert.EqualError(t, err, errSecretPayloadNotFound)
 
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry("k1", "v1"),
 	)
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: "no-version-with-this-id"})
@@ -310,29 +304,28 @@ func TestGetSecretWithTwoNamespaces(t *testing.T) {
 	authorizedKey1 := newFakeAuthorizedKey()
 	authorizedKey2 := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
-	secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
+	secretID1, _ := fakeLockboxServer.CreateSecret(authorizedKey1,
 		textEntry(k1, v1),
 	)
 	k2, v2 := "k2", "v2"
-	secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
+	secretID2, _ := fakeLockboxServer.CreateSecret(authorizedKey2,
 		textEntry(k2, v2),
 	)
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey1)
+	err := createK8sSecret(ctx, t, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey1))
 	tassert.Nil(t, err)
-	err = createK8sSecret(ctx, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey2)
+	err = createK8sSecret(ctx, t, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey2))
 	tassert.Nil(t, err)
 	store1 := newYandexLockboxSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
 	store2 := newYandexLockboxSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient1, err := provider.NewClient(ctx, store1, k8sClient, namespace1)
 	tassert.Nil(t, err)
 	secretsClient2, err := provider.NewClient(ctx, store2, k8sClient, namespace2)
@@ -361,36 +354,33 @@ func TestGetSecretWithTwoApiEndpoints(t *testing.T) {
 	authorizedKey1 := newFakeAuthorizedKey()
 	authorizedKey2 := newFakeAuthorizedKey()
 
-	lockboxBackend1 := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer1 := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
-	secretID1, _ := lockboxBackend1.CreateSecret(authorizedKey1,
+	secretID1, _ := fakeLockboxServer1.CreateSecret(authorizedKey1,
 		textEntry(k1, v1),
 	)
-	lockboxBackend2 := fake.NewLockboxBackend(time.Hour)
+	fakeLockboxServer2 := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k2, v2 := "k2", "v2"
-	secretID2, _ := lockboxBackend2.CreateSecret(authorizedKey2,
+	secretID2, _ := fakeLockboxServer2.CreateSecret(authorizedKey2,
 		textEntry(k2, v2),
 	)
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName1 = "authorizedKeySecretName1"
 	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJSON(t, authorizedKey1))
 	tassert.Nil(t, err)
 	const authorizedKeySecretName2 = "authorizedKeySecretName2"
 	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
-	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, toJSON(t, authorizedKey2))
 	tassert.Nil(t, err)
 
 	store1 := newYandexLockboxSecretStore(apiEndpoint1, namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
 	store2 := newYandexLockboxSecretStore(apiEndpoint2, namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
 
-	provider1 := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend1,
-	})
-	provider2 := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend2,
-	})
+	provider1 := newLockboxProvider(fakeClock, fakeLockboxServer1)
+	provider2 := newLockboxProvider(fakeClock, fakeLockboxServer2)
 
 	secretsClient1, err := provider1.NewClient(ctx, store1, k8sClient, namespace)
 	tassert.Nil(t, err)
@@ -419,23 +409,22 @@ func TestGetSecretWithIamTokenExpiration(t *testing.T) {
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 
+	fakeClock := clock.NewFakeClock()
 	tokenExpirationTime := time.Hour
-	lockboxBackend := fake.NewLockboxBackend(tokenExpirationTime)
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, tokenExpirationTime)
 	k1, v1 := "k1", "v1"
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(k1, v1),
 	)
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 
 	var data []byte
 
@@ -445,7 +434,7 @@ func TestGetSecretWithIamTokenExpiration(t *testing.T) {
 	tassert.Equal(t, v1, string(data))
 	tassert.Nil(t, err)
 
-	lockboxBackend.AdvanceClock(2 * tokenExpirationTime)
+	fakeClock.AddDuration(2 * tokenExpirationTime)
 
 	data, err = oldSecretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
 	tassert.Nil(t, data)
@@ -464,12 +453,13 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
 	authorizedKey1 := newFakeAuthorizedKey()
 	authorizedKey2 := newFakeAuthorizedKey()
 
+	fakeClock := clock.NewFakeClock()
 	tokenExpirationDuration := time.Hour
-	lockboxBackend := fake.NewLockboxBackend(tokenExpirationDuration)
-	secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, tokenExpirationDuration)
+	secretID1, _ := fakeLockboxServer.CreateSecret(authorizedKey1,
 		textEntry("k1", "v1"),
 	)
-	secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
+	secretID2, _ := fakeLockboxServer.CreateSecret(authorizedKey2,
 		textEntry("k2", "v2"),
 	)
 
@@ -478,22 +468,20 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName1 = "authorizedKeySecretName1"
 	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
-	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJSON(t, authorizedKey1))
 	tassert.Nil(t, err)
 	const authorizedKeySecretName2 = "authorizedKeySecretName2"
 	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
-	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
+	err = createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, toJSON(t, authorizedKey2))
 	tassert.Nil(t, err)
 
 	store1 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
 	store2 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 
-	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
 
 	// Access secretID1 with authorizedKey1, IAM token for authorizedKey1 should be cached
 	secretsClient, err := provider.NewClient(ctx, store1, k8sClient, namespace)
@@ -501,10 +489,10 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID1})
 	tassert.Nil(t, err)
 
-	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
 
-	lockboxBackend.AdvanceClock(tokenExpirationDuration * 2)
+	fakeClock.AddDuration(tokenExpirationDuration * 2)
 
 	// Access secretID2 with authorizedKey2, IAM token for authorizedKey2 should be cached
 	secretsClient, err = provider.NewClient(ctx, store2, k8sClient, namespace)
@@ -512,28 +500,28 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
 	_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID2})
 	tassert.Nil(t, err)
 
-	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
 
-	lockboxBackend.AdvanceClock(tokenExpirationDuration)
+	fakeClock.AddDuration(tokenExpirationDuration)
 
-	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
 
-	provider.cleanUpIamTokenMap()
+	provider.CleanUpIamTokenMap()
 
-	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
 
-	lockboxBackend.AdvanceClock(tokenExpirationDuration)
+	fakeClock.AddDuration(tokenExpirationDuration)
 
-	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
 
-	provider.cleanUpIamTokenMap()
+	provider.CleanUpIamTokenMap()
 
-	tassert.False(t, provider.isIamTokenCached(authorizedKey1))
-	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
 }
 
 func TestGetSecretMap(t *testing.T) {
@@ -541,10 +529,11 @@ func TestGetSecretMap(t *testing.T) {
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	k1, v1 := "k1", "v1"
 	k2, v2 := "k2", []byte("v2")
-	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(k1, v1),
 		binaryEntry(k2, v2),
 	)
@@ -552,13 +541,11 @@ func TestGetSecretMap(t *testing.T) {
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
@@ -579,22 +566,21 @@ func TestGetSecretMapByVersionID(t *testing.T) {
 	namespace := uuid.NewString()
 	authorizedKey := newFakeAuthorizedKey()
 
-	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	fakeClock := clock.NewFakeClock()
+	fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
 	oldKey, oldVal := "oldKey", "oldVal"
-	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
+	secretID, oldVersionID := fakeLockboxServer.CreateSecret(authorizedKey,
 		textEntry(oldKey, oldVal),
 	)
 
 	k8sClient := clientfake.NewClientBuilder().Build()
 	const authorizedKeySecretName = "authorizedKeySecretName"
 	const authorizedKeySecretKey = "authorizedKeySecretKey"
-	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	err := createK8sSecret(ctx, t, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJSON(t, authorizedKey))
 	tassert.Nil(t, err)
 	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
 
-	provider := newLockboxProvider(&fake.YandexCloudCreator{
-		Backend: lockboxBackend,
-	})
+	provider := newLockboxProvider(fakeClock, fakeLockboxServer)
 	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
 	tassert.Nil(t, err)
 	data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
@@ -603,7 +589,7 @@ func TestGetSecretMapByVersionID(t *testing.T) {
 	tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
 
 	newKey, newVal := "newKey", "newVal"
-	newVersionID := lockboxBackend.AddVersion(secretID,
+	newVersionID := fakeLockboxServer.AddVersion(secretID,
 		textEntry(newKey, newVal),
 	)
 
@@ -618,6 +604,21 @@ func TestGetSecretMapByVersionID(t *testing.T) {
 
 // helper functions
 
+func newLockboxProvider(clock clock.Clock, fakeLockboxServer *client.FakeLockboxServer) *common.YandexCloudProvider {
+	return common.InitYandexCloudProvider(
+		ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox"),
+		clock,
+		adaptInput,
+		func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (common.SecretGetter, error) {
+			return newLockboxSecretGetter(client.NewFakeLockboxClient(fakeLockboxServer))
+		},
+		func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*common.IamToken, error) {
+			return fakeLockboxServer.NewIamToken(authorizedKey), nil
+		},
+		0,
+	)
+}
+
 func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1beta1.GenericStore {
 	return &esv1beta1.SecretStore{
 		ObjectMeta: metav1.ObjectMeta{
@@ -639,23 +640,21 @@ func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName
 	}
 }
 
-func createK8sSecret(ctx context.Context, k8sClient client.Client, namespace, secretName, secretKey string, secretContent interface{}) error {
-	data, err := json.Marshal(secretContent)
-	if err != nil {
-		return err
-	}
+func toJSON(t *testing.T, v interface{}) []byte {
+	jsonBytes, err := json.Marshal(v)
+	tassert.Nil(t, err)
+	return jsonBytes
+}
 
-	err = k8sClient.Create(ctx, &corev1.Secret{
+func createK8sSecret(ctx context.Context, t *testing.T, k8sClient k8sclient.Client, namespace, secretName, secretKey string, secretValue []byte) error {
+	err := k8sClient.Create(ctx, &corev1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: namespace,
 			Name:      secretName,
 		},
-		Data: map[string][]byte{secretKey: data},
+		Data: map[string][]byte{secretKey: secretValue},
 	})
-	if err != nil {
-		return err
-	}
-
+	tassert.Nil(t, err)
 	return nil
 }
 
@@ -670,26 +669,6 @@ func newFakeAuthorizedKey() *iamkey.Key {
 	}
 }
 
-func newFakeCACertificate() []byte {
-	cert := x509.Certificate{
-		SerialNumber: big.NewInt(2019),
-		Subject: pkix.Name{
-			Organization:  []string{"Company, INC."},
-			Country:       []string{"US"},
-			Locality:      []string{"San Francisco"},
-			StreetAddress: []string{"Golden Gate Bridge"},
-			PostalCode:    []string{"94016"},
-		},
-		NotBefore:             time.Now(),
-		NotAfter:              time.Now().AddDate(10, 0, 0),
-		IsCA:                  true,
-		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
-		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
-		BasicConstraintsValid: true,
-	}
-	return cert.Raw
-}
-
 func textEntry(key, value string) *lockbox.Payload_Entry {
 	return &lockbox.Payload_Entry{
 		Key: key,

+ 113 - 0
pkg/provider/yandex/lockbox/lockboxsecretgetter.go

@@ -0,0 +1,113 @@
+/*
+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 lockbox
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
+)
+
+// Implementation of common.SecretGetter.
+type lockboxSecretGetter struct {
+	lockboxClient client.LockboxClient
+}
+
+func newLockboxSecretGetter(lockboxClient client.LockboxClient) (common.SecretGetter, error) {
+	return &lockboxSecretGetter{
+		lockboxClient: lockboxClient,
+	}, nil
+}
+
+func (g *lockboxSecretGetter) GetSecret(ctx context.Context, iamToken, resourceID, versionID, property string) ([]byte, error) {
+	entries, err := g.lockboxClient.GetPayloadEntries(ctx, iamToken, resourceID, versionID)
+	if err != nil {
+		return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
+	}
+
+	if property == "" {
+		keyToValue := make(map[string]interface{}, len(entries))
+		for _, entry := range entries {
+			value, err := getValueAsIs(entry)
+			if err != nil {
+				return nil, err
+			}
+			keyToValue[entry.Key] = value
+		}
+		out, err := json.Marshal(keyToValue)
+		if err != nil {
+			return nil, fmt.Errorf("failed to marshal secret: %w", err)
+		}
+		return out, nil
+	}
+
+	entry, err := findEntryByKey(entries, property)
+	if err != nil {
+		return nil, err
+	}
+	return getValueAsBinary(entry)
+}
+
+func (g *lockboxSecretGetter) GetSecretMap(ctx context.Context, iamToken, resourceID, versionID string) (map[string][]byte, error) {
+	entries, err := g.lockboxClient.GetPayloadEntries(ctx, iamToken, resourceID, versionID)
+	if err != nil {
+		return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
+	}
+
+	secretMap := make(map[string][]byte, len(entries))
+	for _, entry := range entries {
+		value, err := getValueAsBinary(entry)
+		if err != nil {
+			return nil, err
+		}
+		secretMap[entry.Key] = value
+	}
+	return secretMap, nil
+}
+
+func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
+	switch entry.Value.(type) {
+	case *lockbox.Payload_Entry_TextValue:
+		return entry.GetTextValue(), nil
+	case *lockbox.Payload_Entry_BinaryValue:
+		return entry.GetBinaryValue(), nil
+	default:
+		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
+	}
+}
+
+func getValueAsBinary(entry *lockbox.Payload_Entry) ([]byte, error) {
+	switch entry.Value.(type) {
+	case *lockbox.Payload_Entry_TextValue:
+		return []byte(entry.GetTextValue()), nil
+	case *lockbox.Payload_Entry_BinaryValue:
+		return entry.GetBinaryValue(), nil
+	default:
+		return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
+	}
+}
+
+func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payload_Entry, error) {
+	for i := range entries {
+		if entries[i].Key == key {
+			return entries[i], nil
+		}
+	}
+	return nil, fmt.Errorf("payload entry with key '%s' not found", key)
+}