diff --git a/apis/externalsecrets/v1beta1/secretsstore_infisical_types.go b/apis/externalsecrets/v1beta1/secretsstore_infisical_types.go
new file mode 100644
index 000000000..c1eea0a4d
--- /dev/null
+++ b/apis/externalsecrets/v1beta1/secretsstore_infisical_types.go
@@ -0,0 +1,53 @@
+/*
+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 UniversalAuthCredentials struct {
+ // +kubebuilder:validation:Required
+ ClientID esmeta.SecretKeySelector `json:"clientId"`
+ // +kubebuilder:validation:Required
+ ClientSecret esmeta.SecretKeySelector `json:"clientSecret"`
+}
+
+type InfisicalAuth struct {
+ // +optional
+ UniversalAuthCredentials *UniversalAuthCredentials `json:"universalAuthCredentials,omitempty"`
+}
+
+type MachineIdentityScopeInWorkspace struct {
+ // +kubebuilder:default="/"
+ // +optional
+ SecretsPath string `json:"secretsPath,omitempty"`
+ // +kubebuilder:validation:Required
+ EnvironmentSlug string `json:"environmentSlug"`
+ // +kubebuilder:validation:Required
+ ProjectSlug string `json:"projectSlug"`
+}
+
+// InfisicalProvider configures a store to sync secrets using the Infisical provider.
+type InfisicalProvider struct {
+ // Auth configures how the Operator authenticates with the Infisical API
+ // +kubebuilder:validation:Required
+ Auth InfisicalAuth `json:"auth"`
+ // +kubebuilder:validation:Required
+ SecretsScope MachineIdentityScopeInWorkspace `json:"secretsScope"`
+ // +kubebuilder:default="https://app.infisical.com/api"
+ // +optional
+ HostAPI string `json:"hostAPI,omitempty"`
+}
diff --git a/apis/externalsecrets/v1beta1/secretstore_types.go b/apis/externalsecrets/v1beta1/secretstore_types.go
index b05d32e44..c168f7557 100644
--- a/apis/externalsecrets/v1beta1/secretstore_types.go
+++ b/apis/externalsecrets/v1beta1/secretstore_types.go
@@ -163,6 +163,10 @@ type SecretStoreProvider struct {
// +optional
Passbolt *PassboltProvider `json:"passbolt,omitempty"`
+
+ // Infisical configures this store to sync secrets using the Infisical provider
+ // +optional
+ Infisical *InfisicalProvider `json:"infisical,omitempty"`
}
type CAProviderType string
diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
index db570a763..a1f5b6688 100644
--- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
+++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
@@ -1669,6 +1669,43 @@ func (in *IBMProvider) DeepCopy() *IBMProvider {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InfisicalAuth) DeepCopyInto(out *InfisicalAuth) {
+ *out = *in
+ if in.UniversalAuthCredentials != nil {
+ in, out := &in.UniversalAuthCredentials, &out.UniversalAuthCredentials
+ *out = new(UniversalAuthCredentials)
+ (*in).DeepCopyInto(*out)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalAuth.
+func (in *InfisicalAuth) DeepCopy() *InfisicalAuth {
+ if in == nil {
+ return nil
+ }
+ out := new(InfisicalAuth)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InfisicalProvider) DeepCopyInto(out *InfisicalProvider) {
+ *out = *in
+ in.Auth.DeepCopyInto(&out.Auth)
+ out.SecretsScope = in.SecretsScope
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalProvider.
+func (in *InfisicalProvider) DeepCopy() *InfisicalProvider {
+ if in == nil {
+ return nil
+ }
+ out := new(InfisicalProvider)
+ 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
@@ -1757,6 +1794,21 @@ func (in *KubernetesServer) DeepCopy() *KubernetesServer {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *MachineIdentityScopeInWorkspace) DeepCopyInto(out *MachineIdentityScopeInWorkspace) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineIdentityScopeInWorkspace.
+func (in *MachineIdentityScopeInWorkspace) DeepCopy() *MachineIdentityScopeInWorkspace {
+ if in == nil {
+ return nil
+ }
+ out := new(MachineIdentityScopeInWorkspace)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NoSecretError) DeepCopyInto(out *NoSecretError) {
*out = *in
@@ -2305,6 +2357,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(PassboltProvider)
(*in).DeepCopyInto(*out)
}
+ if in.Infisical != nil {
+ in, out := &in.Infisical, &out.Infisical
+ *out = new(InfisicalProvider)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
@@ -2616,6 +2673,23 @@ func (in *TokenAuth) DeepCopy() *TokenAuth {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UniversalAuthCredentials) DeepCopyInto(out *UniversalAuthCredentials) {
+ *out = *in
+ in.ClientID.DeepCopyInto(&out.ClientID)
+ in.ClientSecret.DeepCopyInto(&out.ClientSecret)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UniversalAuthCredentials.
+func (in *UniversalAuthCredentials) DeepCopy() *UniversalAuthCredentials {
+ if in == nil {
+ return nil
+ }
+ out := new(UniversalAuthCredentials)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VaultAppRole) DeepCopyInto(out *VaultAppRole) {
*out = *in
diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
index 3771ea0b6..2b6ccc9a6 100644
--- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml
+++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml
@@ -2883,6 +2883,81 @@ spec:
required:
- auth
type: object
+ infisical:
+ description: Infisical configures this store to sync secrets using
+ the Infisical provider
+ properties:
+ auth:
+ description: Auth configures how the Operator authenticates
+ with the Infisical API
+ properties:
+ universalAuthCredentials:
+ properties:
+ clientId:
+ 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
+ clientSecret:
+ 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:
+ - clientId
+ - clientSecret
+ type: object
+ type: object
+ hostAPI:
+ default: https://app.infisical.com/api
+ type: string
+ secretsScope:
+ properties:
+ environmentSlug:
+ type: string
+ projectSlug:
+ type: string
+ secretsPath:
+ default: /
+ type: string
+ required:
+ - environmentSlug
+ - projectSlug
+ type: object
+ required:
+ - auth
+ - secretsScope
+ type: object
keepersecurity:
description: KeeperSecurity configures this store to sync secrets
using the KeeperSecurity provider
diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml
index 6bffcf681..d650808b8 100644
--- a/config/crds/bases/external-secrets.io_secretstores.yaml
+++ b/config/crds/bases/external-secrets.io_secretstores.yaml
@@ -2883,6 +2883,81 @@ spec:
required:
- auth
type: object
+ infisical:
+ description: Infisical configures this store to sync secrets using
+ the Infisical provider
+ properties:
+ auth:
+ description: Auth configures how the Operator authenticates
+ with the Infisical API
+ properties:
+ universalAuthCredentials:
+ properties:
+ clientId:
+ 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
+ clientSecret:
+ 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:
+ - clientId
+ - clientSecret
+ type: object
+ type: object
+ hostAPI:
+ default: https://app.infisical.com/api
+ type: string
+ secretsScope:
+ properties:
+ environmentSlug:
+ type: string
+ projectSlug:
+ type: string
+ secretsPath:
+ default: /
+ type: string
+ required:
+ - environmentSlug
+ - projectSlug
+ type: object
+ required:
+ - auth
+ - secretsScope
+ type: object
keepersecurity:
description: KeeperSecurity configures this store to sync secrets
using the KeeperSecurity provider
diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml
index 28f2d2b8a..54e5bfdf3 100644
--- a/deploy/crds/bundle.yaml
+++ b/deploy/crds/bundle.yaml
@@ -3337,6 +3337,77 @@ spec:
required:
- auth
type: object
+ infisical:
+ description: Infisical configures this store to sync secrets using the Infisical provider
+ properties:
+ auth:
+ description: Auth configures how the Operator authenticates with the Infisical API
+ properties:
+ universalAuthCredentials:
+ properties:
+ clientId:
+ 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
+ clientSecret:
+ 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:
+ - clientId
+ - clientSecret
+ type: object
+ type: object
+ hostAPI:
+ default: https://app.infisical.com/api
+ type: string
+ secretsScope:
+ properties:
+ environmentSlug:
+ type: string
+ projectSlug:
+ type: string
+ secretsPath:
+ default: /
+ type: string
+ required:
+ - environmentSlug
+ - projectSlug
+ type: object
+ required:
+ - auth
+ - secretsScope
+ type: object
keepersecurity:
description: KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
properties:
@@ -8712,6 +8783,77 @@ spec:
required:
- auth
type: object
+ infisical:
+ description: Infisical configures this store to sync secrets using the Infisical provider
+ properties:
+ auth:
+ description: Auth configures how the Operator authenticates with the Infisical API
+ properties:
+ universalAuthCredentials:
+ properties:
+ clientId:
+ 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
+ clientSecret:
+ 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:
+ - clientId
+ - clientSecret
+ type: object
+ type: object
+ hostAPI:
+ default: https://app.infisical.com/api
+ type: string
+ secretsScope:
+ properties:
+ environmentSlug:
+ type: string
+ projectSlug:
+ type: string
+ secretsPath:
+ default: /
+ type: string
+ required:
+ - environmentSlug
+ - projectSlug
+ type: object
+ required:
+ - auth
+ - secretsScope
+ type: object
keepersecurity:
description: KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
properties:
diff --git a/docs/api/spec.md b/docs/api/spec.md
index 6d521a384..efd79408a 100644
--- a/docs/api/spec.md
+++ b/docs/api/spec.md
@@ -4372,6 +4372,92 @@ string
+
InfisicalAuth
+
+
+(Appears on:
+InfisicalProvider)
+
+
+
+
+InfisicalProvider
+
+
+(Appears on:
+SecretStoreProvider)
+
+
+
InfisicalProvider configures a store to sync secrets using the Infisical provider.
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+auth
+
+
+InfisicalAuth
+
+
+ |
+
+ Auth configures how the Operator authenticates with the Infisical API
+ |
+
+
+
+secretsScope
+
+
+MachineIdentityScopeInWorkspace
+
+
+ |
+
+ |
+
+
+
+hostAPI
+
+string
+
+ |
+
+(Optional)
+ |
+
+
+
KeeperSecurityProvider
@@ -4586,6 +4672,55 @@ CAProvider
+
MachineIdentityScopeInWorkspace
+
+
+(Appears on:
+InfisicalProvider)
+
+
+
+
+
+
+Field |
+Description |
+
+
+
+
+
+secretsPath
+
+string
+
+ |
+
+(Optional)
+ |
+
+
+
+environmentSlug
+
+string
+
+ |
+
+ |
+
+
+
+projectSlug
+
+string
+
+ |
+
+ |
+
+
+
NoSecretError
@@ -6055,6 +6190,20 @@ PassboltProvider
(Optional)
+
+
+infisical
+
+
+InfisicalProvider
+
+
+ |
+
+(Optional)
+ Infisical configures this store to sync secrets using the Infisical provider
+ |
+
SecretStoreRef
@@ -6932,6 +7081,48 @@ External Secrets meta/v1.SecretKeySelector
+UniversalAuthCredentials
+
+
+(Appears on:
+InfisicalAuth)
+
+
+
+
ValidationResult
(byte
alias)
diff --git a/docs/introduction/stability-support.md b/docs/introduction/stability-support.md
index cabcd8710..eb639b0a5 100644
--- a/docs/introduction/stability-support.md
+++ b/docs/introduction/stability-support.md
@@ -55,6 +55,7 @@ The following table describes the stability level of each provider and who's res
| [Delinea](https://external-secrets.io/latest/provider/delinea) | alpha | [@michaelsauter](https://github.com/michaelsauter/) |
| [Pulumi ESC](https://external-secrets.io/latest/provider/pulumi) | alpha | [@dirien](https://github.com/dirien) |
| [Passbolt](https://external-secrets.io/latest/provider/passbolt) | alpha | |
+| [Infisical](https://external-secrets.io/latest/provider/infisical) | alpha | [@akhilmhdh](https://github.com/akhilmhdh) |
## Provider Feature Support
@@ -84,6 +85,7 @@ The following table show the support for features across different providers.
| Delinea | x | | | | x | | |
| Pulumi ESC | x | | | | x | | |
| Passbolt | x | | | | x | | |
+| Infisical | x | | | x | x | | |
## Support Policy
diff --git a/docs/pictures/external-secrets-operator.png b/docs/pictures/external-secrets-operator.png
new file mode 100644
index 000000000..5264eeea3
Binary files /dev/null and b/docs/pictures/external-secrets-operator.png differ
diff --git a/docs/provider/hashicorp-vault.md b/docs/provider/hashicorp-vault.md
index 27c271c32..ceeaa32d2 100644
--- a/docs/provider/hashicorp-vault.md
+++ b/docs/provider/hashicorp-vault.md
@@ -364,7 +364,7 @@ set of AWS Programmatic access credentials stored in a `Kind=Secret` and referen
### Mutual authentication (mTLS)
-Under specific compliance requirements, the Vault server can be set up to enforce mutual authentication from clients across all APIs by configuring the server with `tls_require_and_verify_client_cert = true`. This configuration differs fundamentally from the [TLS certificates auth method](#TLS-certificates-authentication). While the TLS certificates auth method allows the issuance of a Vault token through the `/v1/auth/cert/login` API, the mTLS configuration solely focuses on TLS transport layer authentication and lacks any authorization-related capabilities. It's important to note that the Vault token must still be included in the request, following any of the supported authentication methods mentioned earlier.
+Under specific compliance requirements, the Vault server can be set up to enforce mutual authentication from clients across all APIs by configuring the server with `tls_require_and_verify_client_cert = true`. This configuration differs fundamentally from the [TLS certificates auth method](#tls-certificates-authentication). While the TLS certificates auth method allows the issuance of a Vault token through the `/v1/auth/cert/login` API, the mTLS configuration solely focuses on TLS transport layer authentication and lacks any authorization-related capabilities. It's important to note that the Vault token must still be included in the request, following any of the supported authentication methods mentioned earlier.
```yaml
{% include 'vault-mtls-store.yaml' %}
diff --git a/docs/provider/infisical.md b/docs/provider/infisical.md
new file mode 100644
index 000000000..617730bf9
--- /dev/null
+++ b/docs/provider/infisical.md
@@ -0,0 +1,68 @@
+![Infisical k8s Diagram](../pictures/external-secrets-operator.png)
+
+Sync secrets from [Infisical](https://www.infisical.com) to your Kubernetes cluster using External Secrets Operator.
+
+## Authentication
+In order for the operator to fetch secrets from Infisical, it needs to first authenticate with Infisical.
+
+To authenticate, you can use [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) from [Machine identities](https://infisical.com/docs/documentation/platform/identities/machine-identities).
+
+Follow the [guide here](https://infisical.com/docs/documentation/platform/identities/universal-auth) to learn how to create and obtain a pair of Client Secret and Client ID.
+
+## Storing Your Machine Identity Secrets
+
+Once you have generated a pair of `Client ID` and `Client Secret`, you will need to store these credentials in your cluster as a Kubernetes secret.
+
+!!! note inline end
+ Remember to replace with your own Machine Identity credentials.
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ name: universal-auth-credentials
+type: Opaque
+
+stringData:
+ clientId:
+ clientSecret:
+```
+
+### Secret Store
+
+You will then need to create a generic `SecretStore`. An sample `SecretStore` has been is shown below.
+
+!!! tip inline end
+ To get your project slug from Infisical, head over to the project settings and click the button `Copy Project Slug`.
+
+```yaml
+{% include 'infisical-generic-secret-store.yaml' %}
+```
+
+!!! Note
+ For `ClusterSecretStore`, be sure to set `namespace` in `universalAuthCredentials.clientId` and `universalAuthCredentials.clientSecret`.
+
+## Fetch Individual Secret(s)
+
+To sync one or more secrets individually, use the following YAML:
+
+```yaml
+{% include 'infisical-fetch-secret.yaml' %}
+```
+
+## Fetch All Secrets
+
+To sync all secrets from an Infisical , use the following YAML:
+
+``` yaml
+{% include 'infisical-fetch-all-secrets.yaml' %}
+```
+
+## Filter By Prefix/Name
+
+To filter secrets by `path` (path prefix) and `name` (regular expression).
+
+``` yaml
+{% include 'infisical-filtered-secrets.yaml' %}
+```
+
diff --git a/docs/snippets/infisical-fetch-all-secrets.yaml b/docs/snippets/infisical-fetch-all-secrets.yaml
new file mode 100644
index 000000000..e0ff9a5c9
--- /dev/null
+++ b/docs/snippets/infisical-fetch-all-secrets.yaml
@@ -0,0 +1,16 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: infisical-managed-secrets
+spec:
+ secretStoreRef:
+ kind: SecretStore
+ name: infisical
+
+ target:
+ name: auth-api
+
+ dataFrom:
+ - find:
+ name:
+ regexp: .*
diff --git a/docs/snippets/infisical-fetch-secret.yaml b/docs/snippets/infisical-fetch-secret.yaml
new file mode 100644
index 000000000..eeb8153e3
--- /dev/null
+++ b/docs/snippets/infisical-fetch-secret.yaml
@@ -0,0 +1,16 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: infisical-managed-secrets
+spec:
+ secretStoreRef:
+ kind: SecretStore
+ name: infisical
+
+ target:
+ name: auth-api
+
+ data:
+ - secretKey: API_KEY
+ remoteRef:
+ key: API_KEY
diff --git a/docs/snippets/infisical-filtered-secrets.yaml b/docs/snippets/infisical-filtered-secrets.yaml
new file mode 100644
index 000000000..51f7d4985
--- /dev/null
+++ b/docs/snippets/infisical-filtered-secrets.yaml
@@ -0,0 +1,15 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: infisical-managed-secrets
+spec:
+ secretStoreRef:
+ kind: SecretStore
+ name: infisical
+
+ target:
+ name: auth-api
+
+ dataFrom:
+ - find:
+ path: DB_
diff --git a/docs/snippets/infisical-generic-secret-store.yaml b/docs/snippets/infisical-generic-secret-store.yaml
new file mode 100644
index 000000000..c3f1e7c3b
--- /dev/null
+++ b/docs/snippets/infisical-generic-secret-store.yaml
@@ -0,0 +1,25 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+ name: infisical
+spec:
+ provider:
+ infisical:
+ auth:
+ universalAuthCredentials:
+ clientId:
+ key: clientId
+ namespace: default
+ name: universal-auth-credentials
+ clientSecret:
+ key: clientSecret
+ namespace: default
+ name: universal-auth-credentials
+ # Details to pull secrets from
+ secretsScope:
+ projectSlug: first-project-fujo
+ environmentSlug: dev # "dev", "staging", "prod", etc..
+ # optional
+ secretsPath: / # Root is "/"
+ # optional
+ hostAPI: https://app.infisical.com
diff --git a/hack/api-docs/mkdocs.yml b/hack/api-docs/mkdocs.yml
index 17367e81a..0d090e530 100644
--- a/hack/api-docs/mkdocs.yml
+++ b/hack/api-docs/mkdocs.yml
@@ -39,12 +39,12 @@ extra:
property: G-QP38TD8K7V
nav:
- Introduction:
- - Introduction: index.md
- - Overview: introduction/overview.md
- - Getting started: introduction/getting-started.md
- - FAQ: introduction/faq.md
- - Stability and Support: introduction/stability-support.md
- - Deprecation Policy: introduction/deprecation-policy.md
+ - Introduction: index.md
+ - Overview: introduction/overview.md
+ - Getting started: introduction/getting-started.md
+ - FAQ: introduction/faq.md
+ - Stability and Support: introduction/stability-support.md
+ - Deprecation Policy: introduction/deprecation-policy.md
- API:
- Components: api/components.md
- Core Resources:
@@ -68,72 +68,73 @@ nav:
- Controller Options: api/controller-options.md
- Metrics: api/metrics.md
- Guides:
- - Introduction: guides/introduction.md
- - External Secrets:
- - Extract structured data: guides/all-keys-one-secret.md
- - Find Secrets by Name or Metadata: guides/getallsecrets.md
- - Rewriting Keys: guides/datafrom-rewrite.md
- - Advanced Templating:
- - v2: guides/templating.md
- - v1: guides/templating-v1.md
- - Kubernetes Secret Types: guides/common-k8s-secret-types.md
- - "Lifecycle: ownership & deletion": guides/ownership-deletion-policy.md
- - Decoding Strategies: guides/decoding-strategy.md
- - Controller Classes: guides/controller-class.md
- - Generators: guides/generator.md
- - Push Secrets: guides/pushsecrets.md
- - Operations:
- - Multi Tenancy: guides/multi-tenancy.md
- - Security Best Practices: guides/security-best-practices.md
- - Threat Model: guides/threat-model.md
- - Upgrading to v1beta1: guides/v1beta1.md
- - Using Latest Image: guides/using-latest-image.md
- - Disable Cluster Features: guides/disable-cluster-features.md
+ - Introduction: guides/introduction.md
+ - External Secrets:
+ - Extract structured data: guides/all-keys-one-secret.md
+ - Find Secrets by Name or Metadata: guides/getallsecrets.md
+ - Rewriting Keys: guides/datafrom-rewrite.md
+ - Advanced Templating:
+ - v2: guides/templating.md
+ - v1: guides/templating-v1.md
+ - Kubernetes Secret Types: guides/common-k8s-secret-types.md
+ - "Lifecycle: ownership & deletion": guides/ownership-deletion-policy.md
+ - Decoding Strategies: guides/decoding-strategy.md
+ - Controller Classes: guides/controller-class.md
+ - Generators: guides/generator.md
+ - Push Secrets: guides/pushsecrets.md
+ - Operations:
+ - Multi Tenancy: guides/multi-tenancy.md
+ - Security Best Practices: guides/security-best-practices.md
+ - Threat Model: guides/threat-model.md
+ - Upgrading to v1beta1: guides/v1beta1.md
+ - Using Latest Image: guides/using-latest-image.md
+ - Disable Cluster Features: guides/disable-cluster-features.md
- Provider:
- - AWS Secrets Manager: provider/aws-secrets-manager.md
- - AWS Parameter Store: provider/aws-parameter-store.md
- - Azure Key Vault: provider/azure-key-vault.md
- - Chef: provider/chef.md
- - CyberArk Conjur: provider/conjur.md
- - Google Cloud Secret Manager: provider/google-secrets-manager.md
- - HashiCorp Vault: provider/hashicorp-vault.md
- - Kubernetes: provider/kubernetes.md
- - IBM Secrets Manager: provider/ibm-secrets-manager.md
- - Akeyless: provider/akeyless.md
- - Yandex Certificate Manager: provider/yandex-certificate-manager.md
- - Yandex Lockbox: provider/yandex-lockbox.md
- - Alibaba Cloud: provider/alibaba.md
- - GitLab Variables: provider/gitlab-variables.md
- - Oracle Vault: provider/oracle-vault.md
- - 1Password Secrets Automation: provider/1password-automation.md
- - Webhook: provider/webhook.md
- - Fake: provider/fake.md
- - senhasegura DevOps Secrets Management (DSM): provider/senhasegura-dsm.md
- - Doppler: provider/doppler.md
- - Keeper Security: provider/keeper-security.md
- - Cloak End 2 End Encrypted Secrets: provider/cloak.md
- - Scaleway: provider/scaleway.md
- - Delinea: provider/delinea.md
- - Passbolt: provider/passbolt.md
- - Pulumi ESC: provider/pulumi.md
- - Onboardbase: provider/onboardbase.md
- - Password Depot: provider-passworddepot.md
- - Fortanix: provider/fortanix.md
+ - AWS Secrets Manager: provider/aws-secrets-manager.md
+ - AWS Parameter Store: provider/aws-parameter-store.md
+ - Azure Key Vault: provider/azure-key-vault.md
+ - Chef: provider/chef.md
+ - CyberArk Conjur: provider/conjur.md
+ - Google Cloud Secret Manager: provider/google-secrets-manager.md
+ - HashiCorp Vault: provider/hashicorp-vault.md
+ - Kubernetes: provider/kubernetes.md
+ - IBM Secrets Manager: provider/ibm-secrets-manager.md
+ - Akeyless: provider/akeyless.md
+ - Yandex Certificate Manager: provider/yandex-certificate-manager.md
+ - Yandex Lockbox: provider/yandex-lockbox.md
+ - Alibaba Cloud: provider/alibaba.md
+ - GitLab Variables: provider/gitlab-variables.md
+ - Oracle Vault: provider/oracle-vault.md
+ - 1Password Secrets Automation: provider/1password-automation.md
+ - Webhook: provider/webhook.md
+ - Fake: provider/fake.md
+ - senhasegura DevOps Secrets Management (DSM): provider/senhasegura-dsm.md
+ - Doppler: provider/doppler.md
+ - Keeper Security: provider/keeper-security.md
+ - Cloak End 2 End Encrypted Secrets: provider/cloak.md
+ - Scaleway: provider/scaleway.md
+ - Delinea: provider/delinea.md
+ - Passbolt: provider/passbolt.md
+ - Pulumi ESC: provider/pulumi.md
+ - Onboardbase: provider/onboardbase.md
+ - Password Depot: provider-passworddepot.md
+ - Fortanix: provider/fortanix.md
+ - Infisical: provider/infisical.md
- Examples:
- - FluxCD: examples/gitops-using-fluxcd.md
- - Anchore Engine: examples/anchore-engine-credentials.md
- - Jenkins: examples/jenkins-kubernetes-credentials.md
- - BitWarden: examples/bitwarden.md
+ - FluxCD: examples/gitops-using-fluxcd.md
+ - Anchore Engine: examples/anchore-engine-credentials.md
+ - Jenkins: examples/jenkins-kubernetes-credentials.md
+ - BitWarden: examples/bitwarden.md
- Community:
- - Contributing:
- - Developer guide: contributing/devguide.md
- - Contributing Process: contributing/process.md
- - Release Process: contributing/release.md
- - Code of Conduct: contributing/coc.md
- - Roadmap: contributing/roadmap.md
- - External Resources:
- - Talks: eso-talks.md
- - Demos: eso-demos.md
- - Blogs: eso-blogs.md
+ - Contributing:
+ - Developer guide: contributing/devguide.md
+ - Contributing Process: contributing/process.md
+ - Release Process: contributing/release.md
+ - Code of Conduct: contributing/coc.md
+ - Roadmap: contributing/roadmap.md
+ - External Resources:
+ - Talks: eso-talks.md
+ - Demos: eso-demos.md
+ - Blogs: eso-blogs.md
- References:
- - API specification: spec.md
+ - API specification: spec.md
diff --git a/pkg/provider/infisical/api/api.go b/pkg/provider/infisical/api/api.go
new file mode 100644
index 000000000..298463c6d
--- /dev/null
+++ b/pkg/provider/infisical/api/api.go
@@ -0,0 +1,257 @@
+/*
+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 impliec.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+ "github.com/external-secrets/external-secrets/pkg/metrics"
+ "github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
+)
+
+type InfisicalClient struct {
+ BaseURL *url.URL
+ client *http.Client
+ token string
+}
+
+type InfisicalApis interface {
+ MachineIdentityLoginViaUniversalAuth(data MachineIdentityUniversalAuthLoginRequest) (*MachineIdentityDetailsResponse, error)
+ GetSecretsV3(data GetSecretsV3Request) (map[string]string, error)
+ GetSecretByKeyV3(data GetSecretByKeyV3Request) (string, error)
+ RevokeAccessToken() error
+}
+
+const UserAgentName = "k8-external-secrets-operator"
+const errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
+
+func NewAPIClient(baseURL string) (*InfisicalClient, error) {
+ baseParsedURL, err := url.Parse(baseURL)
+ if err != nil {
+ return nil, err
+ }
+
+ api := &InfisicalClient{
+ BaseURL: baseParsedURL,
+ client: &http.Client{
+ Timeout: time.Second * 15,
+ },
+ }
+
+ return api, nil
+}
+
+func (a *InfisicalClient) SetTokenViaMachineIdentity(clientID, clientSecret string) error {
+ if a.token != "" {
+ return nil
+ }
+
+ loginResponse, err := a.MachineIdentityLoginViaUniversalAuth(MachineIdentityUniversalAuthLoginRequest{
+ ClientID: clientID,
+ ClientSecret: clientSecret,
+ })
+ if err != nil {
+ return err
+ }
+
+ a.token = loginResponse.AccessToken
+ return nil
+}
+
+func (a *InfisicalClient) RevokeAccessToken() error {
+ if a.token == "" {
+ return nil
+ }
+ if _, err := a.RevokeMachineIdentityAccessToken(RevokeMachineIdentityAccessTokenRequest{AccessToken: a.token}); err != nil {
+ return err
+ }
+
+ a.token = ""
+ return nil
+}
+
+func (a *InfisicalClient) resolveEndpoint(path string) string {
+ return a.BaseURL.ResolveReference(&url.URL{Path: path}).String()
+}
+
+func (a *InfisicalClient) do(r *http.Request) (*http.Response, error) {
+ if a.token != "" {
+ r.Header.Add("Authorization", "Bearer "+a.token)
+ }
+ r.Header.Add("User-Agent", UserAgentName)
+ r.Header.Add("Content-Type", "application/json")
+
+ return a.client.Do(r)
+}
+
+func (a *InfisicalClient) MachineIdentityLoginViaUniversalAuth(data MachineIdentityUniversalAuthLoginRequest) (*MachineIdentityDetailsResponse, error) {
+ endpointURL := a.resolveEndpoint("api/v1/auth/universal-auth/login")
+ body, err := MarshalReqBody(data)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest(http.MethodPost, endpointURL, body)
+ metrics.ObserveAPICall(constants.ProviderName, "MachineIdentityLoginViaUniversalAuth", err)
+ if err != nil {
+ return nil, err
+ }
+
+ rawRes, err := a.do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ var res MachineIdentityDetailsResponse
+ err = ReadAndUnmarshal(rawRes, &res)
+ if err != nil {
+ return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
+ }
+ return &res, nil
+}
+
+func (a *InfisicalClient) RevokeMachineIdentityAccessToken(data RevokeMachineIdentityAccessTokenRequest) (*RevokeMachineIdentityAccessTokenResponse, error) {
+ endpointURL := a.resolveEndpoint("api/v1/auth/token/revoke")
+ body, err := MarshalReqBody(data)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest(http.MethodPost, endpointURL, body)
+ metrics.ObserveAPICall(constants.ProviderName, "RevokeMachineIdentityAccessToken", err)
+ if err != nil {
+ return nil, err
+ }
+
+ rawRes, err := a.do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ var res RevokeMachineIdentityAccessTokenResponse
+ err = ReadAndUnmarshal(rawRes, &res)
+ if err != nil {
+ return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
+ }
+ return &res, nil
+}
+
+func (a *InfisicalClient) GetSecretsV3(data GetSecretsV3Request) (map[string]string, error) {
+ endpointURL := a.resolveEndpoint("api/v3/secrets/raw")
+
+ req, err := http.NewRequest(http.MethodGet, endpointURL, http.NoBody)
+ metrics.ObserveAPICall(constants.ProviderName, "GetSecretsV3", err)
+ if err != nil {
+ return nil, err
+ }
+
+ q := req.URL.Query()
+ q.Add("workspaceSlug", data.ProjectSlug)
+ q.Add("environment", data.EnvironmentSlug)
+ q.Add("secretPath", data.SecretPath)
+ q.Add("include_imports", "true")
+ q.Add("expandSecretReferences", "true")
+ req.URL.RawQuery = q.Encode()
+
+ rawRes, err := a.do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ var res GetSecretsV3Response
+ err = ReadAndUnmarshal(rawRes, &res)
+ if err != nil {
+ return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
+ }
+
+ secrets := make(map[string]string)
+ for _, s := range res.ImportedSecrets {
+ for _, el := range s.Secrets {
+ secrets[el.SecretKey] = el.SecretValue
+ }
+ }
+ for _, el := range res.Secrets {
+ secrets[el.SecretKey] = el.SecretValue
+ }
+
+ return secrets, nil
+}
+
+func (a *InfisicalClient) GetSecretByKeyV3(data GetSecretByKeyV3Request) (string, error) {
+ endpointURL := a.resolveEndpoint(fmt.Sprintf("api/v3/secrets/raw/%s", data.SecretKey))
+
+ req, err := http.NewRequest(http.MethodGet, endpointURL, http.NoBody)
+ metrics.ObserveAPICall(constants.ProviderName, "GetSecretByKeyV3", err)
+ if err != nil {
+ return "", err
+ }
+
+ q := req.URL.Query()
+ q.Add("workspaceSlug", data.ProjectSlug)
+ q.Add("environment", data.EnvironmentSlug)
+ q.Add("secretPath", data.SecretPath)
+ q.Add("include_imports", "true")
+ req.URL.RawQuery = q.Encode()
+
+ rawRes, err := a.do(req)
+ if err != nil {
+ return "", err
+ }
+ if rawRes.StatusCode == 400 {
+ var errRes InfisicalAPIErrorResponse
+ err = ReadAndUnmarshal(rawRes, &errRes)
+ if err != nil {
+ return "", fmt.Errorf(errJSONSecretUnmarshal, err)
+ }
+
+ if errRes.Message == "Secret not found" {
+ return "", esv1beta1.NoSecretError{}
+ }
+ return "", errors.New(errRes.Message)
+ }
+
+ var res GetSecretByKeyV3Response
+ err = ReadAndUnmarshal(rawRes, &res)
+ if err != nil {
+ return "", fmt.Errorf(errJSONSecretUnmarshal, err)
+ }
+
+ return res.Secret.SecretValue, nil
+}
+
+func MarshalReqBody(data any) (*bytes.Reader, error) {
+ body, err := json.Marshal(data)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(body), nil
+}
+
+func ReadAndUnmarshal(resp *http.Response, target any) error {
+ var buf bytes.Buffer
+ defer resp.Body.Close()
+ _, err := buf.ReadFrom(resp.Body)
+ if err != nil {
+ return err
+ }
+ return json.Unmarshal(buf.Bytes(), target)
+}
diff --git a/pkg/provider/infisical/api/api_models.go b/pkg/provider/infisical/api/api_models.go
new file mode 100644
index 000000000..f45ca88b3
--- /dev/null
+++ b/pkg/provider/infisical/api/api_models.go
@@ -0,0 +1,87 @@
+/*
+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 impliec.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package api
+
+type MachineIdentityUniversalAuthRefreshRequest struct {
+ AccessToken string `json:"accessToken"`
+}
+
+type MachineIdentityDetailsResponse struct {
+ AccessToken string `json:"accessToken"`
+ ExpiresIn int `json:"expiresIn"`
+ AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
+ TokenType string `json:"tokenType"`
+}
+
+type MachineIdentityUniversalAuthLoginRequest struct {
+ ClientID string `json:"clientId"`
+ ClientSecret string `json:"clientSecret"`
+}
+
+type RevokeMachineIdentityAccessTokenRequest struct {
+ AccessToken string `json:"accessToken"`
+}
+
+type RevokeMachineIdentityAccessTokenResponse struct {
+ Message string `json:"message"`
+}
+
+type GetSecretByKeyV3Request struct {
+ EnvironmentSlug string `json:"environment"`
+ ProjectSlug string `json:"workspaceSlug"`
+ SecretPath string `json:"secretPath"`
+ SecretKey string `json:"secretKey"`
+}
+
+type GetSecretByKeyV3Response struct {
+ Secret SecretsV3 `json:"secret"`
+}
+
+type GetSecretsV3Request struct {
+ EnvironmentSlug string `json:"environment"`
+ ProjectSlug string `json:"workspaceSlug"`
+ SecretPath string `json:"secretPath"`
+}
+
+type GetSecretsV3Response struct {
+ Secrets []SecretsV3 `json:"secrets"`
+ ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
+ Modified bool `json:"modified,omitempty"`
+ ETag string `json:"ETag,omitempty"`
+}
+
+type SecretsV3 struct {
+ ID string `json:"id"`
+ Workspace string `json:"workspace"`
+ Environment string `json:"environment"`
+ Version int `json:"version"`
+ Type string `json:"string"`
+ SecretKey string `json:"secretKey"`
+ SecretValue string `json:"secretValue"`
+ SecretComment string `json:"secretComment"`
+}
+
+type ImportedSecretV3 struct {
+ Environment string `json:"environment"`
+ FolderID string `json:"folderId"`
+ SecretPath string `json:"secretPath"`
+ Secrets []SecretsV3 `json:"secrets"`
+}
+
+type InfisicalAPIErrorResponse struct {
+ StatusCode int `json:"statusCode"`
+ Message string `json:"message"`
+ Error any `json:"error"`
+}
diff --git a/pkg/provider/infisical/client.go b/pkg/provider/infisical/client.go
new file mode 100644
index 000000000..1df252560
--- /dev/null
+++ b/pkg/provider/infisical/client.go
@@ -0,0 +1,170 @@
+/*
+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 impliec.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package infisical
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/tidwall/gjson"
+ corev1 "k8s.io/api/core/v1"
+
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+ "github.com/external-secrets/external-secrets/pkg/find"
+ "github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
+)
+
+var (
+ errNotImplemented = errors.New("not implemented")
+ errPropertyNotFound = "property %s does not exist in secret %s"
+ errTagsNotImplemented = errors.New("find by tags not supported")
+)
+
+func getPropertyValue(jsonData, propertyName, keyName string) ([]byte, error) {
+ result := gjson.Get(jsonData, propertyName)
+ if !result.Exists() {
+ return nil, fmt.Errorf(errPropertyNotFound, propertyName, keyName)
+ }
+ return []byte(result.Str), nil
+}
+
+// if GetSecret returns an error with type NoSecretError.
+// then the secret entry will be deleted depending on the deletionPolicy.
+func (p *Provider) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+ secret, err := p.apiClient.GetSecretByKeyV3(api.GetSecretByKeyV3Request{
+ EnvironmentSlug: p.apiScope.EnvironmentSlug,
+ ProjectSlug: p.apiScope.ProjectSlug,
+ SecretPath: p.apiScope.SecretPath,
+ SecretKey: ref.Key,
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ if ref.Property != "" {
+ propertyValue, err := getPropertyValue(secret, ref.Property, ref.Key)
+ if err != nil {
+ return nil, err
+ }
+
+ return propertyValue, nil
+ }
+
+ return []byte(secret), nil
+}
+
+// GetSecretMap returns multiple k/v pairs from the provider.
+func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+ secret, err := p.GetSecret(ctx, ref)
+ if err != nil {
+ return nil, err
+ }
+
+ kv := make(map[string]json.RawMessage)
+ err = json.Unmarshal(secret, &kv)
+ if err != nil {
+ return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
+ }
+ secretData := make(map[string][]byte)
+ for k, v := range kv {
+ var strVal string
+ err = json.Unmarshal(v, &strVal)
+ if err == nil {
+ secretData[k] = []byte(strVal)
+ } else {
+ secretData[k] = v
+ }
+ }
+ return secretData, nil
+}
+
+// GetAllSecrets returns multiple k/v pairs from the provider.
+func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+ if ref.Tags != nil {
+ return nil, errTagsNotImplemented
+ }
+
+ secrets, err := p.apiClient.GetSecretsV3(api.GetSecretsV3Request{
+ EnvironmentSlug: p.apiScope.EnvironmentSlug,
+ ProjectSlug: p.apiScope.ProjectSlug,
+ SecretPath: p.apiScope.SecretPath,
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ secretMap := make(map[string][]byte)
+ for key, value := range secrets {
+ secretMap[key] = []byte(value)
+ }
+ if ref.Name == nil && ref.Path == nil {
+ return secretMap, nil
+ }
+
+ var matcher *find.Matcher
+ if ref.Name != nil {
+ m, err := find.New(*ref.Name)
+ if err != nil {
+ return nil, err
+ }
+ matcher = m
+ }
+
+ selected := map[string][]byte{}
+ for key, value := range secrets {
+ if (matcher != nil && !matcher.MatchName(key)) || (ref.Path != nil && !strings.HasPrefix(key, *ref.Path)) {
+ continue
+ }
+ selected[key] = []byte(value)
+ }
+ return selected, nil
+}
+
+// Validate checks if the client is configured correctly.
+// and is able to retrieve secrets from the provider.
+// If the validation result is unknown it will be ignored.
+func (p *Provider) Validate() (esv1beta1.ValidationResult, error) {
+ // try to fetch the secrets to ensure provided credentials has access to read secrets
+ _, err := p.apiClient.GetSecretsV3(api.GetSecretsV3Request{
+ EnvironmentSlug: p.apiScope.EnvironmentSlug,
+ ProjectSlug: p.apiScope.ProjectSlug,
+ SecretPath: p.apiScope.SecretPath,
+ })
+
+ if err != nil {
+ return esv1beta1.ValidationResultError, fmt.Errorf("cannot read secrets with provided project scope project:%s environment:%s secret-path:%s, %w", p.apiScope.ProjectSlug, p.apiScope.EnvironmentSlug, p.apiScope.SecretPath, err)
+ }
+
+ return esv1beta1.ValidationResultReady, nil
+}
+
+// PushSecret will write a single secret into the provider.
+func (p *Provider) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
+ return errNotImplemented
+}
+
+// DeleteSecret will delete the secret from a provider.
+func (p *Provider) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
+ return errNotImplemented
+}
+
+// SecretExists checks if a secret is already present in the provider at the given location.
+func (p *Provider) SecretExists(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) (bool, error) {
+ return false, errNotImplemented
+}
diff --git a/pkg/provider/infisical/constants/constants.go b/pkg/provider/infisical/constants/constants.go
new file mode 100644
index 000000000..987fbe370
--- /dev/null
+++ b/pkg/provider/infisical/constants/constants.go
@@ -0,0 +1,19 @@
+/*
+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 impliec.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package constants
+
+const (
+ UniversalAuth = "universal-auth"
+ ProviderName = "infisical"
+)
diff --git a/pkg/provider/infisical/fake/fake.go b/pkg/provider/infisical/fake/fake.go
new file mode 100644
index 000000000..888f8c74b
--- /dev/null
+++ b/pkg/provider/infisical/fake/fake.go
@@ -0,0 +1,58 @@
+/*
+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 impliec.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package fake
+
+import (
+ "errors"
+ "time"
+
+ "github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
+)
+
+var (
+ ErrMissingMockImplementation = errors.New("missing mock implmentation")
+)
+
+type MockInfisicalClient struct {
+ MockedGetSecretV3 func(data api.GetSecretsV3Request) (map[string]string, error)
+ MockedGetSecretByKeyV3 func(data api.GetSecretByKeyV3Request) (string, error)
+}
+
+func (a *MockInfisicalClient) MachineIdentityLoginViaUniversalAuth(data api.MachineIdentityUniversalAuthLoginRequest) (*api.MachineIdentityDetailsResponse, error) {
+ return &api.MachineIdentityDetailsResponse{
+ AccessToken: "test-access-token",
+ ExpiresIn: int(time.Hour * 24),
+ TokenType: "bearer",
+ AccessTokenMaxTTL: int(time.Hour * 24 * 2),
+ }, nil
+}
+
+func (a *MockInfisicalClient) GetSecretsV3(data api.GetSecretsV3Request) (map[string]string, error) {
+ if a.MockedGetSecretV3 == nil {
+ return nil, ErrMissingMockImplementation
+ }
+
+ return a.MockedGetSecretV3(data)
+}
+
+func (a *MockInfisicalClient) GetSecretByKeyV3(data api.GetSecretByKeyV3Request) (string, error) {
+ if a.MockedGetSecretByKeyV3 == nil {
+ return "", ErrMissingMockImplementation
+ }
+ return a.MockedGetSecretByKeyV3(data)
+}
+
+func (a *MockInfisicalClient) RevokeAccessToken() error {
+ return nil
+}
diff --git a/pkg/provider/infisical/provider.go b/pkg/provider/infisical/provider.go
new file mode 100644
index 000000000..3fd7f90ec
--- /dev/null
+++ b/pkg/provider/infisical/provider.go
@@ -0,0 +1,159 @@
+/*
+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 implieclient.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package infisical
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ ctrl "sigs.k8s.io/controller-runtime"
+ kclient "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+ esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+ "github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
+ "github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
+ "github.com/external-secrets/external-secrets/pkg/utils"
+ "github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+var (
+ Logger = ctrl.Log.WithName("provider").WithName(constants.ProviderName)
+)
+
+type Provider struct {
+ apiClient api.InfisicalApis
+ apiScope *InfisicalClientScope
+}
+
+type InfisicalClientScope struct {
+ SecretPath string
+ ProjectSlug string
+ EnvironmentSlug string
+}
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.SecretsClient = &Provider{}
+var _ esv1beta1.Provider = &Provider{}
+
+func init() {
+ esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+ Infisical: &esv1beta1.InfisicalProvider{},
+ })
+}
+
+func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+ return esv1beta1.SecretStoreReadOnly
+}
+
+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.Infisical == nil {
+ return nil, errors.New("invalid infisical store")
+ }
+
+ infisicalSpec := storeSpec.Provider.Infisical
+
+ apiClient, err := api.NewAPIClient(infisicalSpec.HostAPI)
+ if err != nil {
+ return nil, err
+ }
+
+ if infisicalSpec.Auth.UniversalAuthCredentials != nil {
+ universalAuthCredentials := infisicalSpec.Auth.UniversalAuthCredentials
+ clientID, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientID)
+ if err != nil {
+ return nil, err
+ }
+
+ clientSecret, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientSecret)
+ if err != nil {
+ return nil, err
+ }
+
+ if err := apiClient.SetTokenViaMachineIdentity(clientID, clientSecret); err != nil {
+ return nil, fmt.Errorf("failed to authenticate via universal auth %w", err)
+ }
+
+ return &Provider{
+ apiClient: apiClient,
+ apiScope: &InfisicalClientScope{
+ SecretPath: infisicalSpec.SecretsScope.SecretsPath,
+ ProjectSlug: infisicalSpec.SecretsScope.ProjectSlug,
+ EnvironmentSlug: infisicalSpec.SecretsScope.EnvironmentSlug,
+ },
+ }, nil
+ }
+
+ return &Provider{}, errors.New("authentication method not found")
+}
+
+func (p *Provider) Close(ctx context.Context) error {
+ if err := p.apiClient.RevokeAccessToken(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func GetStoreSecretData(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string, secret esmeta.SecretKeySelector) (string, error) {
+ secretRef := esmeta.SecretKeySelector{
+ Name: secret.Name,
+ Key: secret.Key,
+ }
+ if secret.Namespace != nil {
+ secretRef.Namespace = secret.Namespace
+ }
+
+ secretData, err := resolvers.SecretKeyRef(ctx, kube, store.GetObjectKind().GroupVersionKind().Kind, namespace, &secretRef)
+ if err != nil {
+ return "", err
+ }
+ return secretData, nil
+}
+
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
+ storeSpec := store.GetSpec()
+ infisicalStoreSpec := storeSpec.Provider.Infisical
+ if infisicalStoreSpec == nil {
+ return nil, errors.New("invalid infisical store")
+ }
+
+ if infisicalStoreSpec.SecretsScope.EnvironmentSlug == "" || infisicalStoreSpec.SecretsScope.ProjectSlug == "" {
+ return nil, errors.New("secretsScope.projectSlug and secretsScope.environmentSlug cannot be empty")
+ }
+
+ if infisicalStoreSpec.Auth.UniversalAuthCredentials != nil {
+ uaCredential := infisicalStoreSpec.Auth.UniversalAuthCredentials
+ // to validate reference authentication
+ err := utils.ValidateReferentSecretSelector(store, uaCredential.ClientID)
+ if err != nil {
+ return nil, err
+ }
+
+ err = utils.ValidateReferentSecretSelector(store, uaCredential.ClientSecret)
+ if err != nil {
+ return nil, err
+ }
+
+ if uaCredential.ClientID.Key == "" || uaCredential.ClientSecret.Key == "" {
+ return nil, errors.New("universalAuthCredentials.clientId and universalAuthCredentials.clientSecret cannot be empty")
+ }
+ }
+
+ return nil, nil
+}
diff --git a/pkg/provider/infisical/provider_test.go b/pkg/provider/infisical/provider_test.go
new file mode 100644
index 000000000..24fb34502
--- /dev/null
+++ b/pkg/provider/infisical/provider_test.go
@@ -0,0 +1,238 @@
+/*
+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 impliec.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package infisical
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+ esv1meta "github.com/external-secrets/external-secrets/apis/meta/v1"
+ "github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
+ "github.com/external-secrets/external-secrets/pkg/provider/infisical/fake"
+)
+
+type storeModifier func(*esv1beta1.SecretStore) *esv1beta1.SecretStore
+
+var apiScope = InfisicalClientScope{
+ SecretPath: "/",
+ ProjectSlug: "first-project",
+ EnvironmentSlug: "dev",
+}
+
+type TestCases struct {
+ Name string
+ MockClient *fake.MockInfisicalClient
+ PropertyAccess string
+ Error error
+ Output any
+}
+
+func TestGetSecret(t *testing.T) {
+ testCases := []TestCases{
+ {
+ Name: "Get_valid_key",
+ MockClient: &fake.MockInfisicalClient{
+ MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
+ return "value", nil
+ },
+ },
+ Error: nil,
+ Output: []byte("value"),
+ },
+ {
+ Name: "Get_property_key",
+ MockClient: &fake.MockInfisicalClient{
+ MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
+ return `{"key":"value"}`, nil
+ },
+ },
+ Error: nil,
+ Output: []byte("value"),
+ },
+ {
+ Name: "Key_not_found",
+ MockClient: &fake.MockInfisicalClient{
+ MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
+ // from server
+ return "", errors.New("Secret not found")
+ },
+ },
+ Error: errors.New("Secret not found"),
+ Output: "",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+ p := &Provider{
+ apiClient: tc.MockClient,
+ apiScope: &apiScope,
+ }
+ var property string
+ if tc.Name == "Get_property_key" {
+ property = "key"
+ }
+
+ output, err := p.GetSecret(context.Background(), esv1beta1.ExternalSecretDataRemoteRef{
+ Key: "key",
+ Property: property,
+ })
+
+ if tc.Error == nil {
+ assert.NoError(t, err)
+ assert.Equal(t, tc.Output, output)
+ } else {
+ assert.ErrorAs(t, err, &tc.Error)
+ }
+ })
+ }
+}
+
+func TestGetSecretMap(t *testing.T) {
+ testCases := []TestCases{
+ {
+ Name: "Get_valid_key_map",
+ MockClient: &fake.MockInfisicalClient{
+ MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
+ return `{"key":"value"}`, nil
+ },
+ },
+ Error: nil,
+ Output: map[string][]byte{
+ "key": []byte("value"),
+ },
+ },
+ {
+ Name: "Get_invalid_map",
+ MockClient: &fake.MockInfisicalClient{
+ MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
+ return ``, nil
+ },
+ },
+ Error: errors.New("unexpected end of JSON input"),
+ Output: nil,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.Name, func(t *testing.T) {
+ p := &Provider{
+ apiClient: tc.MockClient,
+ apiScope: &apiScope,
+ }
+ output, err := p.GetSecretMap(context.Background(), esv1beta1.ExternalSecretDataRemoteRef{
+ Key: "key",
+ })
+ if tc.Error == nil {
+ assert.NoError(t, err)
+ assert.Equal(t, tc.Output, output)
+ } else {
+ assert.ErrorAs(t, err, &tc.Error)
+ }
+ })
+ }
+}
+
+func makeSecretStore(projectSlug, environment, secretPath string, fn ...storeModifier) *esv1beta1.SecretStore {
+ store := &esv1beta1.SecretStore{
+ Spec: esv1beta1.SecretStoreSpec{
+ Provider: &esv1beta1.SecretStoreProvider{
+ Infisical: &esv1beta1.InfisicalProvider{
+ Auth: esv1beta1.InfisicalAuth{
+ UniversalAuthCredentials: &esv1beta1.UniversalAuthCredentials{},
+ },
+ SecretsScope: esv1beta1.MachineIdentityScopeInWorkspace{
+ SecretsPath: secretPath,
+ EnvironmentSlug: environment,
+ ProjectSlug: projectSlug,
+ },
+ },
+ },
+ },
+ }
+ for _, f := range fn {
+ store = f(store)
+ }
+ return store
+}
+
+func withClientID(name, key string, namespace *string) storeModifier {
+ return func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+ store.Spec.Provider.Infisical.Auth.UniversalAuthCredentials.ClientID = esv1meta.SecretKeySelector{
+ Name: name,
+ Key: key,
+ Namespace: namespace,
+ }
+ return store
+ }
+}
+
+func withClientSecret(name, key string, namespace *string) storeModifier {
+ return func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+ store.Spec.Provider.Infisical.Auth.UniversalAuthCredentials.ClientSecret = esv1meta.SecretKeySelector{
+ Name: name,
+ Key: key,
+ Namespace: namespace,
+ }
+ return store
+ }
+}
+
+type ValidateStoreTestCase struct {
+ store *esv1beta1.SecretStore
+ assertError func(t *testing.T, err error)
+}
+
+func TestValidateStore(t *testing.T) {
+ const randomID = "some-random-id"
+ const authType = "universal-auth"
+ var authCredMissingErr = errors.New("universalAuthCredentials.clientId and universalAuthCredentials.clientSecret cannot be empty")
+ var authScopeMissingErr = errors.New("secretsScope.projectSlug and secretsScope.environmentSlug cannot be empty")
+
+ testCases := []ValidateStoreTestCase{
+ {
+ store: makeSecretStore("", "", ""),
+ assertError: func(t *testing.T, err error) {
+ require.ErrorAs(t, err, &authScopeMissingErr)
+ },
+ },
+ {
+ store: makeSecretStore(apiScope.ProjectSlug, apiScope.EnvironmentSlug, apiScope.SecretPath, withClientID(authType, randomID, nil)),
+ assertError: func(t *testing.T, err error) {
+ require.ErrorAs(t, err, &authCredMissingErr)
+ },
+ },
+ {
+ store: makeSecretStore(apiScope.ProjectSlug, apiScope.EnvironmentSlug, apiScope.SecretPath, withClientSecret(authType, randomID, nil)),
+ assertError: func(t *testing.T, err error) {
+ require.ErrorAs(t, err, &authCredMissingErr)
+ },
+ },
+ {
+ store: makeSecretStore(apiScope.ProjectSlug, apiScope.EnvironmentSlug, apiScope.SecretPath, withClientID(authType, randomID, nil), withClientSecret(authType, randomID, nil)),
+ assertError: func(t *testing.T, err error) { require.NoError(t, err) },
+ },
+ }
+ p := Provider{}
+ for _, tc := range testCases {
+ _, err := p.ValidateStore(tc.store)
+ tc.assertError(t, err)
+ }
+}
diff --git a/pkg/provider/register/register.go b/pkg/provider/register/register.go
index 80e0e4019..1b339c996 100644
--- a/pkg/provider/register/register.go
+++ b/pkg/provider/register/register.go
@@ -30,6 +30,7 @@ import (
_ "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"
+ _ "github.com/external-secrets/external-secrets/pkg/provider/infisical"
_ "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/onboardbase"