Browse Source

add support for Yandex Certificate Manager

Docs 4 years ago
parent
commit
dc7df48cae

+ 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

@@ -1321,6 +1321,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)
@@ -1865,6 +1870,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

@@ -2645,6 +2645,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

@@ -2648,6 +2648,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

@@ -2270,6 +2270,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:
@@ -4824,6 +4867,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:

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

@@ -29,5 +29,6 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
 	_ "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"
 )

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

@@ -0,0 +1,79 @@
+/*
+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"
+	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"
+	"time"
+
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	ctrl "sigs.k8s.io/controller-runtime"
+)
+
+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{
+		storeSpecYandexCertificateManager.APIEndpoint,
+		storeSpecYandexCertificateManager.Auth.AuthorizedKey,
+		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"
+	"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"
+	"github.com/yandex-cloud/go-genproto/yandex/cloud/certificatemanager/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/google/uuid"
+	tassert "github.com/stretchr/testify/assert"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	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"
+)
+
+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(t, ctx, 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(t, ctx, 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 := "dummyCertificateBlock#1"
+	certificate2 := "dummyCertificateBlock#2"
+	privateKey := "dummyPrivateKeyBlock"
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1, certificate2},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(t, ctx, 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 := "dummyCertificateBlock#1"
+	certificate2 := "dummyCertificateBlock#2"
+	privateKey := "dummyPrivateKeyBlock"
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1, certificate2},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(t, ctx, 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 := "oldCertificateBlock#1"
+	oldCertificate2 := "oldCertificateBlock#2"
+	oldPrivateKey := "oldPrivateKeyBlock"
+	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(t, ctx, 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 := "oldCertificateBlock#1"
+	newCertificate2 := "oldCertificateBlock#2"
+	newPrivateKey := "oldPrivateKeyBlock"
+	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{"dummyCertificateBlock"},
+		PrivateKey:       "dummyPrivateKeyBlock",
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(t, ctx, 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(t, ctx, 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{"dummyCertificateBlock"},
+		PrivateKey:       "dummyPrivateKeyBlock",
+	})
+	_, 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 := "dummyCertificateBlock1"
+	privateKey1 := "dummyPrivateKeyBlock1"
+	certificateID1, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey1, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1},
+		PrivateKey:       privateKey1,
+	})
+	certificate2 := "dummyCertificateBlock1"
+	privateKey2 := "dummyPrivateKeyBlock1"
+	certificateID2, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey2, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate2},
+		PrivateKey:       privateKey2,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(t, ctx, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey1))
+	tassert.Nil(t, err)
+	err = createK8sSecret(t, ctx, 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 := "dummyCertificateBlock1"
+	privateKey1 := "dummyPrivateKeyBlock1"
+	certificateID1, _ := fakeCertificateManagerServer1.CreateCertificate(authorizedKey1, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1},
+		PrivateKey:       privateKey1,
+	})
+	fakeCertificateManagerServer2 := client.NewFakeCertificateManagerServer(fakeClock, time.Hour)
+	certificate2 := "dummyCertificateBlock1"
+	privateKey2 := "dummyPrivateKeyBlock1"
+	certificateID2, _ := fakeCertificateManagerServer2.CreateCertificate(authorizedKey2, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate2},
+		PrivateKey:       privateKey2,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJson(t, authorizedKey1))
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(t, ctx, 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 := "dummyCertificateBlock"
+	privateKey := "dummyPrivateKeyBlock"
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(t, ctx, 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{"dummyCertificateBlock1"},
+		PrivateKey:       "dummyPrivateKeyBlock1",
+	})
+	certificateID2, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey2, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{"dummyCertificateBlock2"},
+		PrivateKey:       "dummyPrivateKeyBlock2",
+	})
+
+	var err error
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName1 = "authorizedKeySecretName1"
+	const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
+	err = createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJson(t, authorizedKey1))
+	tassert.Nil(t, err)
+	const authorizedKeySecretName2 = "authorizedKeySecretName2"
+	const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
+	err = createK8sSecret(t, ctx, 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 := "dummyCertificateBlock#1"
+	certificate2 := "dummyCertificateBlock#2"
+	privateKey := "dummyPrivateKeyBlock"
+	certificateID, _ := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{certificate1, certificate2},
+		PrivateKey:       privateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(t, ctx, 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 := "oldCertificateBlock"
+	oldPrivateKey := "oldPrivateKeyBlock"
+	certificateID, oldVersionID := fakeCertificateManagerServer.CreateCertificate(authorizedKey, &certificatemanager.GetCertificateContentResponse{
+		CertificateChain: []string{oldCertificate},
+		PrivateKey:       oldPrivateKey,
+	})
+
+	k8sClient := clientfake.NewClientBuilder().Build()
+	const authorizedKeySecretName = "authorizedKeySecretName"
+	const authorizedKeySecretKey = "authorizedKeySecretKey"
+	err := createK8sSecret(t, ctx, 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 := "newCertificateBlock"
+	newPrivateKey := "newPrivateKeyBlock"
+	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(t *testing.T, ctx context.Context, 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,
+	}
+}

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

@@ -0,0 +1,84 @@
+/*
+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"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/certificatemanager/client"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"strings"
+)
+
+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()
+}

+ 24 - 0
pkg/provider/yandex/certificatemanager/client/client.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 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)
+}

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

@@ -0,0 +1,134 @@
+/*
+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"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
+	"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"
+	"time"
+)
+
+// 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
+}

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

@@ -0,0 +1,55 @@
+/*
+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"
+	"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
+	api "github.com/yandex-cloud/go-genproto/yandex/cloud/certificatemanager/v1"
+	"github.com/yandex-cloud/go-sdk/iamkey"
+	"google.golang.org/grpc"
+)
+
+// 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
+}