Browse Source

ADD sdkms base implementation (#3180)

* ADD sdkms base implementation

Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com>

* FIX get secret object by name, unmarshalling error formatting

Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com>

* ADD suport for fortanix secret security objects

Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com>

* ADD more tests for opaque, secret, new client

Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com>

* FIX changes required by make reviewable

Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com>

* ADD missing provider registration

Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com>

* FIX remove unused error string, add generated assets

Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com>

---------

Signed-off-by: Recuenco, David <david.recuenco@adidas-group.com>
David Recuenco 2 years ago
parent
commit
af38fc68d5

+ 29 - 0
apis/externalsecrets/v1beta1/secretstore_fortanix_types.go

@@ -0,0 +1,29 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package v1beta1
+
+import esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+
+type FortanixProvider struct {
+	// APIURL is the URL of SDKMS API. Defaults to `sdkms.fortanix.com`.
+	APIURL string `json:"apiUrl,omitempty"`
+
+	// APIKey is the API token to access SDKMS Applications.
+	APIKey *FortanixProviderSecretRef `json:"apiKey,omitempty"`
+}
+
+type FortanixProviderSecretRef struct {
+	// SecretRef is a reference to a secret containing the SDKMS API Key.
+	SecretRef *esmeta.SecretKeySelector `json:"secretRef,omitempty"`
+}

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

@@ -149,6 +149,10 @@ type SecretStoreProvider struct {
 	// Pulumi configures this store to sync secrets using the Pulumi provider
 	// +optional
 	Pulumi *PulumiProvider `json:"pulumi,omitempty"`
+
+	// Fortanix configures this store to sync secrets using the Fortanix provider
+	// +optional
+	Fortanix *FortanixProvider `json:"fortanix,omitempty"`
 }
 
 type CAProviderType string

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

@@ -1376,6 +1376,46 @@ func (in *FindName) DeepCopy() *FindName {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FortanixProvider) DeepCopyInto(out *FortanixProvider) {
+	*out = *in
+	if in.APIKey != nil {
+		in, out := &in.APIKey, &out.APIKey
+		*out = new(FortanixProviderSecretRef)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FortanixProvider.
+func (in *FortanixProvider) DeepCopy() *FortanixProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(FortanixProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *FortanixProviderSecretRef) DeepCopyInto(out *FortanixProviderSecretRef) {
+	*out = *in
+	if in.SecretRef != nil {
+		in, out := &in.SecretRef, &out.SecretRef
+		*out = new(metav1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FortanixProviderSecretRef.
+func (in *FortanixProviderSecretRef) DeepCopy() *FortanixProviderSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(FortanixProviderSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GCPSMAuth) DeepCopyInto(out *GCPSMAuth) {
 	*out = *in
 	if in.SecretRef != nil {
@@ -2094,6 +2134,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(PulumiProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Fortanix != nil {
+		in, out := &in.Fortanix, &out.Fortanix
+		*out = new(FortanixProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.

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

@@ -2574,6 +2574,37 @@ spec:
                     required:
                     - data
                     type: object
+                  fortanix:
+                    description: Fortanix configures this store to sync secrets using
+                      the Fortanix provider
+                    properties:
+                      apiKey:
+                        description: APIKey is the API token to access SDKMS Applications.
+                        properties:
+                          secretRef:
+                            description: SecretRef is a reference to a secret containing
+                              the SDKMS API Key.
+                            properties:
+                              key:
+                                description: |-
+                                  The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
+                                  defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: |-
+                                  Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
+                                  to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                      apiUrl:
+                        description: APIURL is the URL of SDKMS API. Defaults to `sdkms.fortanix.com`.
+                        type: string
+                    type: object
                   gcpsm:
                     description: GCPSM configures this store to sync secrets using
                       Google Cloud Platform Secret Manager provider

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

@@ -2574,6 +2574,37 @@ spec:
                     required:
                     - data
                     type: object
+                  fortanix:
+                    description: Fortanix configures this store to sync secrets using
+                      the Fortanix provider
+                    properties:
+                      apiKey:
+                        description: APIKey is the API token to access SDKMS Applications.
+                        properties:
+                          secretRef:
+                            description: SecretRef is a reference to a secret containing
+                              the SDKMS API Key.
+                            properties:
+                              key:
+                                description: |-
+                                  The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
+                                  defaulted, in others it may be required.
+                                type: string
+                              name:
+                                description: The name of the Secret resource being
+                                  referred to.
+                                type: string
+                              namespace:
+                                description: |-
+                                  Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
+                                  to the namespace of the referent.
+                                type: string
+                            type: object
+                        type: object
+                      apiUrl:
+                        description: APIURL is the URL of SDKMS API. Defaults to `sdkms.fortanix.com`.
+                        type: string
+                    type: object
                   gcpsm:
                     description: GCPSM configures this store to sync secrets using
                       Google Cloud Platform Secret Manager provider

+ 56 - 0
deploy/crds/bundle.yaml

@@ -3007,6 +3007,34 @@ spec:
                       required:
                         - data
                       type: object
+                    fortanix:
+                      description: Fortanix configures this store to sync secrets using the Fortanix provider
+                      properties:
+                        apiKey:
+                          description: APIKey is the API token to access SDKMS Applications.
+                          properties:
+                            secretRef:
+                              description: SecretRef is a reference to a secret containing the SDKMS API Key.
+                              properties:
+                                key:
+                                  description: |-
+                                    The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
+                                    defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
+                                    to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                        apiUrl:
+                          description: APIURL is the URL of SDKMS API. Defaults to `sdkms.fortanix.com`.
+                          type: string
+                      type: object
                     gcpsm:
                       description: GCPSM configures this store to sync secrets using Google Cloud Platform Secret Manager provider
                       properties:
@@ -8069,6 +8097,34 @@ spec:
                       required:
                         - data
                       type: object
+                    fortanix:
+                      description: Fortanix configures this store to sync secrets using the Fortanix provider
+                      properties:
+                        apiKey:
+                          description: APIKey is the API token to access SDKMS Applications.
+                          properties:
+                            secretRef:
+                              description: SecretRef is a reference to a secret containing the SDKMS API Key.
+                              properties:
+                                key:
+                                  description: |-
+                                    The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be
+                                    defaulted, in others it may be required.
+                                  type: string
+                                name:
+                                  description: The name of the Secret resource being referred to.
+                                  type: string
+                                namespace:
+                                  description: |-
+                                    Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults
+                                    to the namespace of the referent.
+                                  type: string
+                              type: object
+                          type: object
+                        apiUrl:
+                          description: APIURL is the URL of SDKMS API. Defaults to `sdkms.fortanix.com`.
+                          type: string
+                      type: object
                     gcpsm:
                       description: GCPSM configures this store to sync secrets using Google Cloud Platform Secret Manager provider
                       properties:

+ 87 - 0
docs/api/spec.md

@@ -3658,6 +3658,79 @@ string
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1beta1.FortanixProvider">FortanixProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>apiUrl</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>APIURL is the URL of SDKMS API. Defaults to <code>sdkms.fortanix.com</code>.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>apiKey</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.FortanixProviderSecretRef">
+FortanixProviderSecretRef
+</a>
+</em>
+</td>
+<td>
+<p>APIKey is the API token to access SDKMS Applications.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.FortanixProviderSecretRef">FortanixProviderSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.FortanixProvider">FortanixProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>secretRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<p>SecretRef is a reference to a secret containing the SDKMS API Key.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.GCPSMAuth">GCPSMAuth
 </h3>
 <p>
@@ -5533,6 +5606,20 @@ PulumiProvider
 <p>Pulumi configures this store to sync secrets using the Pulumi provider</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>fortanix</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.FortanixProvider">
+FortanixProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Fortanix configures this store to sync secrets using the Fortanix provider</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef

+ 50 - 0
docs/provider/fortanix.md

@@ -0,0 +1,50 @@
+## Fortanix DSM / SDKMS
+
+Populate kubernetes secrets from OPAQUE or SECRET security objects in Fortanix.
+
+### Authentication
+
+SDKMS [Application API Key](https://support.fortanix.com/hc/en-us/articles/360015941132-Authentication)
+
+### Creating a SecretStore
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: secret-store
+spec:
+  provider:
+    fortanix:
+      apiUrl: <HOST_OF_SDKMS_API>
+      apiKey:
+        secretRef:
+          name: <NAME_OF_KUBE_SECRET>
+          key: <KEY_IN_KUBE_SECRET>
+```
+
+### Referencing Secrets
+
+```yaml
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: secret
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    kind: SecretStore
+    name: secret-store
+  data:
+
+  # Raw stored value
+  - secretKey: <KEY_IN_KUBE_SECRET>
+    remoteRef:
+      key: <SDKMS_SECURITY_OBJECT_NAME>
+
+  # From stored key-value JSON
+  - secretKey: <KEY_IN_KUBE_SECRET>
+    remoteRef:
+      key: <SDKMS_SECURITY_OBJECT_NAME>
+      property: <SECURITY_OBJECT_VALUE_INNER_PROPERTY>
+```

+ 1 - 0
go.mod

@@ -72,6 +72,7 @@ require (
 	github.com/aliyun/credentials-go v1.3.2
 	github.com/avast/retry-go/v4 v4.5.1
 	github.com/cyberark/conjur-api-go v0.11.1
+	github.com/fortanix/sdkms-client-go v0.4.0
 	github.com/go-openapi/strfmt v0.22.0
 	github.com/golang-jwt/jwt/v5 v5.2.0
 	github.com/hashicorp/golang-lru v1.0.2

+ 2 - 0
go.sum

@@ -288,6 +288,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
 github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fortanix/sdkms-client-go v0.4.0 h1:5cKiFJ4rzc69mhsVVI5Ma5ynr/k5vhvws0yfzfIro/k=
+github.com/fortanix/sdkms-client-go v0.4.0/go.mod h1:gjylIGX+6poVSe+JkbNsLTvseLd+rLjvcGFgXpW56Lo=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=

+ 99 - 0
pkg/provider/fortanix/fortanix.go

@@ -0,0 +1,99 @@
+/*
+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 fortanix
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+
+	"github.com/fortanix/sdkms-client-go/sdkms"
+	corev1 "k8s.io/api/core/v1"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+type client struct {
+	sdkms sdkms.Client
+}
+
+const (
+	errPushSecretsNotSupported       = "pushing secrets is currently not supported"
+	errDeleteSecretsNotSupported     = "deleting secrets is currently not supported"
+	errUnmarshalSecret               = "unable to unmarshal secret, is it a valid JSON?: %w"
+	errUnableToGetValue              = "unable to get value for key %s"
+	errGettingSecretMapNotSupported  = "getting secret map is currently not supported"
+	errGettingAllSecretsNotSupported = "getting all secrets is currently not supported"
+)
+
+func (c *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	securityObject, err := c.sdkms.GetSobject(ctx, &sdkms.GetSobjectParams{}, *sdkms.SobjectByName(ref.Key))
+
+	if err != nil {
+		return nil, err
+	}
+
+	if securityObject.ObjType == sdkms.ObjectTypeSecret {
+		securityObject, err = c.sdkms.ExportSobject(ctx, *sdkms.SobjectByID(*securityObject.Kid))
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if ref.Property == "" {
+		return *securityObject.Value, nil
+	}
+
+	kv := make(map[string]string)
+
+	err = json.Unmarshal(*securityObject.Value, &kv)
+	if err != nil {
+		return nil, fmt.Errorf(errUnmarshalSecret, err)
+	}
+
+	value, ok := kv[ref.Property]
+
+	if !ok {
+		return nil, fmt.Errorf(errUnableToGetValue, ref.Property)
+	}
+
+	return utils.GetByteValue(value)
+}
+
+func (c *client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
+	return errors.New(errPushSecretsNotSupported)
+}
+
+func (c *client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
+	return errors.New(errDeleteSecretsNotSupported)
+}
+
+func (c *client) Validate() (esv1beta1.ValidationResult, error) {
+	return esv1beta1.ValidationResultReady, nil
+}
+
+func (c *client) GetSecretMap(_ context.Context, _ esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	return nil, errors.New(errGettingSecretMapNotSupported)
+}
+
+func (c *client) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	return nil, errors.New(errGettingAllSecretsNotSupported)
+}
+
+func (c *client) Close(context.Context) error {
+	return nil
+}

+ 152 - 0
pkg/provider/fortanix/fortanix_test.go

@@ -0,0 +1,152 @@
+/*
+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 fortanix
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/fortanix/sdkms-client-go/sdkms"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+)
+
+func newTestClient(t *testing.T, handler func(w http.ResponseWriter, r *http.Request)) *client {
+	const apiKey = "api-key"
+
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		handler(w, r)
+	}))
+	t.Cleanup(server.Close)
+
+	return &client{
+		sdkms: sdkms.Client{
+			HTTPClient: http.DefaultClient,
+			Auth:       sdkms.APIKey(apiKey),
+			Endpoint:   server.URL,
+		},
+	}
+}
+
+func toJSON(t *testing.T, v interface{}) []byte {
+	jsonBytes, err := json.Marshal(v)
+	assert.Nil(t, err)
+	return jsonBytes
+}
+
+type testSecurityObjectValue struct {
+	Property string `json:"property"`
+}
+
+func TestGetOpaqueSecurityObject(t *testing.T) {
+	ctx := context.Background()
+	securityObjectName := "securityObjectName"
+
+	securityObjectValue := toJSON(t, testSecurityObjectValue{
+		Property: "value",
+	})
+
+	securityObjectUser := "user"
+
+	securityObject := sdkms.Sobject{
+		Creator: sdkms.Principal{
+			User: &securityObjectUser,
+		},
+		Name:  &securityObjectName,
+		Value: &securityObjectValue,
+	}
+
+	client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
+		err := json.NewEncoder(w).Encode(securityObject)
+		require.NoError(t, err)
+	})
+
+	t.Run("get raw secret value from opaque security object", func(t *testing.T) {
+		ref := esv1beta1.ExternalSecretDataRemoteRef{
+			Key: securityObjectName,
+		}
+
+		got, err := client.GetSecret(ctx, ref)
+
+		assert.NoError(t, err)
+		assert.Equal(t, securityObjectValue, got)
+	})
+
+	t.Run("get inner property value from opaque security object", func(t *testing.T) {
+		ref := esv1beta1.ExternalSecretDataRemoteRef{
+			Key:      securityObjectName,
+			Property: "property",
+		}
+
+		got, err := client.GetSecret(ctx, ref)
+
+		assert.NoError(t, err)
+		assert.Equal(t, []byte(`value`), got)
+	})
+}
+
+func TestGetSecretSecurityObject(t *testing.T) {
+	ctx := context.Background()
+	securityObjectName := "securityObjectName"
+	securityObjectID := "id"
+
+	securityObjectValue := toJSON(t, testSecurityObjectValue{
+		Property: "value",
+	})
+
+	securityObjectUser := "user"
+
+	securityObject := sdkms.Sobject{
+		Creator: sdkms.Principal{
+			User: &securityObjectUser,
+		},
+		Name:    &securityObjectName,
+		Kid:     &securityObjectID,
+		Value:   &securityObjectValue,
+		ObjType: sdkms.ObjectTypeSecret,
+	}
+
+	client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
+		err := json.NewEncoder(w).Encode(securityObject)
+		require.NoError(t, err)
+	})
+
+	t.Run("get raw secret value from secret security object", func(t *testing.T) {
+		ref := esv1beta1.ExternalSecretDataRemoteRef{
+			Key: securityObjectName,
+		}
+
+		got, err := client.GetSecret(ctx, ref)
+
+		assert.NoError(t, err)
+		assert.Equal(t, securityObjectValue, got)
+	})
+
+	t.Run("get inner property value from secret security object", func(t *testing.T) {
+		ref := esv1beta1.ExternalSecretDataRemoteRef{
+			Key:      securityObjectName,
+			Property: "property",
+		}
+
+		got, err := client.GetSecret(ctx, ref)
+
+		assert.NoError(t, err)
+		assert.Equal(t, []byte(`value`), got)
+	})
+}

+ 124 - 0
pkg/provider/fortanix/provider.go

@@ -0,0 +1,124 @@
+/*
+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 fortanix
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/fortanix/sdkms-client-go/sdkms"
+	kubeclient "sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+type Provider struct{}
+
+const (
+	errCannotResolveSecretKeyRef     = "cannot resolve secret key ref: %w"
+	errStoreIsNil                    = "store is nil"
+	errNoStoreTypeOrWrongStoreType   = "no store type or wrong store type"
+	errAPIKeyIsRequired              = "apiKey is required"
+	errAPIKeySecretRefIsRequired     = "apiKey.secretRef is required"
+	errAPIKeySecretRefNameIsRequired = "apiKey.secretRef.name is required"
+	errAPIKeySecretRefKeyIsRequired  = "apiKey.secretRef.key is required"
+)
+
+var _ esv1beta1.Provider = &Provider{}
+
+func init() {
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+		Fortanix: &esv1beta1.FortanixProvider{},
+	})
+}
+
+func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+	return esv1beta1.SecretStoreReadOnly
+}
+
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kubeclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	config, err := getConfig(store)
+	if err != nil {
+		return nil, err
+	}
+
+	apiKey, err := resolvers.SecretKeyRef(ctx, kube, store.GetKind(), namespace, config.APIKey.SecretRef)
+	if err != nil {
+		return nil, fmt.Errorf(errCannotResolveSecretKeyRef, err)
+	}
+
+	sdkmsClient := sdkms.Client{
+		HTTPClient: http.DefaultClient,
+		Auth:       sdkms.APIKey(apiKey),
+		Endpoint:   config.APIURL,
+	}
+
+	return &client{
+		sdkms: sdkmsClient,
+	}, nil
+}
+
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
+	_, err := getConfig(store)
+	return nil, err
+}
+
+func getConfig(store esv1beta1.GenericStore) (*esv1beta1.FortanixProvider, error) {
+	if store == nil {
+		return nil, errors.New(errStoreIsNil)
+	}
+
+	spec := store.GetSpec()
+	if spec == nil || spec.Provider == nil || spec.Provider.Fortanix == nil {
+		return nil, errors.New(errNoStoreTypeOrWrongStoreType)
+	}
+
+	config := spec.Provider.Fortanix
+
+	if config.APIURL == "" {
+		config.APIURL = "https://sdkms.fortanix.com"
+	}
+
+	err := validateSecretStoreRef(store, config.APIKey)
+	if err != nil {
+		return nil, err
+	}
+
+	return config, nil
+}
+
+func validateSecretStoreRef(store esv1beta1.GenericStore, ref *esv1beta1.FortanixProviderSecretRef) error {
+	if ref == nil {
+		return errors.New(errAPIKeyIsRequired)
+	}
+
+	if ref.SecretRef == nil {
+		return errors.New(errAPIKeySecretRefIsRequired)
+	}
+
+	if ref.SecretRef.Name == "" {
+		return errors.New(errAPIKeySecretRefNameIsRequired)
+	}
+
+	if ref.SecretRef.Key == "" {
+		return errors.New(errAPIKeySecretRefKeyIsRequired)
+	}
+
+	return utils.ValidateReferentSecretSelector(store, *ref.SecretRef)
+}

+ 219 - 0
pkg/provider/fortanix/provider_test.go

@@ -0,0 +1,219 @@
+/*
+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 fortanix
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/tools/clientcmd"
+	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
+	kubeclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+func pointer[T any](d T) *T {
+	return &d
+}
+
+func respondJSON(w http.ResponseWriter, data interface{}) {
+	w.Header().Set("Content-Type", "application/json")
+
+	json.NewEncoder(w).Encode(data)
+}
+
+func createMockKubernetesClient(t *testing.T) kubeclient.Client {
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch r.URL.Path {
+		case "/api/v1":
+			respondJSON(w, metav1.APIResourceList{
+				APIResources: []metav1.APIResource{
+					{
+						Name:       "secrets",
+						Namespaced: true,
+						Kind:       "Secret",
+						Verbs: metav1.Verbs{
+							"get",
+						},
+					},
+				},
+			})
+		case "/api/v1/namespaces/test/secrets/secret-name":
+			respondJSON(w, corev1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "secret-name",
+				},
+				Data: map[string][]byte{
+					"apiKey": []byte("apiKey"),
+				},
+			})
+		case "/api/v1/namespaces/test/secrets/missing-secret":
+			w.WriteHeader(404)
+			respondJSON(w, metav1.Status{
+				Code: 404,
+			})
+		}
+	}))
+	t.Cleanup(server.Close)
+
+	clientConfig := clientcmd.NewDefaultClientConfig(clientcmdapi.Config{
+		Clusters: map[string]*clientcmdapi.Cluster{
+			"test": {
+				Server: server.URL,
+			},
+		},
+		AuthInfos: map[string]*clientcmdapi.AuthInfo{
+			"test": {
+				Token: "token",
+			},
+		},
+		Contexts: map[string]*clientcmdapi.Context{
+			"test": {
+				Cluster:  "test",
+				AuthInfo: "test",
+			},
+		},
+		CurrentContext: "test",
+	}, &clientcmd.ConfigOverrides{})
+
+	restConfig, err := clientConfig.ClientConfig()
+	assert.Nil(t, err)
+	c, err := kubeclient.New(restConfig, kubeclient.Options{})
+	assert.Nil(t, err)
+
+	return c
+}
+
+func TestNewClient(t *testing.T) {
+	t.Run("should create new client", func(t *testing.T) {
+		ctx := context.Background()
+		p := &Provider{}
+		c := createMockKubernetesClient(t)
+		s := esv1beta1.SecretStore{
+			Spec: esv1beta1.SecretStoreSpec{
+				Provider: &esv1beta1.SecretStoreProvider{
+					Fortanix: &esv1beta1.FortanixProvider{
+						APIKey: &esv1beta1.FortanixProviderSecretRef{
+							SecretRef: &v1.SecretKeySelector{
+								Name: "secret-name",
+								Key:  "apiKey",
+							},
+						},
+					},
+				},
+			},
+		}
+
+		_, err := p.NewClient(ctx, &s, c, "test")
+
+		assert.Nil(t, err)
+	})
+
+	t.Run("should fail to create new client if secret is missing", func(t *testing.T) {
+		ctx := context.Background()
+		p := &Provider{}
+		c := createMockKubernetesClient(t)
+		s := esv1beta1.SecretStore{
+			Spec: esv1beta1.SecretStoreSpec{
+				Provider: &esv1beta1.SecretStoreProvider{
+					Fortanix: &esv1beta1.FortanixProvider{
+						APIKey: &esv1beta1.FortanixProviderSecretRef{
+							SecretRef: &v1.SecretKeySelector{
+								Name: "missing-secret",
+								Key:  "apiKey",
+							},
+						},
+					},
+				},
+			},
+		}
+
+		_, err := p.NewClient(ctx, &s, c, "test")
+
+		assert.ErrorContains(t, err, "cannot resolve secret key ref")
+	})
+}
+
+func TestValidateStore(t *testing.T) {
+	tests := map[string]struct {
+		cfg  esv1beta1.FortanixProvider
+		want error
+	}{
+		"missing api key": {
+			cfg:  esv1beta1.FortanixProvider{},
+			want: errors.New("apiKey is required"),
+		},
+		"missing api key secret ref": {
+			cfg: esv1beta1.FortanixProvider{
+				APIKey: &esv1beta1.FortanixProviderSecretRef{},
+			},
+			want: errors.New("apiKey.secretRef is required"),
+		},
+		"missing api key secret ref name": {
+			cfg: esv1beta1.FortanixProvider{
+				APIKey: &esv1beta1.FortanixProviderSecretRef{
+					SecretRef: &v1.SecretKeySelector{
+						Key: "key",
+					},
+				},
+			},
+			want: errors.New("apiKey.secretRef.name is required"),
+		},
+		"missing api key secret ref key": {
+			cfg: esv1beta1.FortanixProvider{
+				APIKey: &esv1beta1.FortanixProviderSecretRef{
+					SecretRef: &v1.SecretKeySelector{
+						Name: "name",
+					},
+				},
+			},
+			want: errors.New("apiKey.secretRef.key is required"),
+		},
+		"disallowed namespace in store ref": {
+			cfg: esv1beta1.FortanixProvider{
+				APIKey: &esv1beta1.FortanixProviderSecretRef{
+					SecretRef: &v1.SecretKeySelector{
+						Key:       "key",
+						Name:      "name",
+						Namespace: pointer("namespace"),
+					},
+				},
+			},
+			want: errors.New("namespace not allowed with namespaced SecretStore"),
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			s := esv1beta1.SecretStore{
+				Spec: esv1beta1.SecretStoreSpec{
+					Provider: &esv1beta1.SecretStoreProvider{
+						Fortanix: &tc.cfg,
+					},
+				},
+			}
+			p := &Provider{}
+			_, got := p.ValidateStore(&s)
+			assert.Equal(t, tc.want, got)
+		})
+	}
+}

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

@@ -27,6 +27,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/delinea"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/doppler"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/fake"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/fortanix"
 	_ "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/ibm"