瀏覽代碼

Merge pull request #323 from AndreyZamyslov/lockbox

Add support for Yandex Lockbox
paul-the-alien[bot] 4 年之前
父節點
當前提交
ee830e47e3

+ 2 - 0
README.md

@@ -17,6 +17,7 @@ Multiple people and organizations are joining efforts to create a single Externa
 - [Google Cloud Secrets Manager](https://external-secrets.io/provider-google-secrets-manager/)
 - [Azure Key Vault](https://external-secrets.io/provider-azure-key-vault/)
 - [IBM Cloud Secrets Manager](https://external-secrets.io/provider-ibm-secrets-manager/)
+- [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/)
 
 ## Stability and Support Level
 
@@ -35,6 +36,7 @@ Multiple people and organizations are joining efforts to create a single Externa
 | ------------------------------------------------------------------- | :-------: | :----------------------------------------: |
 | [Azure KV](https://external-secrets.io/provider-azure-key-vault/)   |   alpha   | @ahmedmus-1A @asnowfix @ncourbet-1A @1A-mj |
 | [IBM SM](https://external-secrets.io/provider-ibm-secrets-manager/) |   alpha   |   @knelasevero @sebagomez @ricardoptcosta  |
+| [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) |   alpha   |   @AndreyZamyslov @knelasevero         |
 
 ## Documentation
 

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

@@ -53,6 +53,10 @@ type SecretStoreProvider struct {
 	// IBM configures this store to sync secrets using IBM Cloud provider
 	// +optional
 	IBM *IBMProvider `json:"ibm,omitempty"`
+
+	// YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
+	// +optional
+	YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`
 }
 
 type SecretStoreConditionType string

+ 35 - 0
apis/externalsecrets/v1alpha1/secretstore_yandexlockbox_types.go

@@ -0,0 +1,35 @@
+/*
+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 v1alpha1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type YandexLockboxAuth struct {
+	// The authorized key used for authentication
+	// +optional
+	AuthorizedKey esmeta.SecretKeySelector `json:"authorizedKeySecretRef,omitempty"`
+}
+
+// YandexLockboxProvider Configures a store to sync secrets using the Yandex Lockbox provider.
+type YandexLockboxProvider 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 Lockbox
+	Auth YandexLockboxAuth `json:"auth"`
+}

+ 37 - 0
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -644,6 +644,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(IBMProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.YandexLockbox != nil {
+		in, out := &in.YandexLockbox, &out.YandexLockbox
+		*out = new(YandexLockboxProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
@@ -949,3 +954,35 @@ func (in *VaultProvider) DeepCopy() *VaultProvider {
 	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)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxAuth.
+func (in *YandexLockboxAuth) DeepCopy() *YandexLockboxAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexLockboxAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *YandexLockboxProvider) DeepCopyInto(out *YandexLockboxProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxProvider.
+func (in *YandexLockboxProvider) DeepCopy() *YandexLockboxProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexLockboxProvider)
+	in.DeepCopyInto(out)
+	return out
+}

+ 33 - 0
deploy/crds/external-secrets.io_clustersecretstores.yaml

@@ -580,6 +580,39 @@ spec:
                     - path
                     - server
                     type: object
+                  yandexlockbox:
+                    description: YandexLockbox configures this store to sync secrets
+                      using Yandex Lockbox 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 Lockbox
+                        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
+                    required:
+                    - auth
+                    type: object
                 type: object
             required:
             - provider

+ 33 - 0
deploy/crds/external-secrets.io_secretstores.yaml

@@ -580,6 +580,39 @@ spec:
                     - path
                     - server
                     type: object
+                  yandexlockbox:
+                    description: YandexLockbox configures this store to sync secrets
+                      using Yandex Lockbox 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 Lockbox
+                        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
+                    required:
+                    - auth
+                    type: object
                 type: object
             required:
             - provider

+ 86 - 0
docs/provider-yandex-lockbox.md

@@ -0,0 +1,86 @@
+## Yandex Lockbox
+
+External Secrets Operator integrates with [Yandex Lockbox](https://cloud.yandex.com/docs/lockbox/)
+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/v1alpha1
+kind: SecretStore
+metadata:
+  name: secret-store
+spec:
+  provider:
+    yandexlockbox:
+      auth:
+        authorizedKeySecretRef:
+          name: yc-auth
+          key: authorized-key
+```
+
+### Creating external secret
+To make External Secrets Operator sync a k8s secret with a Lockbox secret:
+
+* Create a Lockbox secret, if not already created:
+```bash
+yc lockbox secret create \
+  --name lockbox-secret \
+  --payload '[{"key": "password","textValue": "p@$$w0rd"}]'
+```
+* Assign the [`lockbox.payloadViewer`](https://cloud.yandex.com/docs/lockbox/security/#roles-list) role
+  for accessing the `lockbox-secret` payload to the service account used for authentication:
+```bash
+yc lockbox secret add-access-binding \
+  --name lockbox-secret \
+  --service-account-name eso-service-account \
+  --role lockbox.payloadViewer
+```
+Run the following command to ensure that the correct access binding has been added:
+```bash
+yc lockbox secret list-access-bindings --name lockbox-secret
+```
+* Create an [ExternalSecret](../api-externalsecret/) pointing to `secret-store` and `lockbox-secret`:
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: external-secret
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: secret-store
+    kind: SecretStore
+  target:
+    name: k8s-secret # the target k8s secret name
+  data:
+  - secretKey: password # the target k8s secret key
+    remoteRef:
+      key: ***** # ID of lockbox-secret
+      property: password # (optional) payload entry key of lockbox-secret
+```
+
+The operator will fetch the Yandex Lockbox secret and inject it as a `Kind=Secret`
+```yaml
+kubectl get secret k8s-secret -n <namespace> | -o jsonpath='{.data.password}' | base64 -d
+```

+ 88 - 0
docs/spec.md

@@ -1469,6 +1469,20 @@ IBMProvider
 <p>IBM configures this store to sync secrets using IBM Cloud provider</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>yandexlockbox</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.YandexLockboxProvider">
+YandexLockboxProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>YandexLockbox configures this store to sync secrets using Yandex Lockbox provider</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1alpha1.SecretStoreRef">SecretStoreRef
@@ -2277,6 +2291,80 @@ are used to validate the TLS connection.</p>
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1alpha1.YandexLockboxAuth">YandexLockboxAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.YandexLockboxProvider">YandexLockboxProvider</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/v1alpha1.YandexLockboxProvider">YandexLockboxProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>YandexLockboxProvider Configures a store to sync secrets using the Yandex Lockbox 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/v1alpha1.YandexLockboxAuth">
+YandexLockboxAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth defines the information necessary to authenticate against Yandex Lockbox</p>
+</td>
+</tr>
+</tbody>
+</table>
 <hr/>
 <p><em>
 Generated with <code>gen-crd-api-reference-docs</code>.

+ 3 - 0
go.mod

@@ -64,6 +64,8 @@ require (
 	github.com/spf13/cobra v1.1.3 // indirect
 	github.com/stretchr/testify v1.7.0
 	github.com/tidwall/gjson v1.7.5
+	github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588
+	github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa
 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
 	go.uber.org/zap v1.17.0
 	golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
@@ -72,6 +74,7 @@ require (
 	golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 // indirect
 	google.golang.org/api v0.30.0
 	google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a
+	google.golang.org/grpc v1.31.0
 	honnef.co/go/tools v0.1.4 // indirect
 	k8s.io/api v0.21.2
 	k8s.io/apimachinery v0.21.2

+ 10 - 0
go.sum

@@ -98,6 +98,7 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ
 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
 github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@@ -135,6 +136,7 @@ github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D
 github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
 github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw=
 github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
@@ -166,6 +168,7 @@ github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -479,6 +482,7 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
 github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
 github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
 github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
@@ -645,6 +649,10 @@ github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
 github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588 h1:Lbz8X5Nre0Lg5QgCblmo0AhScWxeN3CVnX+mZ5Hxksk=
+github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
+github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa h1:Un1jWl/YWbK1179aMbsEZ6uLlDjjBAjL8KXldho1Umo=
+github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa/go.mod h1:UkgAKjyQo+Pylt2HTYz/G0PgnxmKOJ9IX/3XiRYQ9Ns=
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
 github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
@@ -763,6 +771,7 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@@ -996,6 +1005,7 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG
 google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200323114720-3f67cca34472/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=

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

@@ -46,6 +46,8 @@ nav:
     - IBM:
       - Secrets Manager: provider-ibm-secrets-manager.md
     - HashiCorp Vault: provider-hashicorp-vault.md
+    - Yandex:
+        - Lockbox: provider-yandex-lockbox.md
   - References:
     - API specification: spec.md
   - Contributing:

+ 1 - 1
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -147,7 +147,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 	}
 
 	defer func() {
-		err = secretClient.Close()
+		err = secretClient.Close(ctx)
 		if err != nil {
 			log.Error(err, errCloseStoreClient)
 		}

+ 1 - 1
pkg/provider/aws/parameterstore/parameterstore.go

@@ -90,6 +90,6 @@ func (pm *ParameterStore) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
 	return secretData, nil
 }
 
-func (pm *ParameterStore) Close() error {
+func (pm *ParameterStore) Close(ctx context.Context) error {
 	return nil
 }

+ 1 - 1
pkg/provider/aws/secretsmanager/secretsmanager.go

@@ -123,6 +123,6 @@ func (sm *SecretsManager) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
 	return secretData, nil
 }
 
-func (sm *SecretsManager) Close() error {
+func (sm *SecretsManager) Close(ctx context.Context) error {
 	return nil
 }

+ 1 - 1
pkg/provider/azure/keyvault/keyvault.go

@@ -227,7 +227,7 @@ func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef sm
 	return value, nil
 }
 
-func (a *Azure) Close() error {
+func (a *Azure) Close(ctx context.Context) error {
 	return nil
 }
 

+ 1 - 1
pkg/provider/fake/fake.go

@@ -74,7 +74,7 @@ func (v *Client) WithGetSecret(secData []byte, err error) *Client {
 func (v *Client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	return v.GetSecretMapFn(ctx, ref)
 }
-func (v *Client) Close() error {
+func (v *Client) Close(ctx context.Context) error {
 	return nil
 }
 

+ 1 - 1
pkg/provider/gcp/secretmanager/secretsmanager.go

@@ -210,7 +210,7 @@ func (sm *ProviderGCP) GetSecretMap(ctx context.Context, ref esv1alpha1.External
 	return secretData, nil
 }
 
-func (sm *ProviderGCP) Close() error {
+func (sm *ProviderGCP) Close(ctx context.Context) error {
 	err := sm.SecretManagerClient.Close()
 	if err != nil {
 		return fmt.Errorf(errClientClose, err)

+ 1 - 1
pkg/provider/ibm/provider.go

@@ -289,7 +289,7 @@ func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1alpha1.Externa
 	}
 }
 
-func (ibm *providerIBM) Close() error {
+func (ibm *providerIBM) Close(ctx context.Context) error {
 	return nil
 }
 

+ 1 - 1
pkg/provider/provider.go

@@ -35,5 +35,5 @@ type SecretsClient interface {
 
 	// GetSecretMap returns multiple k/v pairs from the provider
 	GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
-	Close() error
+	Close(ctx context.Context) error
 }

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

@@ -22,4 +22,5 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
 )

+ 1 - 1
pkg/provider/schema/schema_test.go

@@ -41,7 +41,7 @@ func (p *PP) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretData
 	return map[string][]byte{}, nil
 }
 
-func (p *PP) Close() error {
+func (p *PP) Close(ctx context.Context) error {
 	return nil
 }
 

+ 1 - 1
pkg/provider/vault/vault.go

@@ -155,7 +155,7 @@ func (v *client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecret
 	return v.readSecret(ctx, ref.Key, ref.Version)
 }
 
-func (v *client) Close() error {
+func (v *client) Close(ctx context.Context) error {
 	return nil
 }
 

+ 39 - 0
pkg/provider/yandex/lockbox/client/client.go

@@ -0,0 +1,39 @@
+/*
+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"
+	"time"
+
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+)
+
+// Creates Lockbox clients and Yandex.Cloud IAM tokens.
+type YandexCloudCreator interface {
+	CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (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.
+type LockboxClient interface {
+	GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error)
+}

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

@@ -0,0 +1,151 @@
+/*
+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/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) (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) {
+		return nil, fmt.Errorf("permission denied")
+	}
+
+	return lb.versionMap[versionKey{secretID, versionID}].entries, nil
+}

+ 144 - 0
pkg/provider/yandex/lockbox/client/grpc/grpc.go

@@ -0,0 +1,144 @@
+/*
+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 grpc
+
+import (
+	"context"
+	"crypto/tls"
+	"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 {
+}
+
+func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
+	if err != nil {
+		return nil, err
+	}
+
+	payloadAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
+		ApiEndpointId: "lockbox-payload", // the ID from https://api.cloud.yandex.net/endpoints
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	err = closeSDK(ctx, sdk)
+	if err != nil {
+		return nil, err
+	}
+
+	conn, err := grpc.Dial(payloadAPIEndpoint.Address,
+		grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})),
+		grpc.WithKeepaliveParams(keepalive.ClientParameters{
+			Time:                time.Second * 30,
+			Timeout:             time.Second * 10,
+			PermitWithoutStream: false,
+		}),
+		grpc.WithUserAgent("external-secrets"),
+	)
+	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)
+	if err != nil {
+		return nil, err
+	}
+
+	iamToken, err := sdk.CreateIAMToken(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	err = closeSDK(ctx, sdk)
+	if err != nil {
+		return nil, err
+	}
+
+	return &client.IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
+}
+
+func (lb *YandexCloudCreator) Now() time.Time {
+	return time.Now()
+}
+
+func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*ycsdk.SDK, error) {
+	creds, err := ycsdk.ServiceAccountKey(authorizedKey)
+	if err != nil {
+		return nil, err
+	}
+
+	sdk, err := ycsdk.Build(ctx, ycsdk.Config{
+		Credentials: creds,
+		Endpoint:    apiEndpoint,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return sdk, nil
+}
+
+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
+}
+
+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 {
+	return true
+}

+ 299 - 0
pkg/provider/yandex/lockbox/lockbox.go

@@ -0,0 +1,299 @@
+/*
+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"
+	"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"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/pkg/provider"
+	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"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
+}
+
+// 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 esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, 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 == "" {
+		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 == esv1alpha1.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)
+	}
+
+	lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
+	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) (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)
+		if err != nil {
+			return nil, err
+		}
+		p.lockboxClientMap[apiEndpoint] = lockboxClient
+	}
+	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
+}
+
+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)
+		}
+	}
+}
+
+// lockboxSecretsClient is a secrets client for Yandex Lockbox.
+type lockboxSecretsClient struct {
+	lockboxClient client.LockboxClient
+	iamToken      string
+}
+
+// GetSecret returns a single secret from the provider.
+func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1alpha1.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)
+	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 esv1alpha1.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 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)
+}
+
+func init() {
+	lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})
+
+	go func() {
+		for {
+			time.Sleep(iamTokenCleanupDelay)
+			lockboxProvider.cleanUpIamTokenMap()
+		}
+	}()
+
+	schema.Register(
+		lockboxProvider,
+		&esv1alpha1.SecretStoreProvider{
+			YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
+		},
+	)
+}

+ 677 - 0
pkg/provider/yandex/lockbox/lockbox_test.go

@@ -0,0 +1,677 @@
+/*
+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"
+	b64 "encoding/base64"
+	"encoding/json"
+	"testing"
+	"time"
+
+	"github.com/google/uuid"
+	tassert "github.com/stretchr/testify/assert"
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
+	"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"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/fake"
+)
+
+func TestNewClient(t *testing.T) {
+	ctx := context.Background()
+	const namespace = "namespace"
+
+	store := &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
+			},
+		},
+	}
+	provider, err := schema.GetProvider(store)
+	tassert.Nil(t, err)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	secretClient, err := provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
+	tassert.Nil(t, secretClient)
+
+	store.Spec.Provider.YandexLockbox.Auth = esv1alpha1.YandexLockboxAuth{}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
+	tassert.Nil(t, secretClient)
+
+	store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey = esmeta.SecretKeySelector{}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
+	tassert.Nil(t, secretClient)
+
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey.Name = authorizedKeySecretName
+	store.Spec.Provider.YandexLockbox.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, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, newFakeAuthorizedKey())
+	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 PEM encoded PKCS1 or PKCS8 private key")
+	tassert.Nil(t, secretClient)
+}
+
+func TestGetSecretForAllEntries(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		map[string]string{
+			k1: v1,
+			k2: base64(v2),
+		},
+		unmarshalStringMap(t, data),
+	)
+}
+
+func TestGetSecretForTextEntry(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, v1, string(data))
+}
+
+func TestGetSecretForBinaryEntry(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k2})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, v2, data)
+}
+
+func TestGetSecretByVersionID(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	oldKey, oldVal := "oldKey", "oldVal"
+	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(oldKey, oldVal),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
+
+	newKey, newVal := "newKey", "newVal"
+	newVersionID := lockboxBackend.AddVersion(secretID,
+		textEntry(newKey, newVal),
+	)
+
+	data, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
+
+	data, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: newVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string]string{newKey: newVal}, unmarshalStringMap(t, data))
+}
+
+func TestGetSecretUnauthorized(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKeyA := newFakeAuthorizedKey()
+	authorizedKeyB := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKeyA,
+		textEntry("k1", "v1"),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKeyB)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
+}
+
+func TestGetSecretNotFound(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
+
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry("k1", "v1"),
+	)
+	_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: "no-version-with-this-id"})
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: version not found")
+}
+
+func TestGetSecretWithTwoNamespaces(t *testing.T) {
+	ctx := context.Background()
+	namespace1 := uuid.NewString()
+	namespace2 := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
+		textEntry(k1, v1),
+	)
+	k2, v2 := "k2", "v2"
+	secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
+		textEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey1)
+	tassert.Nil(t, err)
+	err = createK8sSecret(ctx, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey2)
+	tassert.Nil(t, err)
+	store1 := newYandexLockboxSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
+	store2 := newYandexLockboxSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	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, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+	data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
+
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Equal(t, v2, 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()
+
+	lockboxBackend1 := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	secretID1, _ := lockboxBackend1.CreateSecret(authorizedKey1,
+		textEntry(k1, v1),
+	)
+	lockboxBackend2 := fake.NewLockboxBackend(time.Hour)
+	k2, v2 := "k2", "v2"
+	secretID2, _ := lockboxBackend2.CreateSecret(authorizedKey2,
+		textEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, 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,
+	})
+
+	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, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+	data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
+
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
+	data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
+	tassert.Equal(t, v2, string(data))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithIamTokenExpiration(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	tokenExpirationTime := time.Hour
+	lockboxBackend := fake.NewLockboxBackend(tokenExpirationTime)
+	k1, v1 := "k1", "v1"
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+
+	var data []byte
+
+	oldSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err = oldSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+
+	lockboxBackend.AdvanceClock(2 * tokenExpirationTime)
+
+	data, err = oldSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Nil(t, data)
+	tassert.EqualError(t, err, "unable to request secret payload to get secret: iam token expired")
+
+	newSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err = newSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
+	tassert.Equal(t, v1, string(data))
+	tassert.Nil(t, err)
+}
+
+func TestGetSecretWithIamTokenCleanup(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey1 := newFakeAuthorizedKey()
+	authorizedKey2 := newFakeAuthorizedKey()
+
+	tokenExpirationDuration := time.Hour
+	lockboxBackend := fake.NewLockboxBackend(tokenExpirationDuration)
+	secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
+		textEntry("k1", "v1"),
+	)
+	secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
+		textEntry("k2", "v2"),
+	)
+
+	var err error
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
+	tassert.Nil(t, err)
+
+	store1 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
+	store2 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+
+	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, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1})
+	tassert.Nil(t, err)
+
+	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.False(t, provider.isIamTokenCached(authorizedKey2))
+
+	lockboxBackend.AdvanceClock(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, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2})
+	tassert.Nil(t, err)
+
+	tassert.True(t, provider.isIamTokenCached(authorizedKey1))
+	tassert.True(t, provider.isIamTokenCached(authorizedKey2))
+
+	lockboxBackend.AdvanceClock(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))
+
+	lockboxBackend.AdvanceClock(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()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	k1, v1 := "k1", "v1"
+	k2, v2 := "k2", []byte("v2")
+	secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(k1, v1),
+		binaryEntry(k2, v2),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(
+		t,
+		map[string][]byte{
+			k1: []byte(v1),
+			k2: v2,
+		},
+		data,
+	)
+}
+
+func TestGetSecretMapByVersionID(t *testing.T) {
+	ctx := context.Background()
+	namespace := uuid.NewString()
+	authorizedKey := newFakeAuthorizedKey()
+
+	lockboxBackend := fake.NewLockboxBackend(time.Hour)
+	oldKey, oldVal := "oldKey", "oldVal"
+	secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
+		textEntry(oldKey, oldVal),
+	)
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
+	tassert.Nil(t, err)
+	store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
+
+	provider := newLockboxProvider(&fake.YandexCloudCreator{
+		Backend: lockboxBackend,
+	})
+	secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
+	tassert.Nil(t, err)
+	data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+
+	tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
+
+	newKey, newVal := "newKey", "newVal"
+	newVersionID := lockboxBackend.AddVersion(secretID,
+		textEntry(newKey, newVal),
+	)
+
+	data, err = secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
+
+	data, err = secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: newVersionID})
+	tassert.Nil(t, err)
+	tassert.Equal(t, map[string][]byte{newKey: []byte(newVal)}, data)
+}
+
+// helper functions
+
+func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1alpha1.GenericStore {
+	return &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				YandexLockbox: &esv1alpha1.YandexLockboxProvider{
+					APIEndpoint: apiEndpoint,
+					Auth: esv1alpha1.YandexLockboxAuth{
+						AuthorizedKey: esmeta.SecretKeySelector{
+							Name: authorizedKeySecretName,
+							Key:  authorizedKeySecretKey,
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+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
+	}
+
+	err = k8sClient.Create(ctx, &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+			Name:      secretName,
+		},
+		Data: map[string][]byte{secretKey: data},
+	})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func newFakeAuthorizedKey() *iamkey.Key {
+	uniqueLabel := uuid.NewString()
+	return &iamkey.Key{
+		Id: uniqueLabel,
+		Subject: &iamkey.Key_ServiceAccountId{
+			ServiceAccountId: uniqueLabel,
+		},
+		PrivateKey: uniqueLabel,
+	}
+}
+
+func textEntry(key, value string) *lockbox.Payload_Entry {
+	return &lockbox.Payload_Entry{
+		Key: key,
+		Value: &lockbox.Payload_Entry_TextValue{
+			TextValue: value,
+		},
+	}
+}
+
+func binaryEntry(key string, value []byte) *lockbox.Payload_Entry {
+	return &lockbox.Payload_Entry{
+		Key: key,
+		Value: &lockbox.Payload_Entry_BinaryValue{
+			BinaryValue: value,
+		},
+	}
+}
+
+func unmarshalStringMap(t *testing.T, data []byte) map[string]string {
+	stringMap := make(map[string]string)
+	err := json.Unmarshal(data, &stringMap)
+	tassert.Nil(t, err)
+	return stringMap
+}
+
+func base64(data []byte) string {
+	return b64.StdEncoding.EncodeToString(data)
+}