Browse Source

feat: add cloud.ru secret manager support (#3716)

* feat: add Cloud.ru provider

Add a new SecretManager provider, which
 integrates with cloud.ru API and
 allows to interact with stored secrets.

Signed-off-by: Dmitry Ivanov <dvivanov@cloud.ru>

* feat: add cloudru documentation

Signed-off-by: Dmitry Ivanov <dvivanov@cloud.ru>

---------

Signed-off-by: Dmitry Ivanov <dvivanov@cloud.ru>
Dmitry Ivanov 1 year ago
parent
commit
31e7041c8a

+ 41 - 0
apis/externalsecrets/v1beta1/secretstore_cloudru_types.go

@@ -0,0 +1,41 @@
+/*
+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"
+)
+
+// CSMAuth contains a secretRef for credentials.
+type CSMAuth struct {
+	// +optional
+	SecretRef *CSMAuthSecretRef `json:"secretRef,omitempty"`
+}
+
+// CSMAuthSecretRef holds secret references for Cloud.ru credentials.
+type CSMAuthSecretRef struct {
+	// The AccessKeyID is used for authentication
+	AccessKeyID esmeta.SecretKeySelector `json:"accessKeyIDSecretRef"`
+	// The AccessKeySecret is used for authentication
+	AccessKeySecret esmeta.SecretKeySelector `json:"accessKeySecretSecretRef"`
+}
+
+// CloudruSMProvider configures a store to sync secrets using the Cloud.ru Secret Manager provider.
+type CloudruSMProvider struct {
+	Auth CSMAuth `json:"auth"`
+
+	// ProjectID is the project, which the secrets are stored in.
+	ProjectID string `json:"projectID,omitempty"`
+}

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

@@ -200,6 +200,10 @@ type SecretStoreProvider struct {
 	// Beyondtrust configures this store to sync secrets using Password Safe provider.
 	// +optional
 	Beyondtrust *BeyondtrustProvider `json:"beyondtrust,omitempty"`
+
+	// CloudruSM configures this store to sync secrets using the Cloud.ru Secret Manager provider
+	// +optional
+	CloudruSM *CloudruSMProvider `json:"cloudrusm,omitempty"`
 }
 
 type CAProviderType string

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

@@ -565,6 +565,43 @@ func (in *CAProvider) DeepCopy() *CAProvider {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CSMAuth) DeepCopyInto(out *CSMAuth) {
+	*out = *in
+	if in.SecretRef != nil {
+		in, out := &in.SecretRef, &out.SecretRef
+		*out = new(CSMAuthSecretRef)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSMAuth.
+func (in *CSMAuth) DeepCopy() *CSMAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(CSMAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CSMAuthSecretRef) DeepCopyInto(out *CSMAuthSecretRef) {
+	*out = *in
+	in.AccessKeyID.DeepCopyInto(&out.AccessKeyID)
+	in.AccessKeySecret.DeepCopyInto(&out.AccessKeySecret)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CSMAuthSecretRef.
+func (in *CSMAuthSecretRef) DeepCopy() *CSMAuthSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(CSMAuthSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CertAuth) DeepCopyInto(out *CertAuth) {
 	*out = *in
 	in.ClientCert.DeepCopyInto(&out.ClientCert)
@@ -634,6 +671,22 @@ func (in *ChefProvider) DeepCopy() *ChefProvider {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CloudruSMProvider) DeepCopyInto(out *CloudruSMProvider) {
+	*out = *in
+	in.Auth.DeepCopyInto(&out.Auth)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudruSMProvider.
+func (in *CloudruSMProvider) DeepCopy() *CloudruSMProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(CloudruSMProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterExternalSecret) DeepCopyInto(out *ClusterExternalSecret) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
@@ -2747,6 +2800,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(BeyondtrustProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.CloudruSM != nil {
+		in, out := &in.CloudruSM, &out.CloudruSM
+		*out = new(CloudruSMProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.

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

@@ -3180,6 +3180,83 @@ spec:
                     - serverUrl
                     - username
                     type: object
+                  cloudrusm:
+                    description: CloudruSM configures this store to sync secrets using
+                      the Cloud.ru Secret Manager provider
+                    properties:
+                      auth:
+                        description: CSMAuth contains a secretRef for credentials.
+                        properties:
+                          secretRef:
+                            description: CSMAuthSecretRef holds secret references
+                              for Cloud.ru credentials.
+                            properties:
+                              accessKeyIDSecretRef:
+                                description: The AccessKeyID is used for authentication
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              accessKeySecretSecretRef:
+                                description: The AccessKeySecret is used for authentication
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - accessKeyIDSecretRef
+                            - accessKeySecretSecretRef
+                            type: object
+                        type: object
+                      projectID:
+                        description: ProjectID is the project, which the secrets are
+                          stored in.
+                        type: string
+                    required:
+                    - auth
+                    type: object
                   conjur:
                     description: Conjur configures this store to sync secrets using
                       conjur provider

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

@@ -3180,6 +3180,83 @@ spec:
                     - serverUrl
                     - username
                     type: object
+                  cloudrusm:
+                    description: CloudruSM configures this store to sync secrets using
+                      the Cloud.ru Secret Manager provider
+                    properties:
+                      auth:
+                        description: CSMAuth contains a secretRef for credentials.
+                        properties:
+                          secretRef:
+                            description: CSMAuthSecretRef holds secret references
+                              for Cloud.ru credentials.
+                            properties:
+                              accessKeyIDSecretRef:
+                                description: The AccessKeyID is used for authentication
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              accessKeySecretSecretRef:
+                                description: The AccessKeySecret is used for authentication
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - accessKeyIDSecretRef
+                            - accessKeySecretSecretRef
+                            type: object
+                        type: object
+                      projectID:
+                        description: ProjectID is the project, which the secrets are
+                          stored in.
+                        type: string
+                    required:
+                    - auth
+                    type: object
                   conjur:
                     description: Conjur configures this store to sync secrets using
                       conjur provider

+ 144 - 0
deploy/crds/bundle.yaml

@@ -3728,6 +3728,78 @@ spec:
                         - serverUrl
                         - username
                       type: object
+                    cloudrusm:
+                      description: CloudruSM configures this store to sync secrets using the Cloud.ru Secret Manager provider
+                      properties:
+                        auth:
+                          description: CSMAuth contains a secretRef for credentials.
+                          properties:
+                            secretRef:
+                              description: CSMAuthSecretRef holds secret references for Cloud.ru credentials.
+                              properties:
+                                accessKeyIDSecretRef:
+                                  description: The AccessKeyID is used for authentication
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                accessKeySecretSecretRef:
+                                  description: The AccessKeySecret is used for authentication
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - accessKeyIDSecretRef
+                                - accessKeySecretSecretRef
+                              type: object
+                          type: object
+                        projectID:
+                          description: ProjectID is the project, which the secrets are stored in.
+                          type: string
+                      required:
+                        - auth
+                      type: object
                     conjur:
                       description: Conjur configures this store to sync secrets using conjur provider
                       properties:
@@ -10938,6 +11010,78 @@ spec:
                         - serverUrl
                         - username
                       type: object
+                    cloudrusm:
+                      description: CloudruSM configures this store to sync secrets using the Cloud.ru Secret Manager provider
+                      properties:
+                        auth:
+                          description: CSMAuth contains a secretRef for credentials.
+                          properties:
+                            secretRef:
+                              description: CSMAuthSecretRef holds secret references for Cloud.ru credentials.
+                              properties:
+                                accessKeyIDSecretRef:
+                                  description: The AccessKeyID is used for authentication
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                accessKeySecretSecretRef:
+                                  description: The AccessKeySecret is used for authentication
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - accessKeyIDSecretRef
+                                - accessKeySecretSecretRef
+                              type: object
+                          type: object
+                        projectID:
+                          description: ProjectID is the project, which the secrets are stored in.
+                          type: string
+                      required:
+                        - auth
+                      type: object
                     conjur:
                       description: Conjur configures this store to sync secrets using conjur provider
                       properties:

+ 133 - 0
docs/api/spec.md

@@ -1536,6 +1536,83 @@ Can only be defined when used in a ClusterSecretStore.</p>
 <td></td>
 </tr></tbody>
 </table>
+<h3 id="external-secrets.io/v1beta1.CSMAuth">CSMAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.CloudruSMProvider">CloudruSMProvider</a>)
+</p>
+<p>
+<p>CSMAuth contains a secretRef for credentials.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>secretRef</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.CSMAuthSecretRef">
+CSMAuthSecretRef
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.CSMAuthSecretRef">CSMAuthSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.CSMAuth">CSMAuth</a>)
+</p>
+<p>
+<p>CSMAuthSecretRef holds secret references for Cloud.ru credentials.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>accessKeyIDSecretRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<p>The AccessKeyID is used for authentication</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>accessKeySecretSecretRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<p>The AccessKeySecret is used for authentication</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.CertAuth">CertAuth
 </h3>
 <p>
@@ -1695,6 +1772,48 @@ string
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1beta1.CloudruSMProvider">CloudruSMProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>CloudruSMProvider configures a store to sync secrets using the Cloud.ru Secret Manager provider.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.CSMAuth">
+CSMAuth
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>projectID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>ProjectID is the project, which the secrets are stored in.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.ClusterExternalSecret">ClusterExternalSecret
 </h3>
 <p>
@@ -7264,6 +7383,20 @@ BeyondtrustProvider
 <p>Beyondtrust configures this store to sync secrets using Password Safe provider.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>cloudrusm</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.CloudruSMProvider">
+CloudruSMProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>CloudruSM configures this store to sync secrets using the Cloud.ru Secret Manager provider</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef

+ 2 - 0
docs/introduction/stability-support.md

@@ -65,6 +65,7 @@ The following table describes the stability level of each provider and who's res
 | [Device42](https://external-secrets.io/latest/provider/device42)                                           |   alpha   |                                                                                                                                                                                         |
 | [Bitwarden Secrets Manager](https://external-secrets.io/latest/provider/bitwarden-secrets-manager)         |   alpha   |                                                                                                                                                  [@skarlso](https://github.com/Skarlso) |
 | [Previder](https://external-secrets.io/latest/provider/previder)                                           |  stable   |                                                                                                                                                [@previder](https://github.com/previder) |
+| [Cloud.ru](https://external-secrets.io/latest/provider/cloudru)                                            |   alpha   |                                                                                                                                              [@default23](https://github.com/default23) |
 
 ## Provider Feature Support
 
@@ -100,6 +101,7 @@ The following table show the support for features across different providers.
 | Device42                  |              |              |                      |                         |        x         |             |                             |
 | Bitwarden Secrets Manager |      x       |              |                      |                         |        x         |      x      |              x              |
 | Previder                  |      x       |              |                      |                         |        x         |             |                             |
+| Cloud.ru                  |      x       |      x       |                      |            x            |        x         |             |              x              |
 
 ## Support Policy
 

+ 193 - 0
docs/provider/cloudru.md

@@ -0,0 +1,193 @@
+External Secrets Operator integrates with [Cloud.ru](https://cloud.ru) for secret management.
+
+Cloud.ru Secret Manager works in conjunction with the Key Manager cryptographic key management system to ensure secure
+encryption of secrets.
+
+### Authentication
+
+* Before you can use the Cloud.ru Secret Manager, you need to create a service account in
+  the [Cloud.ru Console](https://console.cloud.ru).
+* Create a [Service Account](https://cloud.ru/ru/docs/console_api/ug/topics/guides__service_accounts_create.html)
+  and [Access Key](https://cloud.ru/ru/docs/console_api/ug/topics/guides__service_accounts_key.html) for it.
+
+**NOTE:** To interact with the SecretManager API, you need to use the access token. You can get it by running the
+following command, using the Access Key, created above:
+
+```shell
+curl -i --data-urlencode 'grant_type=access_key' \
+  --data-urlencode "client_id=$KEY_ID" \
+  --data-urlencode "client_secret=$SECRET" \
+  https://id.cloud.ru/auth/system/openid/token
+```
+
+### Creating Cloud.ru secret
+
+To make External Secrets Operator sync a k8s secret with a Cloud.ru secret:
+
+* Navigate to the [Cloud.ru Console](https://console.cloud.ru/).
+* Click the menu at upper-left corner, scroll down to the `Management` section and click on `Secret Manager`.
+* Click on `Create secret`.
+* Fill in the secret name and secret value.
+* Click on `Create`.
+
+Also, you can use [SecretManager API](https://cloud.ru/ru/docs/scsm/ug/topics/guides__add-secret.html) to create the
+secret:
+
+```shell
+curl --location 'https://secretmanager.api.cloud.ru/v1/secrets' \
+--header 'Content-Type: application/json' \
+--header 'Authorization: Bearer ${ACCESS_TOKEN}' \
+--data '{
+    "description": "your secret description",
+    "labels": {
+        "env": "production"
+    },
+    "name": "my_first_secret",
+    "parent_id": "50000000-4000-3000-2000-100000000001",
+    "payload": {
+        "data": {
+            "value": "aGksIHRoZXJlJ3Mgbm90aGluZyBpbnRlcmVzdGluZyBoZXJlCg=="
+        }
+    }
+}'
+```
+
+* `ACCESS_TOKEN` is the access token for the Cloud.ru API. See **Authentication** section
+* `parent_id` parent service instance identifier: ServiceInstanceID. To get the ID value, in your personal account on
+  the top left panel, click the Button with nine dots, select **Management** → **Secret Manager** and copy the value
+  from the Service Instance ID field.
+* `name` is the name of the secret.
+* `description` is the description of the secret.
+* `labels` are the labels(tags) for the secret. Is used in the search.
+* `payload.data.value` is the base64-encoded secret value.
+
+**NOTE:** To create the Multi KeyValue secret in Cloud.ru, you can use the following format (json):
+
+```json
+{
+  "key1": "value1",
+  "key2": "value2"
+}
+```
+
+### Creating ExternalSecret
+
+* Create the k8s Secret, it will be used for authentication in SecretStore:
+    ```yaml
+    apiVersion: v1
+    kind: Secret
+    metadata:
+        name: csm-secret
+        labels:
+          type: csm
+    type: Opaque
+    stringData:
+        key_id: '000000000000000000001'
+        key_secret: '000000000000000000002'
+    ```
+    * `key_id` is the AccessKey key_id.
+    * `key_secret` is the AccessKey key_secret
+* Create a [SecretStore](../api/secretstore.md) pointing to `csm-secret` k8s Secret:
+    ```yaml
+    apiVersion: external-secrets.io/v1beta1
+    kind: SecretStore
+    metadata:
+      name: csm
+    spec:
+      provider:
+        cloudrusm:
+          auth:
+            secretRef:
+              accessKeyIDSecretRef:
+                name: csm-secret
+                key: key_id
+              accessKeySecretSecretRef:
+                name: csm-secret
+                key: key_secret
+          projectID: 50000000-4000-3000-2000-100000000001
+    ```
+    * `accessKeyIDSecretRef` is the reference to the k8s Secret with the AccessKey.
+    * `projectID`  is the project identifier. To get the project id value, in your
+      personal account on the top left, click on project name, In the opening window,
+      click at 3 points next to the name of the necessary project, then the button "Copy the Project ID".
+#### Create an [ExternalSecret](../api/externalsecret.md) pointing to SecretStore.
+  * Classic, non-json:
+    ```yaml
+    apiVersion: external-secrets.io/v1beta1
+    kind: ExternalSecret
+    metadata:
+      name: csm-ext-secret
+    spec:
+      refreshInterval: 10s
+      secretStoreRef:
+        name: csm
+        kind: SecretStore
+      target:
+        name: my-awesome-secret
+        creationPolicy: Owner
+      data:
+        - secretKey: target_key
+          remoteRef:
+            key: my_first_secret # or you can use the secret.id (e.g. 50000000-4000-3000-2000-100000000001)
+    ```
+  * From Multi KeyValue, value MUST be in **json format**:
+  NOTE: You can use *either* `name` or `tags` to filter the secrets. Here are basic examples of both:
+    ```yaml
+    apiVersion: external-secrets.io/v1beta1
+    kind: ExternalSecret
+    metadata:
+      name: csm-ext-secret
+    spec:
+      refreshInterval: 10s
+      secretStoreRef:
+        name: csm
+        kind: SecretStore
+      target:
+        name: my-awesome-secret
+        creationPolicy: Owner
+      data:
+        - secretKey: target_key
+          remoteRef:
+            key: my_first_secret # or you can use the secret.id (e.g. 50000000-4000-3000-2000-100000000001)
+            property: cloudru.secret.key # is the JSON path for the key in the secret value.
+    ```
+
+  * With all fields, value MUST be in **json format**:
+    ```yaml
+    apiVersion: external-secrets.io/v1beta1
+    kind: ExternalSecret
+    metadata:
+      name: csm-ext-secret
+    spec:
+      refreshInterval: 10s
+      secretStoreRef:
+        name: csm
+        kind: SecretStore
+      target:
+        name: my-awesome-secret
+        creationPolicy: Owner
+      dataFrom:
+        - extract:
+            key: my_first_secret # or you can use the secret.id (e.g. 50000000-4000-3000-2000-100000000001)
+    ```
+  * Search the secrets by the Name or Labels (tags):
+    ```yaml
+    apiVersion: external-secrets.io/v1beta1
+    kind: ExternalSecret
+    metadata:
+      name: csm-ext-secret
+    spec:
+      refreshInterval: 10s
+      secretStoreRef:
+        name: csm
+        kind: SecretStore
+      target:
+        name: my-awesome-secret
+        creationPolicy: Owner
+      dataFrom:
+        - find: # You can use the name and tags separately or together to search for secrets.
+            tags:
+              env: production
+            name:
+              regexp: "my.*secret"
+    ```

+ 2 - 0
go.mod

@@ -78,6 +78,8 @@ require (
 	github.com/avast/retry-go/v4 v4.6.1
 	github.com/bradleyfalzon/ghinstallation/v2 v2.14.0
 	github.com/cenkalti/backoff/v4 v4.3.0
+	github.com/cloudru-tech/iam-sdk v1.0.4
+	github.com/cloudru-tech/secret-manager-sdk v1.1.1
 	github.com/cyberark/conjur-api-go v0.12.12
 	github.com/fortanix/sdkms-client-go v0.4.0
 	github.com/go-openapi/strfmt v0.23.0

+ 4 - 0
go.sum

@@ -227,6 +227,10 @@ github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
 github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
+github.com/cloudru-tech/iam-sdk v1.0.4 h1:qnGMhft3FnknCj6F+vTSbwznqJDDCAG6hWRe6IcCyVM=
+github.com/cloudru-tech/iam-sdk v1.0.4/go.mod h1:V7O/EJJrIAHk1WEXclh7A/p0yQiaFIoPeIfJoc0O61w=
+github.com/cloudru-tech/secret-manager-sdk v1.1.1 h1:ykgp0pz37WEYE4KyrSIt1SCwkY2vs5ItyFGd2Tn5tf0=
+github.com/cloudru-tech/secret-manager-sdk v1.1.1/go.mod h1:xnWywhBmESQBaGc/9D1pKBKcWDZN81NcPLFQVZY14Ao=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=

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

@@ -115,6 +115,7 @@ nav:
       - BeyondTrust: provider/beyondtrust.md
       - Bitwarden Secrets Manager: provider/bitwarden-secrets-manager.md
       - Chef: provider/chef.md
+      - Cloud.ru Secret Manager: provider/cloudru.md
       - CyberArk Conjur: provider/conjur.md
       - Device42: provider/device42.md
       - Google Cloud Secret Manager: provider/google-secrets-manager.md

+ 198 - 0
pkg/provider/cloudru/secretmanager/adapter/csm_client.go

@@ -0,0 +1,198 @@
+/*
+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 adapter
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"sync"
+	"time"
+
+	iamAuthV1 "github.com/cloudru-tech/iam-sdk/api/auth/v1"
+	smssdk "github.com/cloudru-tech/secret-manager-sdk"
+	smsV1 "github.com/cloudru-tech/secret-manager-sdk/api/v1"
+	smsV2 "github.com/cloudru-tech/secret-manager-sdk/api/v2"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/metadata"
+	"google.golang.org/grpc/status"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+// CredentialsResolver returns the actual client credentials.
+type CredentialsResolver interface {
+	Resolve(ctx context.Context) (*Credentials, error)
+}
+
+// APIClient - Cloudru Secret Manager Service Client.
+type APIClient struct {
+	cr CredentialsResolver
+
+	iamClient iamAuthV1.AuthServiceClient
+	smsClient *smssdk.Client
+
+	mu                   sync.Mutex
+	accessToken          string
+	accessTokenExpiresAt time.Time
+}
+
+// ListSecretsRequest is a request to list secrets.
+type ListSecretsRequest struct {
+	ProjectID string
+	Labels    map[string]string
+	NameExact string
+	NameRegex string
+}
+
+// Credentials holds the keyID and secret for the CSM client.
+type Credentials struct {
+	KeyID  string
+	Secret string
+}
+
+// NewCredentials creates a new Credentials object.
+func NewCredentials(kid, secret string) (*Credentials, error) {
+	if kid == "" || secret == "" {
+		return nil, errors.New("keyID and secret must be provided")
+	}
+
+	return &Credentials{KeyID: kid, Secret: secret}, nil
+}
+
+// NewAPIClient creates a new grpc SecretManager client.
+func NewAPIClient(cr CredentialsResolver, iamClient iamAuthV1.AuthServiceClient, client *smssdk.Client) *APIClient {
+	return &APIClient{
+		cr:        cr,
+		iamClient: iamClient,
+		smsClient: client,
+	}
+}
+
+func (c *APIClient) ListSecrets(ctx context.Context, req *ListSecretsRequest) ([]*smsV2.Secret, error) {
+	searchReq := &smsV2.SearchSecretRequest{
+		ProjectId: req.ProjectID,
+		Labels:    req.Labels,
+		Depth:     -1,
+	}
+	switch {
+	case req.NameExact != "":
+		searchReq.Name = &smsV2.SearchSecretRequest_Exact{Exact: req.NameExact}
+	case req.NameRegex != "":
+		searchReq.Name = &smsV2.SearchSecretRequest_Regex{Regex: req.NameRegex}
+	}
+
+	var err error
+	ctx, err = c.authCtx(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("unauthorized: %w", err)
+	}
+
+	resp, err := c.smsClient.V2.SecretService.Search(ctx, searchReq)
+	if err != nil {
+		return nil, err
+	}
+
+	return resp.Secrets, nil
+}
+
+func (c *APIClient) AccessSecretVersionByPath(ctx context.Context, projectID, path string, version *int32) ([]byte, error) {
+	var err error
+	ctx, err = c.authCtx(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("unauthorized: %w", err)
+	}
+
+	req := &smsV2.AccessSecretRequest{
+		ProjectId: projectID,
+		Path:      path,
+		Version:   version,
+	}
+	secret, err := c.smsClient.V2.SecretService.Access(ctx, req)
+	if err != nil {
+		st, _ := status.FromError(err)
+		if st.Code() == codes.NotFound {
+			return nil, esv1beta1.NoSecretErr
+		}
+
+		return nil, fmt.Errorf("failed to get the secret by path '%s': %w", path, err)
+	}
+
+	return secret.GetPayload().GetValue(), nil
+}
+
+func (c *APIClient) AccessSecretVersion(ctx context.Context, id, version string) ([]byte, error) {
+	var err error
+	ctx, err = c.authCtx(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("unauthorized: %w", err)
+	}
+
+	if version == "" {
+		version = "latest"
+	}
+	req := &smsV1.AccessSecretVersionRequest{
+		SecretId:        id,
+		SecretVersionId: version,
+	}
+	secret, err := c.smsClient.SecretService.AccessSecretVersion(ctx, req)
+	if err != nil {
+		st, _ := status.FromError(err)
+		if st.Code() == codes.NotFound {
+			return nil, esv1beta1.NoSecretErr
+		}
+
+		return nil, fmt.Errorf("failed to get the secret by id '%s v%s': %w", id, version, err)
+	}
+
+	return secret.GetData().GetValue(), nil
+}
+
+func (c *APIClient) authCtx(ctx context.Context) (context.Context, error) {
+	md, ok := metadata.FromOutgoingContext(ctx)
+	if !ok {
+		md = metadata.New(map[string]string{})
+	}
+	token, err := c.getOrCreateToken(ctx)
+	if err != nil {
+		return ctx, fmt.Errorf("fetch IAM access token: %w", err)
+	}
+
+	md.Set("authorization", "Bearer "+token)
+	return metadata.NewOutgoingContext(ctx, md), nil
+}
+
+func (c *APIClient) getOrCreateToken(ctx context.Context) (string, error) {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+
+	if c.accessToken != "" && c.accessTokenExpiresAt.After(time.Now()) {
+		return c.accessToken, nil
+	}
+
+	creds, err := c.cr.Resolve(ctx)
+	if err != nil {
+		return "", fmt.Errorf("resolve API credentials: %w", err)
+	}
+
+	resp, err := c.iamClient.GetToken(ctx, &iamAuthV1.GetTokenRequest{KeyId: creds.KeyID, Secret: creds.Secret})
+	if err != nil {
+		return "", fmt.Errorf("get access token: %w", err)
+	}
+
+	c.accessToken = resp.AccessToken
+	c.accessTokenExpiresAt = time.Now().Add(time.Second * time.Duration(resp.ExpiresIn))
+	return c.accessToken, nil
+}

+ 180 - 0
pkg/provider/cloudru/secretmanager/client.go

@@ -0,0 +1,180 @@
+/*
+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 secretmanager
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"strconv"
+	"strings"
+
+	smsV2 "github.com/cloudru-tech/secret-manager-sdk/api/v2"
+	"github.com/google/uuid"
+	"github.com/tidwall/gjson"
+	corev1 "k8s.io/api/core/v1"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/provider/cloudru/secretmanager/adapter"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+var (
+	// ErrInvalidSecretVersion represents the error, when trying to access the secret with non-numeric version.
+	ErrInvalidSecretVersion = errors.New("invalid secret version: should be a valid int32 value or 'latest' keyword")
+)
+
+// SecretProvider is an API client for the Cloud.ru Secret Manager.
+type SecretProvider interface {
+	// ListSecrets lists secrets by the given request.
+	ListSecrets(ctx context.Context, req *adapter.ListSecretsRequest) ([]*smsV2.Secret, error)
+	// AccessSecretVersionByPath gets the secret by the given path.
+	AccessSecretVersionByPath(ctx context.Context, projectID, path string, version *int32) ([]byte, error)
+	// AccessSecretVersion gets the secret by the given request.
+	AccessSecretVersion(ctx context.Context, id, version string) ([]byte, error)
+}
+
+// Client is a client for the Cloud.ru Secret Manager.
+type Client struct {
+	apiClient SecretProvider
+
+	projectID string
+}
+
+// GetSecret gets the secret by the remote reference.
+func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	secret, err := c.accessSecret(ctx, ref.Key, ref.Version)
+	if err != nil {
+		return nil, err
+	}
+
+	prop := strings.TrimSpace(ref.Property)
+	if prop == "" {
+		return secret, nil
+	}
+
+	// For more obvious behavior, we return an error if we are dealing with invalid JSON
+	// this is needed, because the gjson library works fine with value for `key`, for example:
+	//
+	// {"key": "value", another: "value"}
+	//
+	// but it will return "" when accessing to a property `another` (no quotes)
+	if err = json.Unmarshal(secret, &map[string]interface{}{}); err != nil {
+		return nil, fmt.Errorf("expecting the secret %q in JSON format, could not access property %q", ref.Key, ref.Property)
+	}
+
+	result := gjson.Parse(string(secret)).Get(prop)
+	if !result.Exists() {
+		return nil, fmt.Errorf("the requested property %q does not exist in secret %q", prop, ref.Key)
+	}
+
+	return []byte(result.Str), nil
+}
+
+func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	secret, err := c.accessSecret(ctx, ref.Key, ref.Version)
+	if err != nil {
+		return nil, err
+	}
+
+	secretMap := make(map[string]json.RawMessage)
+	if err = json.Unmarshal(secret, &secretMap); err != nil {
+		return nil, fmt.Errorf("expecting the secret %q in JSON format", ref.Key)
+	}
+
+	out := make(map[string][]byte)
+	for k, v := range secretMap {
+		out[k] = []byte(strings.Trim(string(v), "\""))
+	}
+
+	return out, nil
+}
+
+// GetAllSecrets gets all secrets by the remote reference.
+func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	if len(ref.Tags) == 0 && ref.Name == nil {
+		return nil, fmt.Errorf("at least one of the following fields must be set: tags, name")
+	}
+
+	var nameFilter string
+	if ref.Name != nil {
+		nameFilter = ref.Name.RegExp
+	}
+
+	searchReq := &adapter.ListSecretsRequest{
+		ProjectID: c.projectID,
+		Labels:    ref.Tags,
+		NameRegex: nameFilter,
+	}
+	secrets, err := c.apiClient.ListSecrets(ctx, searchReq)
+	if err != nil {
+		return nil, fmt.Errorf("failed to list secrets: %w", err)
+	}
+
+	out := make(map[string][]byte)
+	for _, s := range secrets {
+		secret, accessErr := c.accessSecret(ctx, s.GetId(), "latest")
+		if accessErr != nil {
+			return nil, accessErr
+		}
+
+		out[s.GetPath()] = secret
+	}
+
+	return utils.ConvertKeys(ref.ConversionStrategy, out)
+}
+
+func (c *Client) accessSecret(ctx context.Context, key, version string) ([]byte, error) {
+	// check if the secret key is UUID
+	// The uuid value means that the provided `key` is a secret identifier.
+	// if not, then it is a secret name, and we need to get the secret by
+	// name before accessing the version.
+	if _, err := uuid.Parse(key); err != nil {
+		var versionNum *int32
+		if version != "" && version != "latest" {
+			num, parseErr := strconv.ParseInt(version, 10, 32)
+			if parseErr != nil {
+				return nil, ErrInvalidSecretVersion
+			}
+
+			versionNum = &[]int32{int32(num)}[0]
+		}
+
+		return c.apiClient.AccessSecretVersionByPath(ctx, c.projectID, key, versionNum)
+	}
+
+	return c.apiClient.AccessSecretVersion(ctx, key, version)
+}
+
+func (c *Client) PushSecret(context.Context, *corev1.Secret, esv1beta1.PushSecretData) error {
+	return fmt.Errorf("push secret is not supported")
+}
+
+func (c *Client) DeleteSecret(context.Context, esv1beta1.PushSecretRemoteRef) error {
+	return fmt.Errorf("delete secret is not supported")
+}
+
+func (c *Client) SecretExists(context.Context, esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf("secret exists is not supported")
+}
+
+// Validate validates the client.
+func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
+	return esv1beta1.ValidationResultUnknown, nil
+}
+
+// Close closes the client.
+func (c *Client) Close(_ context.Context) error { return nil }

+ 340 - 0
pkg/provider/cloudru/secretmanager/client_test.go

@@ -0,0 +1,340 @@
+/*
+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 secretmanager
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"testing"
+
+	smsV2 "github.com/cloudru-tech/secret-manager-sdk/api/v2"
+	"github.com/google/uuid"
+	tassert "github.com/stretchr/testify/assert"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/provider/cloudru/secretmanager/fake"
+)
+
+const (
+	keyID        = "50000000-4000-3000-2000-100000000001"
+	anotherKeyID = "50000000-4000-3000-2000-100000000002"
+)
+
+var (
+	errInvalidSecretID = errors.New("secret id is invalid")
+	errInternal        = errors.New("internal server error")
+)
+
+func TestClientGetSecret(t *testing.T) {
+	tests := []struct {
+		name        string
+		ref         esv1beta1.ExternalSecretDataRemoteRef
+		setup       func(mock *fake.MockSecretProvider)
+		wantPayload []byte
+		wantErr     error
+	}{
+		{
+			name: "success",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     uuid.NewString(),
+				Version: "1",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockAccessSecretVersion([]byte("secret"), nil)
+			},
+			wantPayload: []byte("secret"),
+			wantErr:     nil,
+		},
+		{
+			name: "success_named_secret",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     "very_secret",
+				Version: "1",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				// before it should find the secret by the name.
+				mock.MockListSecrets([]*smsV2.Secret{
+					{
+						Id:   keyID,
+						Name: "very_secret",
+					},
+				}, nil)
+				mock.MockAccessSecretVersion([]byte("secret"), nil)
+			},
+			wantPayload: []byte("secret"),
+			wantErr:     nil,
+		},
+		{
+			name: "success_multikv",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      uuid.NewString(),
+				Version:  "1",
+				Property: "another.secret",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockAccessSecretVersion([]byte(`{"some": "value", "another": {"secret": "another_value"}}`), nil)
+			},
+			wantPayload: []byte("another_value"),
+			wantErr:     nil,
+		},
+		{
+			name: "error_access_secret",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     uuid.NewString(),
+				Version: "1",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockAccessSecretVersion(nil, errInvalidSecretID)
+			},
+			wantPayload: nil,
+			wantErr:     errInvalidSecretID,
+		},
+		{
+			name: "error_access_named_secret",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     "very_secret",
+				Version: "1",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockAccessSecretVersionPath(nil, errInternal)
+			},
+			wantPayload: nil,
+			wantErr:     errInternal,
+		},
+		{
+			name: "error_access_named_secret:invalid_version",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     "very_secret",
+				Version: "hello",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+			},
+			wantPayload: nil,
+			wantErr:     ErrInvalidSecretVersion,
+		},
+		{
+			name: "error_multikv:invalid_json",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      keyID,
+				Version:  "1",
+				Property: "some",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockAccessSecretVersion([]byte(`"some": "value"`), nil)
+			},
+			wantPayload: nil,
+			wantErr:     fmt.Errorf(`expecting the secret %q in JSON format, could not access property "some"`, keyID),
+		},
+		{
+			name: "error_multikv:not_found",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      keyID,
+				Version:  "1",
+				Property: "unexpected",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockAccessSecretVersion([]byte(`{"some": "value"}`), nil)
+			},
+			wantPayload: nil,
+			wantErr:     fmt.Errorf(`the requested property "unexpected" does not exist in secret %q`, keyID),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mock := &fake.MockSecretProvider{}
+			tt.setup(mock)
+			c := &Client{
+				apiClient: mock,
+				projectID: "123",
+			}
+
+			got, gotErr := c.GetSecret(context.Background(), tt.ref)
+
+			tassert.Equal(t, tt.wantPayload, got)
+			tassert.Equal(t, tt.wantErr, gotErr)
+		})
+	}
+}
+
+func TestClientGetSecretMap(t *testing.T) {
+	tests := []struct {
+		name        string
+		ref         esv1beta1.ExternalSecretDataRemoteRef
+		setup       func(mock *fake.MockSecretProvider)
+		wantPayload map[string][]byte
+		wantErr     error
+	}{
+		{
+			name: "success",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     keyID,
+				Version: "1",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockAccessSecretVersion([]byte(`{"some": "value", "another": "value", "foo": {"bar": "baz"}}`), nil)
+			},
+			wantPayload: map[string][]byte{
+				"some":    []byte("value"),
+				"another": []byte("value"),
+				"foo":     []byte(`{"bar": "baz"}`),
+			},
+			wantErr: nil,
+		},
+		{
+			name: "error_access_secret",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     keyID,
+				Version: "1",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockAccessSecretVersion(nil, errInvalidSecretID)
+			},
+			wantPayload: nil,
+			wantErr:     errInvalidSecretID,
+		},
+		{
+			name: "error_not_json",
+			ref: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:     keyID,
+				Version: "1",
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockAccessSecretVersion([]byte(`top_secret`), nil)
+			},
+			wantPayload: nil,
+			wantErr:     fmt.Errorf(`expecting the secret %q in JSON format`, keyID),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mock := &fake.MockSecretProvider{}
+			tt.setup(mock)
+			c := &Client{
+				apiClient: mock,
+				projectID: "123",
+			}
+
+			got, gotErr := c.GetSecretMap(context.Background(), tt.ref)
+
+			tassert.Equal(t, tt.wantErr, gotErr)
+			tassert.Equal(t, len(tt.wantPayload), len(got))
+			for k, v := range tt.wantPayload {
+				tassert.Equal(t, v, got[k])
+			}
+		})
+	}
+}
+
+func TestClientGetAllSecrets(t *testing.T) {
+	tests := []struct {
+		name        string
+		ref         esv1beta1.ExternalSecretFind
+		setup       func(mock *fake.MockSecretProvider)
+		wantPayload map[string][]byte
+		wantErr     error
+	}{
+		{
+			name: "success",
+			ref: esv1beta1.ExternalSecretFind{
+				Name: &esv1beta1.FindName{RegExp: "secret.*"},
+				Tags: map[string]string{
+					"env": "prod",
+				},
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockListSecrets([]*smsV2.Secret{
+					{Id: keyID, Name: "secret1", Path: "secret1"},
+					{Id: anotherKeyID, Name: "secret", Path: "storage/secret"},
+				}, nil)
+
+				mock.MockAccessSecretVersion([]byte(`{"some": "value", "another": "value", "foo": {"bar": "baz"}}`), nil)
+				mock.MockAccessSecretVersion([]byte(`{"second_secret": "prop_value"}`), nil)
+			},
+			wantPayload: map[string][]byte{
+				"secret1":        []byte(`{"some": "value", "another": "value", "foo": {"bar": "baz"}}`),
+				"storage/secret": []byte(`{"second_secret": "prop_value"}`),
+			},
+			wantErr: nil,
+		},
+		{
+			name: "success_not_json",
+			ref: esv1beta1.ExternalSecretFind{
+				Name: &esv1beta1.FindName{RegExp: "secr.*"},
+				Tags: map[string]string{
+					"env": "prod",
+				},
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockListSecrets([]*smsV2.Secret{
+					{Id: keyID, Name: "secret", Path: "secret"},
+					{Id: anotherKeyID, Name: "secret2", Path: "storage/secret"},
+				}, nil)
+				mock.MockListSecrets(nil, nil) // mock next call
+
+				mock.MockAccessSecretVersion([]byte(`{"some": "value", "another": "value", "foo": {"bar": "baz"}}`), nil)
+				mock.MockAccessSecretVersion([]byte(`top_secret`), nil)
+			},
+			wantPayload: map[string][]byte{
+				"secret":         []byte(`{"some": "value", "another": "value", "foo": {"bar": "baz"}}`),
+				"storage/secret": []byte(`top_secret`),
+			},
+			wantErr: nil,
+		},
+		{
+			name:        "error_no_filters",
+			ref:         esv1beta1.ExternalSecretFind{},
+			wantPayload: nil,
+			wantErr:     errors.New("at least one of the following fields must be set: tags, name"),
+		},
+		{
+			name: "error_list_secrets",
+			ref: esv1beta1.ExternalSecretFind{
+				Name: &esv1beta1.FindName{RegExp: "label.*"},
+				Tags: map[string]string{
+					"env": "prod",
+				},
+			},
+			setup: func(mock *fake.MockSecretProvider) {
+				mock.MockListSecrets(nil, errInternal)
+			},
+			wantPayload: nil,
+			wantErr:     fmt.Errorf("failed to list secrets: %w", errInternal),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mock := &fake.MockSecretProvider{}
+			if tt.setup != nil {
+				tt.setup(mock)
+			}
+
+			c := &Client{
+				apiClient: mock,
+				projectID: "123",
+			}
+			got, gotErr := c.GetAllSecrets(context.Background(), tt.ref)
+
+			tassert.Equal(t, tt.wantErr, gotErr)
+			tassert.Equal(t, len(tt.wantPayload), len(got))
+			for k, v := range tt.wantPayload {
+				tassert.Equal(t, v, got[k])
+			}
+		})
+	}
+}

+ 73 - 0
pkg/provider/cloudru/secretmanager/endpoints.go

@@ -0,0 +1,73 @@
+/*
+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 secretmanager
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// EndpointsURI is the URI for getting the actual Cloud.ru API endpoints.
+const EndpointsURI = "https://api.cloud.ru/endpoints"
+
+// EndpointsResponse is a response from the Cloud.ru API.
+type EndpointsResponse struct {
+	// Endpoints contains the list of actual API addresses of Cloud.ru products.
+	Endpoints []Endpoint `json:"endpoints"`
+}
+
+// Endpoint is a product API address.
+type Endpoint struct {
+	ID      string `json:"id"`
+	Address string `json:"address"`
+}
+
+// GetEndpoints returns the actual Cloud.ru API endpoints.
+func GetEndpoints(url string) (*EndpointsResponse, error) {
+	req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
+	if err != nil {
+		return nil, fmt.Errorf("construct HTTP request for cloud.ru endpoints: %w", err)
+	}
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("get cloud.ru endpoints: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("get cloud.ru endpoints: unexpected status code %d", resp.StatusCode)
+	}
+
+	var endpoints EndpointsResponse
+	if err = json.NewDecoder(resp.Body).Decode(&endpoints); err != nil {
+		return nil, fmt.Errorf("decode cloud.ru endpoints: %w", err)
+	}
+
+	return &endpoints, nil
+}
+
+// Get returns the API address of the product by its ID.
+// If the product is not found, the function returns nil.
+func (er *EndpointsResponse) Get(id string) *Endpoint {
+	for i := range er.Endpoints {
+		if er.Endpoints[i].ID == id {
+			return &er.Endpoints[i]
+		}
+	}
+
+	return nil
+}

+ 73 - 0
pkg/provider/cloudru/secretmanager/fake/fake.go

@@ -0,0 +1,73 @@
+/*
+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"
+
+	smsV2 "github.com/cloudru-tech/secret-manager-sdk/api/v2"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/cloudru/secretmanager/adapter"
+)
+
+type MockSecretProvider struct {
+	ListSecretsFns  []func() ([]*smsV2.Secret, error)
+	AccessSecretFns []func() ([]byte, error)
+}
+
+func (m *MockSecretProvider) ListSecrets(_ context.Context, _ *adapter.ListSecretsRequest) ([]*smsV2.Secret, error) {
+	fn := m.ListSecretsFns[0]
+	if len(m.ListSecretsFns) > 1 {
+		m.ListSecretsFns = m.ListSecretsFns[1:]
+	} else {
+		m.ListSecretsFns = nil
+	}
+
+	return fn()
+}
+
+func (m *MockSecretProvider) AccessSecretVersionByPath(_ context.Context, _, _ string, _ *int32) ([]byte, error) {
+	fn := m.AccessSecretFns[0]
+	if len(m.AccessSecretFns) > 1 {
+		m.AccessSecretFns = m.AccessSecretFns[1:]
+	} else {
+		m.AccessSecretFns = nil
+	}
+	return fn()
+}
+
+func (m *MockSecretProvider) AccessSecretVersion(_ context.Context, _, _ string) ([]byte, error) {
+	fn := m.AccessSecretFns[0]
+	if len(m.AccessSecretFns) > 1 {
+		m.AccessSecretFns = m.AccessSecretFns[1:]
+	} else {
+		m.AccessSecretFns = nil
+	}
+	return fn()
+}
+
+func (m *MockSecretProvider) MockListSecrets(list []*smsV2.Secret, err error) {
+	m.ListSecretsFns = append(m.ListSecretsFns, func() ([]*smsV2.Secret, error) { return list, err })
+}
+
+func (m *MockSecretProvider) MockAccessSecretVersion(data []byte, err error) {
+	m.AccessSecretFns = append(m.AccessSecretFns, func() ([]byte, error) { return data, err })
+}
+
+func (m *MockSecretProvider) MockAccessSecretVersionPath(data []byte, err error) {
+	m.AccessSecretFns = append(m.AccessSecretFns, func() ([]byte, error) { return data, err })
+}
+
+func (m *MockSecretProvider) Close() error { return nil }

+ 223 - 0
pkg/provider/cloudru/secretmanager/provider.go

@@ -0,0 +1,223 @@
+/*
+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 secretmanager
+
+import (
+	"context"
+	"crypto/tls"
+	"errors"
+	"fmt"
+	"net/url"
+	"os"
+	"sync"
+	"time"
+
+	authV1 "github.com/cloudru-tech/iam-sdk/api/auth/v1"
+	smssdk "github.com/cloudru-tech/secret-manager-sdk"
+	"github.com/google/uuid"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/keepalive"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/provider/cloudru/secretmanager/adapter"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+func init() {
+	esv1beta1.Register(NewProvider(), &esv1beta1.SecretStoreProvider{CloudruSM: &esv1beta1.CloudruSMProvider{}})
+}
+
+var _ esv1beta1.Provider = &Provider{}
+var _ esv1beta1.SecretsClient = &Client{}
+
+// Provider is a secrets provider for Cloud.ru Secret Manager.
+type Provider struct {
+	mu sync.Mutex
+
+	// clients is a map of Cloud.ru Secret Manager clients.
+	// Is used to cache the clients to avoid multiple connections,
+	// and excess token retrieving with same credentials.
+	clients map[string]*adapter.APIClient
+}
+
+// NewProvider creates a new Cloud.ru Secret Manager Provider.
+func NewProvider() *Provider {
+	return &Provider{
+		clients: make(map[string]*adapter.APIClient),
+	}
+}
+
+// NewClient constructs a Cloud.ru Secret Manager Provider.
+func (p *Provider) NewClient(
+	ctx context.Context,
+	store esv1beta1.GenericStore,
+	kube kclient.Client,
+	namespace string,
+) (esv1beta1.SecretsClient, error) {
+	if _, err := p.ValidateStore(store); err != nil {
+		return nil, fmt.Errorf("invalid store: %w", err)
+	}
+
+	csmRef := store.GetSpec().Provider.CloudruSM
+	storeKind := store.GetObjectKind().GroupVersionKind().Kind
+	cr := NewKubeCredentialsResolver(kube, namespace, storeKind, csmRef.Auth.SecretRef)
+
+	client, err := p.getClient(ctx, cr)
+	if err != nil {
+		return nil, fmt.Errorf("failed to connect cloud.ru services: %w", err)
+	}
+
+	return &Client{
+		apiClient: client,
+		projectID: csmRef.ProjectID,
+	}, nil
+}
+
+func (p *Provider) getClient(ctx context.Context, cr adapter.CredentialsResolver) (*adapter.APIClient, error) {
+	p.mu.Lock()
+	defer p.mu.Unlock()
+
+	discoveryURL, tokenURL, smURL, err := provideEndpoints()
+	if err != nil {
+		return nil, fmt.Errorf("parse endpoint URLs: %w", err)
+	}
+
+	creds, err := cr.Resolve(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("resolve API credentials: %w", err)
+	}
+
+	connStack := fmt.Sprintf("%s,%s+%s", discoveryURL, creds.KeyID, creds.Secret)
+	client, ok := p.clients[connStack]
+	if ok {
+		return client, nil
+	}
+	iamConn, err := grpc.NewClient(tokenURL,
+		grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS13})),
+		grpc.WithKeepaliveParams(keepalive.ClientParameters{
+			Time:                time.Second * 30,
+			Timeout:             time.Second * 5,
+			PermitWithoutStream: false,
+		}),
+		grpc.WithUserAgent("external-secrets"),
+	)
+	if err != nil {
+		return nil, fmt.Errorf("initialize cloud.ru IAM gRPC client: initiate connection: %w", err)
+	}
+
+	smsClient, err := smssdk.New(&smssdk.Config{Host: smURL},
+		grpc.WithKeepaliveParams(keepalive.ClientParameters{
+			Time:                time.Second * 30,
+			Timeout:             time.Second * 5,
+			PermitWithoutStream: false,
+		}),
+		grpc.WithUserAgent("external-secrets"),
+	)
+	if err != nil {
+		return nil, fmt.Errorf("initialize cloud.ru Secret Manager gRPC client: initiate connection: %w", err)
+	}
+
+	iamClient := authV1.NewAuthServiceClient(iamConn)
+	client = adapter.NewAPIClient(cr, iamClient, smsClient)
+
+	p.clients[connStack] = client
+	return client, nil
+}
+
+// ValidateStore validates the store specification.
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
+	if store == nil {
+		return nil, errors.New("store is not provided")
+	}
+	spec := store.GetSpec()
+	if spec == nil || spec.Provider == nil || spec.Provider.CloudruSM == nil {
+		return nil, errors.New("csm spec is not provided")
+	}
+
+	csmProvider := spec.Provider.CloudruSM
+	switch {
+	case csmProvider.Auth.SecretRef == nil:
+		return nil, errors.New("invalid spec: auth.secretRef is required")
+	case csmProvider.ProjectID == "":
+		return nil, errors.New("invalid spec: projectID is required")
+	}
+	if _, err := uuid.Parse(csmProvider.ProjectID); err != nil {
+		return nil, fmt.Errorf("invalid spec: projectID is invalid UUID: %w", err)
+	}
+
+	ref := csmProvider.Auth.SecretRef
+	err := utils.ValidateReferentSecretSelector(store, ref.AccessKeyID)
+	if err != nil {
+		return nil, fmt.Errorf("invalid spec: auth.secretRef.accessKeyID: %w", err)
+	}
+
+	err = utils.ValidateReferentSecretSelector(store, ref.AccessKeySecret)
+	if err != nil {
+		return nil, fmt.Errorf("invalid spec: auth.secretRef.accessKeySecret: %w", err)
+	}
+
+	return nil, nil
+}
+
+// Capabilities returns the provider Capabilities (ReadOnly).
+func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+	return esv1beta1.SecretStoreReadOnly
+}
+
+func provideEndpoints() (discoveryURL, tokenURL, smURL string, err error) {
+	discoveryURL = EndpointsURI
+	if du := os.Getenv("CLOUDRU_DISCOVERY_URL"); du != "" {
+		var u *url.URL
+		u, err = url.Parse(du)
+		if err != nil {
+			return "", "", "", fmt.Errorf("parse discovery URL: %w", err)
+		}
+
+		if u.Scheme != "https" && u.Scheme != "http" {
+			return "", "", "", fmt.Errorf("invalid scheme in discovery URL, expected http or https, got %s", u.Scheme)
+		}
+
+		discoveryURL = du
+	}
+
+	// try to get the endpoints from the environment variables.
+	csmAddress := os.Getenv("CLOUDRU_CSM_ADDRESS")
+	iamAddress := os.Getenv("CLOUDRU_IAM_ADDRESS")
+	if csmAddress != "" && iamAddress != "" {
+		return discoveryURL, iamAddress, csmAddress, nil
+	}
+
+	// using the discovery URL to get the endpoints.
+	var endpoints *EndpointsResponse
+	endpoints, err = GetEndpoints(discoveryURL)
+	if err != nil {
+		return "", "", "", fmt.Errorf("discover cloud.ru API endpoints: %w", err)
+	}
+
+	smEndpoint := endpoints.Get("secret-manager")
+	if smEndpoint == nil {
+		return "", "", "", errors.New("secret-manager API is not available")
+	}
+
+	iamEndpoint := endpoints.Get("iam")
+	if iamEndpoint == nil {
+		return "", "", "", errors.New("iam API is not available")
+	}
+
+	return discoveryURL, iamEndpoint.Address, smEndpoint.Address, nil
+}

+ 65 - 0
pkg/provider/cloudru/secretmanager/resolver.go

@@ -0,0 +1,65 @@
+/*
+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 secretmanager
+
+import (
+	"context"
+	"fmt"
+
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/provider/cloudru/secretmanager/adapter"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+// KubeCredentialsResolver resolves the credentials from the Kubernetes secret.
+type KubeCredentialsResolver struct {
+	ref  *v1beta1.CSMAuthSecretRef
+	kube kclient.Client
+
+	namespace string
+	storeKind string
+}
+
+// NewKubeCredentialsResolver creates a new KubeCredentialsResolver.
+func NewKubeCredentialsResolver(kube kclient.Client, namespace, storeKind string, ref *v1beta1.CSMAuthSecretRef) *KubeCredentialsResolver {
+	return &KubeCredentialsResolver{
+		ref:       ref,
+		kube:      kube,
+		namespace: namespace,
+		storeKind: storeKind,
+	}
+}
+
+// Resolve resolves the credentials from the Kubernetes secret.
+func (kcr *KubeCredentialsResolver) Resolve(ctx context.Context) (*adapter.Credentials, error) {
+	keyID, err := resolvers.SecretKeyRef(ctx, kcr.kube, kcr.storeKind, kcr.namespace, &kcr.ref.AccessKeyID)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve accessKeyID: %w", err)
+	}
+
+	secret, err := resolvers.SecretKeyRef(ctx, kcr.kube, kcr.storeKind, kcr.namespace, &kcr.ref.AccessKeySecret)
+	if err != nil {
+		return nil, fmt.Errorf("failed to resolve accessKeySecret")
+	}
+
+	creds, err := adapter.NewCredentials(keyID, secret)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get auth credentials: %w", err)
+	}
+
+	return creds, nil
+}

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

@@ -24,6 +24,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/beyondtrust"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/bitwarden"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/chef"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/cloudru/secretmanager"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/conjur"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/delinea"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/device42"