Browse Source

:sparkles: add-keeper-security-provider (#1768)

* add keepersecurity provider

Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* 🧹chore: bumps (#1758)

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* ✨Feature/push secret (#1315)

Introduces Push Secret feature with implementations for the following providers:

* GCP Secret Manager
* AWS Secrets Manager
* AWS Parameter Store
* Hashicorp Vault KV

Signed-off-by: Dominic Meddick <dominic.meddick@engineerbetter.com>
Signed-off-by: Amr Fawzy <amr.fawzy@container-solutions.com>
Signed-off-by: William Young <will.young@engineerbetter.com>
Signed-off-by: James Cleveland <james.cleveland@engineerbetter.com>
Signed-off-by: Lilly Daniell <lilly.daniell@engineerbetter.com>
Signed-off-by: Adrienne Galloway <adrienne.galloway@engineerbetter.com>
Signed-off-by: Marcus Dantas <marcus.dantas@engineerbetter.com>
Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
Signed-off-by: Nick Ruffles <nick.ruffles@engineerbetter.com>
Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* Fixing release pipeline for boringssl (#1763)

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* chore: bump 0.7.0-rc1 (#1765)

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* added documentation

Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* added pushSecret first iteration

Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* added pushSecret and updated documentation

Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* refactor client

Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* update code and unit tests

Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* fix code smells

Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* fix code smells

Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* fix custom fields

Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>

* making it reviewable

Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>

* fix custom field on secret map

Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>

* Update docs/snippets/keepersecurity-push-secret.yaml

Co-authored-by: Moritz Johner <moolen@users.noreply.github.com>
Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>

* fixed edge case, improved validation errors and updated docs

Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>

* fix logic retrieving secrets

Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>

* Update pkg/provider/keepersecurity/client.go

Co-authored-by: Moritz Johner <moolen@users.noreply.github.com>
Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>

* lint code

Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>

* linting code

Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>

* go linter fixed

Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>

* fix crds and documentation

Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>

---------

Signed-off-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>
Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
Signed-off-by: Dominic Meddick <dominic.meddick@engineerbetter.com>
Signed-off-by: Amr Fawzy <amr.fawzy@container-solutions.com>
Signed-off-by: William Young <will.young@engineerbetter.com>
Signed-off-by: James Cleveland <james.cleveland@engineerbetter.com>
Signed-off-by: Lilly Daniell <lilly.daniell@engineerbetter.com>
Signed-off-by: Adrienne Galloway <adrienne.galloway@engineerbetter.com>
Signed-off-by: Marcus Dantas <marcus.dantas@engineerbetter.com>
Signed-off-by: Nick Ruffles <nick.ruffles@engineerbetter.com>
Signed-off-by: Pedro Parra Ortega <parraortega.pedro@gmail.com>
Co-authored-by: Pedro Parra Ortega <pedro.parraortega@enreach.com>
Co-authored-by: Gustavo Fernandes de Carvalho <gusfcarvalho@gmail.com>
Co-authored-by: Moritz Johner <moolen@users.noreply.github.com>
Pedro Parra Ortega 3 years ago
parent
commit
c2054cc1bf

+ 35 - 0
apis/externalsecrets/v1beta1/secretstore_keepersecurity_types.go

@@ -0,0 +1,35 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
+
+import smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+
+// KeeperSecurityProvider Configures a store to sync secrets using Keeper Security.
+type KeeperSecurityProvider struct {
+	Auth *KeeperSecurityAuth `json:"auth"`
+
+	// Keeper Url from which the secrets to be fetched from.
+	Hostname string `json:"hostname"`
+	FolderID string `json:"folderID"`
+}
+
+// KeeperSecurityAuth Configuration used to authenticate with KeeperSecurity.
+type KeeperSecurityAuth struct {
+	AppKey            smmeta.SecretKeySelector `json:"appKeySecretRef"`
+	AppOwnerPublicKey smmeta.SecretKeySelector `json:"appOwnerPublicKeySecretRef"`
+	ClientID          smmeta.SecretKeySelector `json:"clientIdSecretRef"`
+	PrivateKey        smmeta.SecretKeySelector `json:"privateKeySecretRef"`
+	ServerPublicKeyID smmeta.SecretKeySelector `json:"serverPublicKeyIdSecretRef"`
+}

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

@@ -124,6 +124,10 @@ type SecretStoreProvider struct {
 	// Doppler configures this store to sync secrets using the Doppler provider
 	// Doppler configures this store to sync secrets using the Doppler provider
 	// +optional
 	// +optional
 	Doppler *DopplerProvider `json:"doppler,omitempty"`
 	Doppler *DopplerProvider `json:"doppler,omitempty"`
+
+	// KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
+	// +optional
+	KeeperSecurity *KeeperSecurityProvider `json:"keepersecurity,omitempty"`
 }
 }
 
 
 type CAProviderType string
 type CAProviderType string

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

@@ -1292,6 +1292,46 @@ func (in *IBMProvider) DeepCopy() *IBMProvider {
 }
 }
 
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KeeperSecurityAuth) DeepCopyInto(out *KeeperSecurityAuth) {
+	*out = *in
+	in.AppKey.DeepCopyInto(&out.AppKey)
+	in.AppOwnerPublicKey.DeepCopyInto(&out.AppOwnerPublicKey)
+	in.ClientID.DeepCopyInto(&out.ClientID)
+	in.PrivateKey.DeepCopyInto(&out.PrivateKey)
+	in.ServerPublicKeyID.DeepCopyInto(&out.ServerPublicKeyID)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeeperSecurityAuth.
+func (in *KeeperSecurityAuth) DeepCopy() *KeeperSecurityAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(KeeperSecurityAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *KeeperSecurityProvider) DeepCopyInto(out *KeeperSecurityProvider) {
+	*out = *in
+	if in.Auth != nil {
+		in, out := &in.Auth, &out.Auth
+		*out = new(KeeperSecurityAuth)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KeeperSecurityProvider.
+func (in *KeeperSecurityProvider) DeepCopy() *KeeperSecurityProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(KeeperSecurityProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *KubernetesAuth) DeepCopyInto(out *KubernetesAuth) {
 func (in *KubernetesAuth) DeepCopyInto(out *KubernetesAuth) {
 	*out = *in
 	*out = *in
 	if in.Cert != nil {
 	if in.Cert != nil {
@@ -1641,6 +1681,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(DopplerProvider)
 		*out = new(DopplerProvider)
 		(*in).DeepCopyInto(*out)
 		(*in).DeepCopyInto(*out)
 	}
 	}
+	if in.KeeperSecurity != nil {
+		in, out := &in.KeeperSecurity, &out.KeeperSecurity
+		*out = new(KeeperSecurityProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 }
 
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.

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

@@ -2363,6 +2363,132 @@ spec:
                     required:
                     required:
                     - auth
                     - auth
                     type: object
                     type: object
+                  keepersecurity:
+                    description: KeeperSecurity configures this store to sync secrets
+                      using the KeeperSecurity provider
+                    properties:
+                      auth:
+                        description: KeeperSecurityAuth Configuration used to authenticate
+                          with KeeperSecurity.
+                        properties:
+                          appKeySecretRef:
+                            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
+                          appOwnerPublicKeySecretRef:
+                            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
+                          clientIdSecretRef:
+                            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
+                          privateKeySecretRef:
+                            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
+                          serverPublicKeyIdSecretRef:
+                            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
+                        required:
+                        - appKeySecretRef
+                        - appOwnerPublicKeySecretRef
+                        - clientIdSecretRef
+                        - privateKeySecretRef
+                        - serverPublicKeyIdSecretRef
+                        type: object
+                      folderID:
+                        type: string
+                      hostname:
+                        description: Keeper Url from which the secrets to be fetched
+                          from.
+                        type: string
+                    required:
+                    - auth
+                    - folderID
+                    - hostname
+                    type: object
                   kubernetes:
                   kubernetes:
                     description: Kubernetes configures this store to sync secrets
                     description: Kubernetes configures this store to sync secrets
                       using a Kubernetes cluster provider
                       using a Kubernetes cluster provider

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

@@ -2363,6 +2363,132 @@ spec:
                     required:
                     required:
                     - auth
                     - auth
                     type: object
                     type: object
+                  keepersecurity:
+                    description: KeeperSecurity configures this store to sync secrets
+                      using the KeeperSecurity provider
+                    properties:
+                      auth:
+                        description: KeeperSecurityAuth Configuration used to authenticate
+                          with KeeperSecurity.
+                        properties:
+                          appKeySecretRef:
+                            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
+                          appOwnerPublicKeySecretRef:
+                            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
+                          clientIdSecretRef:
+                            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
+                          privateKeySecretRef:
+                            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
+                          serverPublicKeyIdSecretRef:
+                            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
+                        required:
+                        - appKeySecretRef
+                        - appOwnerPublicKeySecretRef
+                        - clientIdSecretRef
+                        - privateKeySecretRef
+                        - serverPublicKeyIdSecretRef
+                        type: object
+                      folderID:
+                        type: string
+                      hostname:
+                        description: Keeper Url from which the secrets to be fetched
+                          from.
+                        type: string
+                    required:
+                    - auth
+                    - folderID
+                    - hostname
+                    type: object
                   kubernetes:
                   kubernetes:
                     description: Kubernetes configures this store to sync secrets
                     description: Kubernetes configures this store to sync secrets
                       using a Kubernetes cluster provider
                       using a Kubernetes cluster provider

+ 176 - 0
deploy/crds/bundle.yaml

@@ -2173,6 +2173,94 @@ spec:
                       required:
                       required:
                         - auth
                         - auth
                       type: object
                       type: object
+                    keepersecurity:
+                      description: KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
+                      properties:
+                        auth:
+                          description: KeeperSecurityAuth Configuration used to authenticate with KeeperSecurity.
+                          properties:
+                            appKeySecretRef:
+                              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
+                            appOwnerPublicKeySecretRef:
+                              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
+                            clientIdSecretRef:
+                              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
+                            privateKeySecretRef:
+                              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
+                            serverPublicKeyIdSecretRef:
+                              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
+                          required:
+                            - appKeySecretRef
+                            - appOwnerPublicKeySecretRef
+                            - clientIdSecretRef
+                            - privateKeySecretRef
+                            - serverPublicKeyIdSecretRef
+                          type: object
+                        folderID:
+                          type: string
+                        hostname:
+                          description: Keeper Url from which the secrets to be fetched from.
+                          type: string
+                      required:
+                        - auth
+                        - folderID
+                        - hostname
+                      type: object
                     kubernetes:
                     kubernetes:
                       description: Kubernetes configures this store to sync secrets using a Kubernetes cluster provider
                       description: Kubernetes configures this store to sync secrets using a Kubernetes cluster provider
                       properties:
                       properties:
@@ -5466,6 +5554,94 @@ spec:
                       required:
                       required:
                         - auth
                         - auth
                       type: object
                       type: object
+                    keepersecurity:
+                      description: KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
+                      properties:
+                        auth:
+                          description: KeeperSecurityAuth Configuration used to authenticate with KeeperSecurity.
+                          properties:
+                            appKeySecretRef:
+                              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
+                            appOwnerPublicKeySecretRef:
+                              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
+                            clientIdSecretRef:
+                              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
+                            privateKeySecretRef:
+                              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
+                            serverPublicKeyIdSecretRef:
+                              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
+                          required:
+                            - appKeySecretRef
+                            - appOwnerPublicKeySecretRef
+                            - clientIdSecretRef
+                            - privateKeySecretRef
+                            - serverPublicKeyIdSecretRef
+                          type: object
+                        folderID:
+                          type: string
+                        hostname:
+                          description: Keeper Url from which the secrets to be fetched from.
+                          type: string
+                      required:
+                        - auth
+                        - folderID
+                        - hostname
+                      type: object
                     kubernetes:
                     kubernetes:
                       description: Kubernetes configures this store to sync secrets using a Kubernetes cluster provider
                       description: Kubernetes configures this store to sync secrets using a Kubernetes cluster provider
                       properties:
                       properties:

+ 145 - 0
docs/api/spec.md

@@ -3425,6 +3425,137 @@ string
 </tr>
 </tr>
 </tbody>
 </tbody>
 </table>
 </table>
+<h3 id="external-secrets.io/v1beta1.KeeperSecurityAuth">KeeperSecurityAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.KeeperSecurityProvider">KeeperSecurityProvider</a>)
+</p>
+<p>
+<p>KeeperSecurityAuth Configuration used to authenticate with KeeperSecurity.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>appKeySecretRef</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>
+</td>
+</tr>
+<tr>
+<td>
+<code>appOwnerPublicKeySecretRef</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>
+</td>
+</tr>
+<tr>
+<td>
+<code>clientIdSecretRef</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>
+</td>
+</tr>
+<tr>
+<td>
+<code>privateKeySecretRef</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>
+</td>
+</tr>
+<tr>
+<td>
+<code>serverPublicKeyIdSecretRef</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>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.KeeperSecurityProvider">KeeperSecurityProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>KeeperSecurityProvider Configures a store to sync secrets using Keeper Security.</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.KeeperSecurityAuth">
+KeeperSecurityAuth
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>hostname</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Keeper Url from which the secrets to be fetched from.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>folderID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.KubernetesAuth">KubernetesAuth
 <h3 id="external-secrets.io/v1beta1.KubernetesAuth">KubernetesAuth
 </h3>
 </h3>
 <p>
 <p>
@@ -4306,6 +4437,20 @@ DopplerProvider
 <p>Doppler configures this store to sync secrets using the Doppler provider</p>
 <p>Doppler configures this store to sync secrets using the Doppler provider</p>
 </td>
 </td>
 </tr>
 </tr>
+<tr>
+<td>
+<code>keepersecurity</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.KeeperSecurityProvider">
+KeeperSecurityProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider</p>
+</td>
+</tr>
 </tbody>
 </tbody>
 </table>
 </table>
 <h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef
 <h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef

+ 97 - 0
docs/provider/keeper-security.md

@@ -0,0 +1,97 @@
+## Keeper Security
+
+External Secrets Operator integrates with [Keeper Security](https://www.keepersecurity.com/) for secret management by using [Keeper Secrets Manager](https://docs.keeper.io/secrets-manager/secrets-manager/about).
+
+
+## Authentication
+
+### Secrets Manager Configuration (SMC)
+
+KSM can authenticate using *One Time Access Token* or *Secret Manager Configuration*. In order to work with External Secret Operator we need to configure a Secret Manager Configuration.
+
+#### Creating Secrets Manager Configuration
+
+You can find the documentation for the Secret Manager Configuration creation [here](https://docs.keeper.io/secrets-manager/secrets-manager/about/secrets-manager-configuration). Make sure you add the proper permissions to your device in order to be able to read and write secrets
+
+Once you have created your SMC, you will get a config.json file containing the following keys:
+- `hostname`
+- `clientId`
+- `privateKey`
+- `serverPublicKeyId`
+- `appKey`
+- `appOwnerPublicKey`
+
+This config will be required to create your secretStores
+
+## Important note about this documentation
+_**The KepeerSecurity calls the entries in vaults 'Records'. These docs use the same term.**_
+
+### Update secret store
+Be sure the `keepersecurity` provider is listed in the `Kind=SecretStore`
+
+```yaml
+{% include 'keepersecurity-secret-store.yaml' %}
+```
+
+**NOTE 1:** `folderID` target the folder ID where the secrets should be pushed to. It requires write permissions within the folder
+**NOTE 2:** In case of a `ClusterSecretStore`, Be sure to provide `namespace` for `SecretAccessKeyRef` with the namespace of the secret that we just created.
+
+## External Secrets
+### Behavior
+* How a Record is equated to an ExternalSecret:
+    * `remoteRef.key` is equated to a Record's ID
+    * `remoteRef.property` is equated to one of the following options:
+        * Fields: [Record's field's Type](https://docs.keeper.io/secrets-manager/secrets-manager/about/field-record-types)
+        * CustomFields: Record's field's Label
+        * Files: Record's file's Name
+        * If empty, defaults to the complete Record in JSON format
+    * `remoteRef.version` is currently not supported.
+* `dataFrom`:
+    * `find.path` is currently not supported.
+    * `find.name.regexp` is equated to one of the following options:
+        * Fields: Record's field's Type
+        * CustomFields: Record's field's Label
+        * Files: Record's file's Name
+    * `find.tags` are not supported at this time.
+
+### Creating external secret
+To create a kubernetes secret from the GCP Secret Manager secret a `Kind=ExternalSecret` is needed.
+
+```yaml
+{% include 'keepersecurity-external-secret.yaml' %}
+```
+
+The operator will fetch the Keeper Secret Manager secret and inject it as a `Kind=Secret`
+```
+kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.dev-secret-test}' | base64 -d
+```
+
+## Limitations
+
+There are some limitations using this provider.
+* Keeper Secret Manager does not work with `General` Records types nor legacy non-typed records
+* Using tags `find.tags` is not supported by KSM
+* Using path `find.path` is not supported at the moment
+
+## Push Secrets
+
+Push Secret will only work with a custom KeeperSecurity Record type `ExternalSecret`
+
+### Behavior
+* `selector`:
+  * `secret.name`: name of the kubernetes secret to be pushed
+* `data.match`:
+  * `secretKey`: key on the selected secret to be pushed
+  * `remoteRef.remoteKey`: Secret and key to be created on the remote provider
+    * Format: SecretName/SecretKey
+
+### Creating push secret
+To create a Keeper Security record from kubernetes a `Kind=PushSecret` is needed.
+
+```yaml
+{% include 'keepersecurity-push-secret.yaml' %}
+```
+
+### Limitations
+* Only possible to push one key per secret at the moment
+* If the record with the selected name exists but the key does not exists the record can not be updated. See [Ability to add custom fields to existing secret #17](https://github.com/Keeper-Security/secrets-manager-go/issues/17)

+ 71 - 0
docs/snippets/keepersecurity-external-secret.yaml

@@ -0,0 +1,71 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: example
+spec:
+  refreshInterval: 1h           # rate SecretManager pulls KeeperSrucity
+  secretStoreRef:
+    kind: SecretStore
+    name: example               # name of the SecretStore (or kind specified)
+  target:
+    name: secret-to-be-created  # name of the k8s Secret to be created
+    creationPolicy: Owner
+  dataFrom:
+    - extract:
+        key: OqPt3Vd37My7G8rTb-8Q  # ID of the Keeper Record
+---
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: regcred
+  namespace: external-secrets
+spec:
+  refreshInterval: 1m
+  secretStoreRef:
+    name: keeper
+    kind: ClusterSecretStore
+  target:
+    name: regcred
+    creationPolicy: Owner
+    template:
+      engineVersion: v2
+      type: kubernetes.io/dockerconfigjson
+      data:
+        .dockerconfigjson: "{\"auths\":{\"registry.example.com\":{\"username\":\"{{ .username }}\",\"password\":\"{{ .password }}\",\"auth\":\"{{(printf \"%s:%s\" .username .password) | b64enc }}\"}}}"
+  data:
+    - secretKey: username
+      remoteRef:
+        key: OqPt3Vd37My7G8rTb-8Q
+        property: login
+    - secretKey: password
+      remoteRef:
+        key: OqPt3Vd37My7G8rTb-8Q
+        property: password
+---
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: config
+  namespace: external-secrets
+spec:
+  refreshInterval: 1m
+  secretStoreRef:
+    name: keeper
+    kind: ClusterSecretStore
+  target:
+    name: credentials
+    creationPolicy: Owner
+    template:
+      engineVersion: v2
+      data:
+        username: "{{ .login }}"
+        password: "{{ .password }}"
+  data:
+    - secretKey: login
+      remoteRef:
+        key: OqPt3Vd37My7G8rTb-8Q
+        property: login
+    - secretKey: password
+      remoteRef:
+        key: OqPt3Vd37My7G8rTb-8Q
+        property: password

+ 20 - 0
docs/snippets/keepersecurity-push-secret.yaml

@@ -0,0 +1,20 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: example
+spec:
+  secretStoreRefs:
+    - name: keeper
+      kind: SecretStore
+  refreshInterval: "1h"
+  deletionPolicy: Delete
+  selector:
+    secret:
+      name: secret-name # k8s secret to be pushed
+  data:
+    - match:
+        secretKey: secret-key # k8s key within the secret to be pushed
+        remoteRef:
+          remoteKey: remote-secret-name/remote-secret-key # This will create a record called "remote-secret-name" with a key "remote-secret-key"
+
+

+ 26 - 0
docs/snippets/keepersecurity-secret-store.yaml

@@ -0,0 +1,26 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: keeper
+spec:
+  provider:
+    keepersecurity:
+      hostname: keepersecurity.eu
+      auth:
+        appKeySecretRef:
+          name: keeper-configuration
+          key:  appKey
+        appOwnerPublicKeySecretRef:
+          name: keeper-configuration
+          key: appOwnerPublicKey
+        privateKeySecretRef:
+          name: keeper-configuration
+          key: privateKey
+        serverPublicKeyIdSecretRef:
+          name: keeper-configuration
+          key: serverPublicKeyId
+        clientIdSecretRef:
+          name: keeper-configuration
+          key: clientId
+      folderID: 1qdsiewFW-U # Folder ID where the secrets can be pushed. It requires write permissions

+ 1 - 0
go.mod

@@ -96,6 +96,7 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0
 	github.com/hashicorp/golang-lru v0.5.4
 	github.com/hashicorp/golang-lru v0.5.4
+	github.com/keeper-security/secrets-manager-go/core v1.4.0
 	github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0
 	github.com/maxbrunsfeld/counterfeiter/v6 v6.5.0
 	github.com/sethvargo/go-password v0.2.0
 	github.com/sethvargo/go-password v0.2.0
 	github.com/spf13/pflag v1.0.5
 	github.com/spf13/pflag v1.0.5

+ 2 - 0
go.sum

@@ -456,6 +456,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
 github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
 github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/keeper-security/secrets-manager-go/core v1.4.0 h1:6x65lMBPwHNirQRXwGByCHrzJx7LDWt06uBUKCLs92w=
+github.com/keeper-security/secrets-manager-go/core v1.4.0/go.mod h1:dtlaeeds9+SZsbDAZnQRsDSqEAK9a62SYtqhNql+VgQ=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=

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

@@ -95,6 +95,7 @@ nav:
     - Kubernetes: provider/kubernetes.md
     - Kubernetes: provider/kubernetes.md
     - senhasegura DevOps Secrets Management (DSM): provider/senhasegura-dsm.md
     - senhasegura DevOps Secrets Management (DSM): provider/senhasegura-dsm.md
     - Doppler: provider/doppler.md
     - Doppler: provider/doppler.md
+    - Keeper Security: provider/keeper-security.md
   - Examples:
   - Examples:
     - FluxCD: examples/gitops-using-fluxcd.md
     - FluxCD: examples/gitops-using-fluxcd.md
     - Anchore Engine: examples/anchore-engine-credentials.md
     - Anchore Engine: examples/anchore-engine-credentials.md

+ 483 - 0
pkg/provider/keepersecurity/client.go

@@ -0,0 +1,483 @@
+/*
+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 keepersecurity
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+
+	ksm "github.com/keeper-security/secrets-manager-go/core"
+	"golang.org/x/exp/maps"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+const (
+	errKeeperSecuritySecretsNotFound            = "unable to find secrets. %w"
+	errKeeperSecuritySecretNotFound             = "unable to find secret %s. Error: %w"
+	errKeeperSecuritySecretNotUnique            = "more than 1 secret %s found"
+	errKeeperSecurityNoSecretsFound             = "no secrets found"
+	errKeeperSecurityInvalidSecretInvalidFormat = "invalid secret. Invalid format: %w"
+	errKeeperSecurityInvalidSecretDuplicatedKey = "invalid Secret. Following keys are duplicated %s"
+	errKeeperSecurityInvalidProperty            = "invalid Property. Secret %s does not have any key matching %s"
+	errKeeperSecurityInvalidField               = "invalid Field. Key %s does not exists"
+	errKeeperSecurityNoFields                   = "invalid Secret. Secret %s does not contain any valid field/file"
+	keeperSecurityFileRef                       = "fileRef"
+	keeperSecurityMfa                           = "oneTimeCode"
+	errTagsNotImplemented                       = "'find.tags' is not implemented in the KeeperSecurity provider"
+	errPathNotImplemented                       = "'find.path' is not implemented in the KeeperSecurity provider"
+	errInvalidJSONSecret                        = "invalid Secret. Secret %s can not be converted to JSON. %w"
+	errInvalidRegex                             = "find.name.regex. Invalid Regular expresion %s. %w"
+	errInvalidRemoteRefKey                      = "match.remoteRef.remoteKey. Invalid format. Format should match secretName/key got %s"
+	errInvalidSecretType                        = "ESO can only push/delete %s record types. Secret %s is type %s"
+	errFieldNotFound                            = "secret %s does not contain any custom field with label %s"
+
+	externalSecretType = "externalSecrets"
+	secretType         = "secret"
+	LoginType          = "login"
+	LoginTypeExpr      = "login|username"
+	PasswordType       = "password"
+	URLTypeExpr        = "url|baseurl"
+	URLType            = "url"
+)
+
+type Client struct {
+	ksmClient SecurityClient
+	folderID  string
+}
+
+type SecurityClient interface {
+	GetSecrets(filter []string) ([]*ksm.Record, error)
+	GetSecretByTitle(recordTitle string) (*ksm.Record, error)
+	CreateSecretWithRecordData(recUID, folderUID string, recordData *ksm.RecordCreate) (string, error)
+	DeleteSecrets(recrecordUids []string) (map[string]string, error)
+	Save(record *ksm.Record) error
+}
+
+type Field struct {
+	Type  string   `json:"type"`
+	Value []string `json:"value"`
+}
+
+type CustomField struct {
+	Type  string   `json:"type"`
+	Label string   `json:"label"`
+	Value []string `json:"value"`
+}
+
+type File struct {
+	Title   string `json:"type"`
+	Content string `json:"content"`
+}
+
+type Secret struct {
+	Title  string        `json:"title"`
+	Type   string        `json:"type"`
+	Fields []Field       `json:"fields"`
+	Custom []CustomField `json:"custom"`
+	Files  []File        `json:"files"`
+}
+
+func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
+	return esv1beta1.ValidationResultReady, nil
+}
+
+func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	record, err := c.findSecretByID(ref.Key)
+	if err != nil {
+		return nil, err
+	}
+	secret, err := c.getValidKeeperSecret(record)
+	if err != nil {
+		return nil, err
+	}
+
+	return secret.getItem(ref)
+}
+
+func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	record, err := c.findSecretByID(ref.Key)
+	if err != nil {
+		return nil, err
+	}
+	secret, err := c.getValidKeeperSecret(record)
+	if err != nil {
+		return nil, err
+	}
+
+	return secret.getItems(ref)
+}
+
+func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	if ref.Tags != nil {
+		return nil, fmt.Errorf(errTagsNotImplemented)
+	}
+	if ref.Path != nil {
+		return nil, fmt.Errorf(errPathNotImplemented)
+	}
+	secretData := make(map[string][]byte)
+	records, err := c.findSecrets()
+	if err != nil {
+		return nil, err
+	}
+	for _, record := range records {
+		secret, err := c.getValidKeeperSecret(record)
+		if err != nil {
+			return nil, err
+		}
+		match, err := regexp.MatchString(ref.Name.RegExp, secret.Title)
+		if err != nil {
+			return nil, fmt.Errorf(errInvalidRegex, ref.Name.RegExp, err)
+		}
+		if !match {
+			continue
+		}
+		secretData[secret.Title], err = secret.getItem(esv1beta1.ExternalSecretDataRemoteRef{})
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return secretData, nil
+}
+
+func (c *Client) Close(ctx context.Context) error {
+	return nil
+}
+
+func (c *Client) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
+	parts, err := c.buildSecretNameAndKey(remoteRef)
+	if err != nil {
+		return err
+	}
+	secret, err := c.findSecretByName(parts[0])
+	if err != nil {
+		_, err = c.createSecret(parts[0], parts[1], value)
+		if err != nil {
+			return err
+		}
+	}
+	if secret != nil {
+		if secret.Type() != externalSecretType {
+			return fmt.Errorf(errInvalidSecretType, externalSecretType, secret.Title(), secret.Type())
+		}
+		err = c.updateSecret(secret, parts[1], value)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
+	parts, err := c.buildSecretNameAndKey(remoteRef)
+	if err != nil {
+		return err
+	}
+	secret, err := c.findSecretByName(parts[0])
+	if err != nil {
+		return err
+	}
+	if secret.Type() != externalSecretType {
+		return fmt.Errorf(errInvalidSecretType, externalSecretType, secret.Title(), secret.Type())
+	}
+	_, err = c.ksmClient.DeleteSecrets([]string{secret.Uid})
+	if err != nil {
+		return nil
+	}
+
+	return nil
+}
+
+func (c *Client) buildSecretNameAndKey(remoteRef esv1beta1.PushRemoteRef) ([]string, error) {
+	parts := strings.Split(remoteRef.GetRemoteKey(), "/")
+	if len(parts) != 2 {
+		return nil, fmt.Errorf(errInvalidRemoteRefKey, remoteRef.GetRemoteKey())
+	}
+
+	return parts, nil
+}
+
+func (c *Client) createSecret(name, key string, value []byte) (string, error) {
+	normalizedKey := strings.ToLower(key)
+	externalSecretRecord := ksm.NewRecordCreate(externalSecretType, name)
+	login := regexp.MustCompile(LoginTypeExpr)
+	pass := regexp.MustCompile(PasswordType)
+	url := regexp.MustCompile(URLTypeExpr)
+
+	switch {
+	case login.MatchString(normalizedKey):
+		externalSecretRecord.Fields = append(externalSecretRecord.Fields,
+			ksm.NewLogin(string(value)),
+		)
+	case pass.MatchString(normalizedKey):
+		externalSecretRecord.Fields = append(externalSecretRecord.Fields,
+			ksm.NewPassword(string(value)),
+		)
+	case url.MatchString(normalizedKey):
+		externalSecretRecord.Fields = append(externalSecretRecord.Fields,
+			ksm.NewUrl(string(value)),
+		)
+	default:
+		field := ksm.KeeperRecordField{Type: secretType, Label: key}
+		externalSecretRecord.Custom = append(externalSecretRecord.Custom,
+			ksm.Secret{KeeperRecordField: field, Value: []string{string(value)}},
+		)
+	}
+
+	return c.ksmClient.CreateSecretWithRecordData("", c.folderID, externalSecretRecord)
+}
+
+func (c *Client) updateSecret(secret *ksm.Record, key string, value []byte) error {
+	normalizedKey := strings.ToLower(key)
+	login := regexp.MustCompile(LoginTypeExpr)
+	pass := regexp.MustCompile(PasswordType)
+	url := regexp.MustCompile(URLTypeExpr)
+	custom := false
+
+	switch {
+	case login.MatchString(normalizedKey):
+		secret.SetFieldValueSingle(LoginType, string(value))
+	case pass.MatchString(normalizedKey):
+		secret.SetPassword(string(value))
+	case url.MatchString(normalizedKey):
+		secret.SetFieldValueSingle(URLType, string(value))
+	default:
+		custom = true
+	}
+	if custom {
+		field := secret.GetCustomFieldValueByLabel(key)
+		if field != "" {
+			secret.SetCustomFieldValueSingle(key, string(value))
+		} else {
+			return fmt.Errorf(errFieldNotFound, secret.Title(), key)
+		}
+	}
+
+	return c.ksmClient.Save(secret)
+}
+
+func (c *Client) getValidKeeperSecret(secret *ksm.Record) (*Secret, error) {
+	keeperSecret := Secret{}
+	err := json.Unmarshal([]byte(secret.RawJson), &keeperSecret)
+	if err != nil {
+		return nil, fmt.Errorf(errKeeperSecurityInvalidSecretInvalidFormat, err)
+	}
+	keeperSecret.addFiles(secret.Files)
+	err = keeperSecret.validate()
+	if err != nil {
+		return nil, err
+	}
+
+	return &keeperSecret, nil
+}
+
+func (c *Client) findSecrets() ([]*ksm.Record, error) {
+	records, err := c.ksmClient.GetSecrets([]string{})
+	if err != nil {
+		return nil, fmt.Errorf(errKeeperSecuritySecretsNotFound, err)
+	}
+
+	return records, nil
+}
+
+func (c *Client) findSecretByID(id string) (*ksm.Record, error) {
+	records, err := c.ksmClient.GetSecrets([]string{id})
+	if err != nil {
+		return nil, fmt.Errorf(errKeeperSecuritySecretNotFound, id, err)
+	}
+
+	if len(records) == 0 {
+		return nil, errors.New(errKeeperSecurityNoSecretsFound)
+	}
+	if len(records) > 1 {
+		return nil, fmt.Errorf(errKeeperSecuritySecretNotUnique, id)
+	}
+
+	return records[0], nil
+}
+
+func (c *Client) findSecretByName(name string) (*ksm.Record, error) {
+	record, err := c.ksmClient.GetSecretByTitle(name)
+	if err != nil {
+		return nil, err
+	}
+
+	return record, nil
+}
+
+func (s *Secret) validate() error {
+	fields := make(map[string]int)
+	for _, field := range s.Fields {
+		fields[field.Type]++
+	}
+
+	for _, customField := range s.Custom {
+		fields[customField.Label]++
+	}
+
+	for _, file := range s.Files {
+		fields[file.Title]++
+	}
+	var duplicates []string
+	for key, ocurrences := range fields {
+		if ocurrences > 1 {
+			duplicates = append(duplicates, key)
+		}
+	}
+	if len(duplicates) != 0 {
+		return fmt.Errorf(errKeeperSecurityInvalidSecretDuplicatedKey, strings.Join(duplicates, ", "))
+	}
+
+	return nil
+}
+
+func (s *Secret) addFiles(keeperFiles []*ksm.KeeperFile) {
+	for _, f := range keeperFiles {
+		s.Files = append(
+			s.Files,
+			File{
+				Title:   f.Title,
+				Content: string(f.GetFileData()),
+			},
+		)
+	}
+}
+
+func (s *Secret) getItem(ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	if ref.Property != "" {
+		return s.getProperty(ref.Property)
+	}
+	secret, err := s.toString()
+
+	return []byte(secret), err
+}
+
+func (s *Secret) getItems(ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	secretData := make(map[string][]byte)
+	if ref.Property != "" {
+		value, err := s.getProperty(ref.Property)
+		if err != nil {
+			return nil, err
+		}
+		secretData[ref.Property] = value
+
+		return secretData, nil
+	}
+
+	fields := s.getFields()
+	maps.Copy(secretData, fields)
+	customFields := s.getCustomFields()
+	maps.Copy(secretData, customFields)
+	files := s.getFiles()
+	maps.Copy(secretData, files)
+
+	if len(secretData) == 0 {
+		return nil, fmt.Errorf(errKeeperSecurityNoFields, s.Title)
+	}
+
+	return secretData, nil
+}
+
+func (s *Secret) getField(key string) ([]byte, error) {
+	for _, field := range s.Fields {
+		if field.Type == key && field.Type != keeperSecurityFileRef && field.Type != keeperSecurityMfa && len(field.Value) > 0 {
+			return []byte(field.Value[0]), nil
+		}
+	}
+
+	return nil, fmt.Errorf(errKeeperSecurityInvalidField, key)
+}
+
+func (s *Secret) getFields() map[string][]byte {
+	secretData := make(map[string][]byte)
+	for _, field := range s.Fields {
+		if len(field.Value) > 0 {
+			secretData[field.Type] = []byte(field.Value[0])
+		}
+	}
+
+	return secretData
+}
+
+func (s *Secret) getCustomField(key string) ([]byte, error) {
+	for _, field := range s.Custom {
+		if field.Label == key && len(field.Value) > 0 {
+			return []byte(field.Value[0]), nil
+		}
+	}
+
+	return nil, fmt.Errorf(errKeeperSecurityInvalidField, key)
+}
+
+func (s *Secret) getCustomFields() map[string][]byte {
+	secretData := make(map[string][]byte)
+	for _, field := range s.Custom {
+		if len(field.Value) > 0 {
+			secretData[field.Label] = []byte(field.Value[0])
+		}
+	}
+
+	return secretData
+}
+
+func (s *Secret) getFile(key string) ([]byte, error) {
+	for _, file := range s.Files {
+		if file.Title == key {
+			return []byte(file.Content), nil
+		}
+	}
+
+	return nil, fmt.Errorf(errKeeperSecurityInvalidField, key)
+}
+
+func (s *Secret) getProperty(key string) ([]byte, error) {
+	field, _ := s.getField(key)
+	if field != nil {
+		return field, nil
+	}
+	customField, _ := s.getCustomField(key)
+	if customField != nil {
+		return customField, nil
+	}
+	file, _ := s.getFile(key)
+	if file != nil {
+		return file, nil
+	}
+
+	return nil, fmt.Errorf(errKeeperSecurityInvalidProperty, s.Title, key)
+}
+
+func (s *Secret) getFiles() map[string][]byte {
+	secretData := make(map[string][]byte)
+	for _, file := range s.Files {
+		secretData[file.Title] = []byte(file.Content)
+	}
+
+	return secretData
+}
+
+func (s *Secret) toString() (string, error) {
+	secretJSON, err := json.Marshal(s)
+	if err != nil {
+		return "", fmt.Errorf(errInvalidJSONSecret, s.Title, err)
+	}
+
+	return string(secretJSON), nil
+}

+ 659 - 0
pkg/provider/keepersecurity/client_test.go

@@ -0,0 +1,659 @@
+/*
+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 keepersecurity
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"reflect"
+	"testing"
+
+	ksm "github.com/keeper-security/secrets-manager-go/core"
+
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/provider/keepersecurity/fake"
+)
+
+const (
+	folderID            = "a8ekf031k"
+	validExistingRecord = "record0/login"
+	invalidRecord       = "record5/login"
+	outputRecord0       = "{\"title\":\"record0\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":null,\"files\":null}"
+	outputRecord1       = "{\"title\":\"record1\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":null,\"files\":null}"
+	outputRecord2       = "{\"title\":\"record2\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":null,\"files\":null}"
+	record0             = "record0"
+	record1             = "record1"
+	record2             = "record2"
+	LoginKey            = "login"
+	PasswordKey         = "password"
+	RecordNameFormat    = "record%d"
+)
+
+func TestClientDeleteSecret(t *testing.T) {
+	type fields struct {
+		ksmClient SecurityClient
+		folderID  string
+	}
+	type args struct {
+		ctx       context.Context
+		remoteRef v1beta1.PushRemoteRef
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "Delete valid secret",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					DeleteSecretsFn: func(recrecordUids []string) (map[string]string, error) {
+						return map[string]string{
+							record0: record0,
+						}, nil
+					},
+					GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
+						return generateRecords()[0], nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				context.Background(),
+				&v1alpha1.PushSecretRemoteRef{
+					RemoteKey: validExistingRecord,
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Delete invalid secret type",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
+						return generateRecords()[1], nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				context.Background(),
+				&v1alpha1.PushSecretRemoteRef{
+					RemoteKey: validExistingRecord,
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Delete non existing secret",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
+						return nil, errors.New("failed")
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				context.Background(),
+				&v1alpha1.PushSecretRemoteRef{
+					RemoteKey: invalidRecord,
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &Client{
+				ksmClient: tt.fields.ksmClient,
+				folderID:  tt.fields.folderID,
+			}
+			if err := c.DeleteSecret(tt.args.ctx, tt.args.remoteRef); (err != nil) != tt.wantErr {
+				t.Errorf("DeleteSecret() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestClientGetAllSecrets(t *testing.T) {
+	type fields struct {
+		ksmClient SecurityClient
+		folderID  string
+	}
+	type args struct {
+		ctx context.Context
+		ref v1beta1.ExternalSecretFind
+	}
+	var path = "path_to_fail"
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		want    map[string][]byte
+		wantErr bool
+	}{
+		{
+			name: "Tags not Implemented",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{},
+				folderID:  folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretFind{
+					Tags: map[string]string{
+						"xxx": "yyy",
+					},
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Path not Implemented",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{},
+				folderID:  folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretFind{
+					Path: &path,
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Get secrets with matching regex",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(strings []string) ([]*ksm.Record, error) {
+						return generateRecords(), nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretFind{
+					Name: &v1beta1.FindName{
+						RegExp: "record",
+					},
+				},
+			},
+			want: map[string][]byte{
+				record0: []byte(outputRecord0),
+				record1: []byte(outputRecord1),
+				record2: []byte(outputRecord2),
+			},
+			wantErr: false,
+		},
+		{
+			name: "Get 1 secret with matching regex",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(strings []string) ([]*ksm.Record, error) {
+						return generateRecords(), nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretFind{
+					Name: &v1beta1.FindName{
+						RegExp: record0,
+					},
+				},
+			},
+			want: map[string][]byte{
+				record0: []byte(outputRecord0),
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &Client{
+				ksmClient: tt.fields.ksmClient,
+				folderID:  tt.fields.folderID,
+			}
+			got, err := c.GetAllSecrets(tt.args.ctx, tt.args.ref)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("GetAllSecrets() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("GetAllSecrets() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestClientGetSecret(t *testing.T) {
+	type fields struct {
+		ksmClient SecurityClient
+		folderID  string
+	}
+	type args struct {
+		ctx context.Context
+		ref v1beta1.ExternalSecretDataRemoteRef
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		want    []byte
+		wantErr bool
+	}{
+		{
+			name: "Get Secret with a property",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return []*ksm.Record{generateRecords()[0]}, nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretDataRemoteRef{
+					Key:      record0,
+					Property: LoginKey,
+				},
+			},
+			want:    []byte("foo"),
+			wantErr: false,
+		},
+		{
+			name: "Get Secret without property",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return []*ksm.Record{generateRecords()[0]}, nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretDataRemoteRef{
+					Key: record0,
+				},
+			},
+			want:    []byte(outputRecord0),
+			wantErr: false,
+		},
+		{
+			name: "Get non existing secret",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return nil, errors.New("not found")
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretDataRemoteRef{
+					Key: "record5",
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Get valid secret with non existing property",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return []*ksm.Record{generateRecords()[0]}, nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretDataRemoteRef{
+					Key:      record0,
+					Property: "invalid",
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &Client{
+				ksmClient: tt.fields.ksmClient,
+				folderID:  tt.fields.folderID,
+			}
+			got, err := c.GetSecret(tt.args.ctx, tt.args.ref)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("GetSecret() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("GetSecret() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestClientGetSecretMap(t *testing.T) {
+	type fields struct {
+		ksmClient SecurityClient
+		folderID  string
+	}
+	type args struct {
+		ctx context.Context
+		ref v1beta1.ExternalSecretDataRemoteRef
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		want    map[string][]byte
+		wantErr bool
+	}{
+		{
+			name: "Get Secret with valid property",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return []*ksm.Record{generateRecords()[0]}, nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretDataRemoteRef{
+					Key:      record0,
+					Property: LoginKey,
+				},
+			},
+			want: map[string][]byte{
+				LoginKey: []byte("foo"),
+			},
+			wantErr: false,
+		},
+		{
+			name: "Get Secret without property",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return []*ksm.Record{generateRecords()[0]}, nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretDataRemoteRef{
+					Key: record0,
+				},
+			},
+			want: map[string][]byte{
+				LoginKey:    []byte("foo"),
+				PasswordKey: []byte("bar"),
+			},
+			wantErr: false,
+		},
+		{
+			name: "Get non existing secret",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return nil, errors.New("not found")
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretDataRemoteRef{
+					Key: "record5",
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Get Secret with invalid property",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return []*ksm.Record{generateRecords()[0]}, nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: v1beta1.ExternalSecretDataRemoteRef{
+					Key:      record0,
+					Property: "invalid",
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &Client{
+				ksmClient: tt.fields.ksmClient,
+				folderID:  tt.fields.folderID,
+			}
+			got, err := c.GetSecretMap(tt.args.ctx, tt.args.ref)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("GetSecretMap() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("GetSecretMap() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestClientPushSecret(t *testing.T) {
+	type fields struct {
+		ksmClient SecurityClient
+		folderID  string
+	}
+	type args struct {
+		ctx       context.Context
+		value     []byte
+		remoteRef v1beta1.PushRemoteRef
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "Invalid remote ref",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{},
+				folderID:  folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				remoteRef: v1alpha1.PushSecretRemoteRef{
+					RemoteKey: record0,
+				},
+				value: []byte("foo"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "Push new valid secret",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
+						return nil, errors.New("NotFound")
+					},
+					CreateSecretWithRecordDataFn: func(recUID, folderUid string, recordData *ksm.RecordCreate) (string, error) {
+						return "record5", nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				remoteRef: v1alpha1.PushSecretRemoteRef{
+					RemoteKey: invalidRecord,
+				},
+				value: []byte("foo"),
+			},
+			wantErr: false,
+		},
+		{
+			name: "Push existing valid secret",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
+						return generateRecords()[0], nil
+					},
+					SaveFn: func(record *ksm.Record) error {
+						return nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				remoteRef: v1alpha1.PushSecretRemoteRef{
+					RemoteKey: validExistingRecord,
+				},
+				value: []byte("foo2"),
+			},
+			wantErr: false,
+		},
+		{
+			name: "Push existing invalid secret",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
+						return generateRecords()[1], nil
+					},
+					SaveFn: func(record *ksm.Record) error {
+						return nil
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				remoteRef: v1alpha1.PushSecretRemoteRef{
+					RemoteKey: validExistingRecord,
+				},
+				value: []byte("foo2"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "Unable to push new valid secret",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
+						return nil, errors.New("NotFound")
+					},
+					CreateSecretWithRecordDataFn: func(recUID, folderUID string, recordData *ksm.RecordCreate) (string, error) {
+						return "", errors.New("Unable to push")
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				remoteRef: v1alpha1.PushSecretRemoteRef{
+					RemoteKey: invalidRecord,
+				},
+				value: []byte("foo"),
+			},
+			wantErr: true,
+		},
+		{
+			name: "Unable to save existing valid secret",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
+						return generateRecords()[0], nil
+					},
+					SaveFn: func(record *ksm.Record) error {
+						return errors.New("Unable to save")
+					},
+				},
+				folderID: folderID,
+			},
+			args: args{
+				ctx: context.Background(),
+				remoteRef: v1alpha1.PushSecretRemoteRef{
+					RemoteKey: validExistingRecord,
+				},
+				value: []byte("foo2"),
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &Client{
+				ksmClient: tt.fields.ksmClient,
+				folderID:  tt.fields.folderID,
+			}
+			if err := c.PushSecret(tt.args.ctx, tt.args.value, tt.args.remoteRef); (err != nil) != tt.wantErr {
+				t.Errorf("PushSecret() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func generateRecords() []*ksm.Record {
+	var records []*ksm.Record
+	for i := 0; i < 3; i++ {
+		var record ksm.Record
+		if i == 0 {
+			record = ksm.Record{
+				Uid: fmt.Sprintf(RecordNameFormat, i),
+				RecordDict: map[string]interface{}{
+					"type":      externalSecretType,
+					"folderUID": folderID,
+				},
+			}
+		} else {
+			record = ksm.Record{
+				Uid: fmt.Sprintf(RecordNameFormat, i),
+				RecordDict: map[string]interface{}{
+					"type":      LoginType,
+					"folderUID": folderID,
+				},
+			}
+		}
+		sec := fmt.Sprintf("{\"title\":\"record%d\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}]}", i)
+		record.SetTitle(fmt.Sprintf(RecordNameFormat, i))
+		record.SetStandardFieldValue(LoginKey, "foo")
+		record.SetStandardFieldValue(PasswordKey, "bar")
+		record.RawJson = sec
+		records = append(records, &record)
+	}
+
+	return records
+}

+ 59 - 0
pkg/provider/keepersecurity/fake/fake.go

@@ -0,0 +1,59 @@
+/*
+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 ksm "github.com/keeper-security/secrets-manager-go/core"
+
+type MockKeeperClient struct {
+	GetSecretsFn                 func([]string) ([]*ksm.Record, error)
+	GetSecretByTitleFn           func(recordTitle string) (*ksm.Record, error)
+	CreateSecretWithRecordDataFn func(recUID, folderUID string, recordData *ksm.RecordCreate) (string, error)
+	DeleteSecretsFn              func(recrecordUids []string) (map[string]string, error)
+	SaveFn                       func(record *ksm.Record) error
+}
+
+type GetSecretsMockReturn struct {
+	Secrets []*ksm.Record
+	Err     error
+}
+
+type GetSecretsByTitleMockReturn struct {
+	Secret *ksm.Record
+	Err    error
+}
+
+type CreateSecretWithRecordDataMockReturn struct {
+	ID  string
+	Err error
+}
+
+func (mc *MockKeeperClient) GetSecrets(filter []string) ([]*ksm.Record, error) {
+	return mc.GetSecretsFn(filter)
+}
+
+func (mc *MockKeeperClient) GetSecretByTitle(recordTitle string) (*ksm.Record, error) {
+	return mc.GetSecretByTitleFn(recordTitle)
+}
+
+func (mc *MockKeeperClient) CreateSecretWithRecordData(recUID, folderUID string, recordData *ksm.RecordCreate) (string, error) {
+	return mc.CreateSecretWithRecordDataFn(recUID, folderUID, recordData)
+}
+
+func (mc *MockKeeperClient) DeleteSecrets(recrecordUids []string) (map[string]string, error) {
+	return mc.DeleteSecretsFn(recrecordUids)
+}
+
+func (mc *MockKeeperClient) Save(record *ksm.Record) error {
+	return mc.SaveFn(record)
+}

+ 204 - 0
pkg/provider/keepersecurity/provider.go

@@ -0,0 +1,204 @@
+/*
+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 keepersecurity
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+
+	ksm "github.com/keeper-security/secrets-manager-go/core"
+	"github.com/keeper-security/secrets-manager-go/core/logger"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+	errKeeperSecurityUnableToCreateConfig           = "unable to create valid KeeperSecurity config: %w"
+	errKeeperSecurityStore                          = "received invalid KeeperSecurity SecretStore resource: %s"
+	errKeeperSecurityNilSpec                        = "nil spec"
+	errKeeperSecurityNilSpecProvider                = "nil spec.provider"
+	errKeeperSecurityNilSpecProviderKeeperSecurity  = "nil spec.provider.keepersecurity"
+	errKeeperSecurityStoreMissingAuth               = "missing: spec.provider.keepersecurity.auth"
+	errKeeperSecurityStoreMissingAppKey             = "missing: spec.provider.keepersecurity.auth.appKeySecretRef %w"
+	errKeeperSecurityStoreMissingAppOwnerPublicKey  = "missing: spec.provider.keepersecurity.auth.appOwnerPublicKeySecretRef %w"
+	errKeeperSecurityStoreMissingClientID           = "missing: spec.provider.keepersecurity.auth.clientIdSecretRef %w"
+	errKeeperSecurityStoreMissingPrivateKey         = "missing: spec.provider.keepersecurity.auth.privateKeySecretRef %w"
+	errKeeperSecurityStoreMissingServerPublicKeyID  = "missing: spec.provider.keepersecurity.auth.serverPublicKeyIDSecretRef %w"
+	errKeeperSecurityStoreInvalidConnectHost        = "unable to parse URL: spec.provider.keepersecurity.connectHost: %w"
+	errInvalidClusterStoreMissingK8sSecretNamespace = "invalid ClusterSecretStore: missing KeeperSecurity k8s Auth Secret Namespace"
+	errFetchK8sSecret                               = "could not fetch k8s Secret: %w"
+	errMissingK8sSecretKey                          = "missing Secret key: %s"
+)
+
+// Provider implements the necessary NewClient() and ValidateStore() funcs.
+type Provider struct{}
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.SecretsClient = &Client{}
+var _ esv1beta1.Provider = &Provider{}
+
+func init() {
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+		KeeperSecurity: &esv1beta1.KeeperSecurityProvider{},
+	})
+}
+
+func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+	return esv1beta1.SecretStoreReadWrite
+}
+
+// NewClient constructs a GCP Provider.
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	storeSpec := store.GetSpec()
+	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.KeeperSecurity == nil {
+		return nil, fmt.Errorf(errKeeperSecurityStore, store)
+	}
+
+	keeperStore := storeSpec.Provider.KeeperSecurity
+
+	isClusterKind := store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind
+	clientConfig, err := getKeeperSecurityConfig(ctx, keeperStore, kube, isClusterKind, namespace)
+	if err != nil {
+		return nil, fmt.Errorf(errKeeperSecurityUnableToCreateConfig, err)
+	}
+	ksmClientOptions := &ksm.ClientOptions{
+		Config:   ksm.NewMemoryKeyValueStorage(clientConfig),
+		LogLevel: logger.ErrorLevel,
+	}
+	ksmClient := ksm.NewSecretsManager(ksmClientOptions)
+	client := &Client{
+		folderID:  keeperStore.FolderID,
+		ksmClient: ksmClient,
+	}
+
+	return client, nil
+}
+
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+	if store == nil {
+		return fmt.Errorf(errKeeperSecurityStore, store)
+	}
+	spc := store.GetSpec()
+	if spc == nil {
+		return fmt.Errorf(errKeeperSecurityNilSpec)
+	}
+	if spc.Provider == nil {
+		return fmt.Errorf(errKeeperSecurityNilSpecProvider)
+	}
+	if spc.Provider.KeeperSecurity == nil {
+		return fmt.Errorf(errKeeperSecurityNilSpecProviderKeeperSecurity)
+	}
+
+	// check mandatory fields
+	config := spc.Provider.KeeperSecurity
+
+	// check valid URL
+	if _, err := url.Parse(config.Hostname); err != nil {
+		return fmt.Errorf(errKeeperSecurityStoreInvalidConnectHost, err)
+	}
+
+	if config.Auth == nil {
+		return fmt.Errorf(errKeeperSecurityStoreMissingAuth)
+	}
+
+	if err := utils.ValidateSecretSelector(store, config.Auth.AppKey); err != nil {
+		return fmt.Errorf(errKeeperSecurityStoreMissingAppKey, err)
+	}
+
+	if err := utils.ValidateSecretSelector(store, config.Auth.AppOwnerPublicKey); err != nil {
+		return fmt.Errorf(errKeeperSecurityStoreMissingAppOwnerPublicKey, err)
+	}
+
+	if err := utils.ValidateSecretSelector(store, config.Auth.PrivateKey); err != nil {
+		return fmt.Errorf(errKeeperSecurityStoreMissingPrivateKey, err)
+	}
+
+	if err := utils.ValidateSecretSelector(store, config.Auth.ClientID); err != nil {
+		return fmt.Errorf(errKeeperSecurityStoreMissingClientID, err)
+	}
+
+	if err := utils.ValidateSecretSelector(store, config.Auth.ServerPublicKeyID); err != nil {
+		return fmt.Errorf(errKeeperSecurityStoreMissingServerPublicKeyID, err)
+	}
+
+	return nil
+}
+
+func getKeeperSecurityConfig(ctx context.Context, store *esv1beta1.KeeperSecurityProvider, kube kclient.Client, isClusterKind bool, namespace string) (map[string]string, error) {
+	auth := store.Auth
+	apiKey, err := getAuthParameter(ctx, auth.AppKey, kube, isClusterKind, namespace)
+	if err != nil {
+		return nil, err
+	}
+	appOwnerPublicKey, err := getAuthParameter(ctx, auth.AppOwnerPublicKey, kube, isClusterKind, namespace)
+	if err != nil {
+		return nil, err
+	}
+	clientID, err := getAuthParameter(ctx, auth.ClientID, kube, isClusterKind, namespace)
+	if err != nil {
+		return nil, err
+	}
+	privateKey, err := getAuthParameter(ctx, auth.PrivateKey, kube, isClusterKind, namespace)
+	if err != nil {
+		return nil, err
+	}
+	serverPublicKeyID, err := getAuthParameter(ctx, auth.ServerPublicKeyID, kube, isClusterKind, namespace)
+	if err != nil {
+		return nil, err
+	}
+
+	return map[string]string{
+		"appKey":            apiKey,
+		"appOwnerPublicKey": appOwnerPublicKey,
+		"clientId":          clientID,
+		"hostname":          store.Hostname,
+		"privateKey":        privateKey,
+		"serverPublicKeyID": serverPublicKeyID,
+	}, nil
+}
+
+func getAuthParameter(ctx context.Context, param smmeta.SecretKeySelector, kube kclient.Client, isClusterKind bool, namespace string) (string, error) {
+	credentialsSecret := &v1.Secret{}
+	credentialsSecretName := param.Name
+	objectKey := types.NamespacedName{
+		Name:      credentialsSecretName,
+		Namespace: namespace,
+	}
+
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if isClusterKind {
+		if credentialsSecretName != "" && param.Namespace == nil {
+			return "", fmt.Errorf(errInvalidClusterStoreMissingK8sSecretNamespace)
+		} else if credentialsSecretName != "" {
+			objectKey.Namespace = *param.Namespace
+		}
+	}
+
+	err := kube.Get(ctx, objectKey, credentialsSecret)
+	if err != nil {
+		return "", fmt.Errorf(errFetchK8sSecret, err)
+	}
+	data := credentialsSecret.Data[param.Key]
+	if (data == nil) || (len(data) == 0) {
+		return "", fmt.Errorf(errMissingK8sSecretKey, param.Key)
+	}
+
+	return string(data), nil
+}

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

@@ -26,6 +26,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/keepersecurity"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/kubernetes"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/kubernetes"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/onepassword"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/onepassword"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"