Browse Source

Add Doppler provider (#1573)

* Add Doppler provider

Signed-off-by: Ryan Blunden <ryan.blunden@doppler.com>
Ryan Blunden 3 years ago
parent
commit
f01e13f21b
36 changed files with 1986 additions and 39 deletions
  1. 57 0
      apis/externalsecrets/v1beta1/secretstore_doppler_types.go
  2. 5 1
      apis/externalsecrets/v1beta1/secretstore_types.go
  3. 57 0
      apis/externalsecrets/v1beta1/zz_generated.deepcopy.go
  4. 70 0
      config/crds/bases/external-secrets.io_clustersecretstores.yaml
  5. 70 0
      config/crds/bases/external-secrets.io_secretstores.yaml
  6. 108 0
      deploy/crds/bundle.yaml
  7. BIN
      docs/pictures/doppler-create-service-token.jpg
  8. BIN
      docs/pictures/doppler-download.png
  9. BIN
      docs/pictures/doppler-fetch-all.png
  10. BIN
      docs/pictures/doppler-fetch.png
  11. BIN
      docs/pictures/doppler-filter.png
  12. BIN
      docs/pictures/doppler-get-db-url-secret.jpg
  13. BIN
      docs/pictures/doppler-json.png
  14. BIN
      docs/pictures/doppler-name-transformer.png
  15. BIN
      docs/pictures/doppler-provider-header.jpg
  16. BIN
      docs/pictures/doppler-service-tokens.png
  17. 135 0
      docs/provider/doppler.md
  18. 16 0
      docs/snippets/doppler-fetch-all-secrets.yaml
  19. 16 0
      docs/snippets/doppler-fetch-secret.yaml
  20. 15 0
      docs/snippets/doppler-filtered-secrets.yaml
  21. 12 0
      docs/snippets/doppler-generic-secret-store.yaml
  22. 17 0
      docs/snippets/doppler-name-transformer-external-secret.yaml
  23. 12 0
      docs/snippets/doppler-name-transformer-secret-store.yaml
  24. 15 0
      docs/snippets/doppler-parse-json-secret.yaml
  25. 15 0
      docs/snippets/doppler-secrets-download-external-secret.yaml
  26. 13 0
      docs/snippets/doppler-secrets-download-secret-store.yaml
  27. 1 1
      docs/snippets/fake-provider-es.yaml
  28. 312 21
      docs/spec.md
  29. 18 16
      docs/stability-support.md
  30. 2 0
      hack/api-docs/mkdocs.yml
  31. 221 0
      pkg/provider/doppler/client.go
  32. 348 0
      pkg/provider/doppler/client/client.go
  33. 281 0
      pkg/provider/doppler/doppler_test.go
  34. 55 0
      pkg/provider/doppler/fake/fake.go
  35. 114 0
      pkg/provider/doppler/provider.go
  36. 1 0
      pkg/provider/register/register.go

+ 57 - 0
apis/externalsecrets/v1beta1/secretstore_doppler_types.go

@@ -0,0 +1,57 @@
+/*
+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"
+)
+
+// Set DOPPLER_BASE_URL and DOPPLER_VERIFY_TLS environment variables to override defaults
+
+type DopplerAuth struct {
+	SecretRef DopplerAuthSecretRef `json:"secretRef"`
+}
+
+type DopplerAuthSecretRef struct {
+	// The DopplerToken is used for authentication.
+	// See https://docs.doppler.com/reference/api#authentication for auth token types.
+	// The Key attribute defaults to dopplerToken if not specified.
+	DopplerToken esmeta.SecretKeySelector `json:"dopplerToken"`
+}
+
+// DopplerProvider configures a store to sync secrets using the Doppler provider.
+// Project and Config are required if not using a Service Token.
+type DopplerProvider struct {
+	// Auth configures how the Operator authenticates with the Doppler API
+	Auth *DopplerAuth `json:"auth"`
+
+	// Doppler project (required if not using a Service Token)
+	// +optional
+	Project string `json:"project,omitempty"`
+
+	// Doppler config (required if not using a Service Token)
+	// +optional
+	Config string `json:"config,omitempty"`
+
+	// Environment variable compatible name transforms that change secret names to a different format
+	// +kubebuilder:validation:Enum=upper-camel;camel;lower-snake;tf-var;dotnet-env
+	// +optional
+	NameTransformer string `json:"nameTransformer,omitempty"`
+
+	// Format enables the downloading of secrets as a file (string)
+	// +kubebuilder:validation:Enum=json;dotnet-json;env;yaml;docker
+	// +optional
+	Format string `json:"format,omitempty"`
+}

+ 5 - 1
apis/externalsecrets/v1beta1/secretstore_types.go

@@ -38,7 +38,7 @@ type SecretStoreSpec struct {
 	RefreshInterval int `json:"refreshInterval"`
 }
 
-// SecretStoreProvider contains the provider-specific configration.
+// SecretStoreProvider contains the provider-specific configuration.
 // +kubebuilder:validation:MinProperties=1
 // +kubebuilder:validation:MaxProperties=1
 type SecretStoreProvider struct {
@@ -105,6 +105,10 @@ type SecretStoreProvider struct {
 	// Senhasegura configures this store to sync secrets using senhasegura provider
 	// +optional
 	Senhasegura *SenhaseguraProvider `json:"senhasegura,omitempty"`
+
+	// Doppler configures this store to sync secrets using the Doppler provider
+	// +optional
+	Doppler *DopplerProvider `json:"doppler,omitempty"`
 }
 
 type CAProviderType string

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

@@ -553,6 +553,58 @@ func (in *ClusterSecretStoreList) DeepCopyObject() runtime.Object {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DopplerAuth) DeepCopyInto(out *DopplerAuth) {
+	*out = *in
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DopplerAuth.
+func (in *DopplerAuth) DeepCopy() *DopplerAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(DopplerAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DopplerAuthSecretRef) DeepCopyInto(out *DopplerAuthSecretRef) {
+	*out = *in
+	in.DopplerToken.DeepCopyInto(&out.DopplerToken)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DopplerAuthSecretRef.
+func (in *DopplerAuthSecretRef) DeepCopy() *DopplerAuthSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(DopplerAuthSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DopplerProvider) DeepCopyInto(out *DopplerProvider) {
+	*out = *in
+	if in.Auth != nil {
+		in, out := &in.Auth, &out.Auth
+		*out = new(DopplerAuth)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DopplerProvider.
+func (in *DopplerProvider) DeepCopy() *DopplerProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(DopplerProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExternalSecret) DeepCopyInto(out *ExternalSecret) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
@@ -1517,6 +1569,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(SenhaseguraProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Doppler != nil {
+		in, out := &in.Doppler, &out.Doppler
+		*out = new(DopplerProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.

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

@@ -1986,6 +1986,76 @@ spec:
                     required:
                     - vaultUrl
                     type: object
+                  doppler:
+                    description: Doppler configures this store to sync secrets using
+                      the Doppler provider
+                    properties:
+                      auth:
+                        description: Auth configures how the Operator authenticates
+                          with the Doppler API
+                        properties:
+                          secretRef:
+                            properties:
+                              dopplerToken:
+                                description: The DopplerToken is used for authentication.
+                                  See https://docs.doppler.com/reference/api#authentication
+                                  for auth token types. The Key attribute defaults
+                                  to dopplerToken if not specified.
+                                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:
+                            - dopplerToken
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                      config:
+                        description: Doppler config (required if not using a Service
+                          Token)
+                        type: string
+                      format:
+                        description: Format enables the downloading of secrets as
+                          a file (string)
+                        enum:
+                        - json
+                        - dotnet-json
+                        - env
+                        - yaml
+                        - docker
+                        type: string
+                      nameTransformer:
+                        description: Environment variable compatible name transforms
+                          that change secret names to a different format
+                        enum:
+                        - upper-camel
+                        - camel
+                        - lower-snake
+                        - tf-var
+                        - dotnet-env
+                        type: string
+                      project:
+                        description: Doppler project (required if not using a Service
+                          Token)
+                        type: string
+                    required:
+                    - auth
+                    type: object
                   fake:
                     description: Fake configures a store with static key/value pairs
                     properties:

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

@@ -1986,6 +1986,76 @@ spec:
                     required:
                     - vaultUrl
                     type: object
+                  doppler:
+                    description: Doppler configures this store to sync secrets using
+                      the Doppler provider
+                    properties:
+                      auth:
+                        description: Auth configures how the Operator authenticates
+                          with the Doppler API
+                        properties:
+                          secretRef:
+                            properties:
+                              dopplerToken:
+                                description: The DopplerToken is used for authentication.
+                                  See https://docs.doppler.com/reference/api#authentication
+                                  for auth token types. The Key attribute defaults
+                                  to dopplerToken if not specified.
+                                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:
+                            - dopplerToken
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                      config:
+                        description: Doppler config (required if not using a Service
+                          Token)
+                        type: string
+                      format:
+                        description: Format enables the downloading of secrets as
+                          a file (string)
+                        enum:
+                        - json
+                        - dotnet-json
+                        - env
+                        - yaml
+                        - docker
+                        type: string
+                      nameTransformer:
+                        description: Environment variable compatible name transforms
+                          that change secret names to a different format
+                        enum:
+                        - upper-camel
+                        - camel
+                        - lower-snake
+                        - tf-var
+                        - dotnet-env
+                        type: string
+                      project:
+                        description: Doppler project (required if not using a Service
+                          Token)
+                        type: string
+                    required:
+                    - auth
+                    type: object
                   fake:
                     description: Fake configures a store with static key/value pairs
                     properties:

+ 108 - 0
deploy/crds/bundle.yaml

@@ -1809,6 +1809,60 @@ spec:
                       required:
                         - vaultUrl
                       type: object
+                    doppler:
+                      description: Doppler configures this store to sync secrets using the Doppler provider
+                      properties:
+                        auth:
+                          description: Auth configures how the Operator authenticates with the Doppler API
+                          properties:
+                            secretRef:
+                              properties:
+                                dopplerToken:
+                                  description: The DopplerToken is used for authentication. See https://docs.doppler.com/reference/api#authentication for auth token types. The Key attribute defaults to dopplerToken if not specified.
+                                  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:
+                                - dopplerToken
+                              type: object
+                          required:
+                            - secretRef
+                          type: object
+                        config:
+                          description: Doppler config (required if not using a Service Token)
+                          type: string
+                        format:
+                          description: Format enables the downloading of secrets as a file (string)
+                          enum:
+                            - json
+                            - dotnet-json
+                            - env
+                            - yaml
+                            - docker
+                          type: string
+                        nameTransformer:
+                          description: Environment variable compatible name transforms that change secret names to a different format
+                          enum:
+                            - upper-camel
+                            - camel
+                            - lower-snake
+                            - tf-var
+                            - dotnet-env
+                          type: string
+                        project:
+                          description: Doppler project (required if not using a Service Token)
+                          type: string
+                      required:
+                        - auth
+                      type: object
                     fake:
                       description: Fake configures a store with static key/value pairs
                       properties:
@@ -4680,6 +4734,60 @@ spec:
                       required:
                         - vaultUrl
                       type: object
+                    doppler:
+                      description: Doppler configures this store to sync secrets using the Doppler provider
+                      properties:
+                        auth:
+                          description: Auth configures how the Operator authenticates with the Doppler API
+                          properties:
+                            secretRef:
+                              properties:
+                                dopplerToken:
+                                  description: The DopplerToken is used for authentication. See https://docs.doppler.com/reference/api#authentication for auth token types. The Key attribute defaults to dopplerToken if not specified.
+                                  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:
+                                - dopplerToken
+                              type: object
+                          required:
+                            - secretRef
+                          type: object
+                        config:
+                          description: Doppler config (required if not using a Service Token)
+                          type: string
+                        format:
+                          description: Format enables the downloading of secrets as a file (string)
+                          enum:
+                            - json
+                            - dotnet-json
+                            - env
+                            - yaml
+                            - docker
+                          type: string
+                        nameTransformer:
+                          description: Environment variable compatible name transforms that change secret names to a different format
+                          enum:
+                            - upper-camel
+                            - camel
+                            - lower-snake
+                            - tf-var
+                            - dotnet-env
+                          type: string
+                        project:
+                          description: Doppler project (required if not using a Service Token)
+                          type: string
+                      required:
+                        - auth
+                      type: object
                     fake:
                       description: Fake configures a store with static key/value pairs
                       properties:

BIN
docs/pictures/doppler-create-service-token.jpg


BIN
docs/pictures/doppler-download.png


BIN
docs/pictures/doppler-fetch-all.png


BIN
docs/pictures/doppler-fetch.png


BIN
docs/pictures/doppler-filter.png


BIN
docs/pictures/doppler-get-db-url-secret.jpg


BIN
docs/pictures/doppler-json.png


BIN
docs/pictures/doppler-name-transformer.png


BIN
docs/pictures/doppler-provider-header.jpg


BIN
docs/pictures/doppler-service-tokens.png


+ 135 - 0
docs/provider/doppler.md

@@ -0,0 +1,135 @@
+![Doppler External Secrets Provider](../pictures/doppler-provider-header.jpg)
+
+## Doppler SecretOps Platform
+
+Sync secrets from the [Doppler SecretOps Platform](https://www.doppler.com/) to Kubernetes using the External Secrets Operator.
+
+## Authentication
+
+Doppler [Service Tokens](https://docs.doppler.com/docs/service-tokens) are recommended as they restrict access to a single config.
+
+![Doppler Service Token](../pictures/doppler-service-tokens.png)
+
+> NOTE: Doppler Personal Tokens are also supported but require `project` and `config` to be set on the `SecretStore` or `ClusterSecretStore`.
+
+Create the Doppler Token secret by opening the Doppler dashboard and navigating to the desired Project and Config, then create a new Service Token from the **Access** tab:
+
+![Create Doppler Service Token](../pictures/doppler-create-service-token.jpg)
+
+Create the Doppler Token Kubernetes secret with your Service Token value:
+
+```sh
+HISTIGNORE='*kubectl*' kubectl create secret generic \
+    doppler-token-auth-api \
+    --from-literal dopplerToken="dp.st.xxxx"
+```
+
+Then to create a generic `SecretStore`:
+
+```yaml
+{% include 'doppler-generic-secret-store.yaml' %}
+```
+
+> **NOTE:** In case of a `ClusterSecretStore`, be sure to set `namespace` in `secretRef.dopplerToken`.
+
+
+## Use Cases
+
+The Doppler provider allows for a wide range of use cases:
+
+1. [Fetch](#1-fetch)
+2. [Fetch all](#2-fetch-all)
+3. [Filter](#3-filter)
+4. [JSON secret](#4-json-secret)
+5. [Name transformer](#5-name-transformer)
+6. [Download](#6-download)
+
+Let's explore each use case using a fictional `auth-api` Doppler project.
+
+## 1. Fetch
+
+To sync one or more individual secrets:
+
+``` yaml
+{% include 'doppler-fetch-secret.yaml' %}
+```
+
+![Doppler fetch](../pictures/doppler-fetch.png)
+
+## 2. Fetch all
+
+To sync every secret from a config:
+
+``` yaml
+{% include 'doppler-fetch-all-secrets.yaml' %}
+```
+
+![Doppler fetch all](../pictures/doppler-fetch-all.png)
+
+## 3. Filter
+
+To filter secrets by `path` (path prefix), `name` (regular expression) or a combination of both:
+
+``` yaml
+{% include 'doppler-filtered-secrets.yaml' %}
+```
+
+![Doppler filter](../pictures/doppler-filter.png)
+
+## 4. JSON secret
+
+To parse a JSON secret to its key-value pairs:
+
+``` yaml
+{% include 'doppler-parse-json-secret.yaml' %}
+```
+
+![Doppler JSON Secret](../pictures/doppler-json.png)
+
+## 5. Name transformer
+
+Name transformers format keys from Doppler's UPPER_SNAKE_CASE to one of the following alternatives:
+
+- upper-camel
+- camel
+- lower-snake
+- tf-var
+- dotnet-env
+
+Name transformers require a specifically configured `SecretStore`:
+
+```yaml
+{% include 'doppler-name-transformer-secret-store.yaml' %}
+```
+
+Then an `ExternalSecret` referencing the `SecretStore`:
+
+```yaml
+{% include 'doppler-name-transformer-external-secret.yaml' %}
+```
+
+![Doppler name transformer](../pictures/doppler-name-transformer.png)
+
+### 6. Download
+
+A single `DOPPLER_SECRETS_FILE` key is set where the value is the secrets downloaded in one of the following formats:
+
+- json
+- dotnet-json
+- env
+- env-no-quotes
+- yaml
+
+Downloading secrets requires a specifically configured `SecretStore`:
+
+```yaml
+{% include 'doppler-secrets-download-secret-store.yaml' %}
+```
+
+Then an `ExternalSecret` referencing the `SecretStore`:
+
+```yaml
+{% include 'doppler-secrets-download-external-secret.yaml' %}
+```
+
+![Doppler download](../pictures/doppler-download.png)

+ 16 - 0
docs/snippets/doppler-fetch-all-secrets.yaml

@@ -0,0 +1,16 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: auth-api-all
+spec:
+  secretStoreRef:
+    kind: SecretStore
+    name: doppler-auth-api
+
+  target:
+    name: auth-api-all
+
+  dataFrom:
+    - find:
+        name:
+          regexp: .*

+ 16 - 0
docs/snippets/doppler-fetch-secret.yaml

@@ -0,0 +1,16 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: auth-api-db-url
+spec:
+  secretStoreRef:
+    kind: SecretStore
+    name: doppler-auth-api
+
+  target:
+    name: auth-api-db-url
+
+  data:
+    - secretKey: DB_URL
+      remoteRef:
+        key: DB_URL

+ 15 - 0
docs/snippets/doppler-filtered-secrets.yaml

@@ -0,0 +1,15 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: auth-api-db
+spec:
+  secretStoreRef:
+    kind: SecretStore
+    name: doppler-auth-api
+
+  target:
+    name: auth-api-db
+
+  dataFrom:
+    - find:
+        path: DB_

+ 12 - 0
docs/snippets/doppler-generic-secret-store.yaml

@@ -0,0 +1,12 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: doppler-auth-api
+spec:
+  provider:
+    doppler:
+      auth:
+        secretRef:
+          dopplerToken:
+            name: doppler-token-auth-api
+            key: dopplerToken

+ 17 - 0
docs/snippets/doppler-name-transformer-external-secret.yaml

@@ -0,0 +1,17 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: doppler-auth-api-dotnet-env
+spec:
+  secretStoreRef:
+    kind: SecretStore
+    name: doppler-auth-api-dotnet-env
+
+  target:
+    name: doppler-auth-api-dotnet-env
+    creationPolicy: Owner
+
+  dataFrom:
+    - find:
+        name:
+          regexp: .*

+ 12 - 0
docs/snippets/doppler-name-transformer-secret-store.yaml

@@ -0,0 +1,12 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: doppler-auth-api-dotnet-env
+spec:
+  provider:
+    doppler:
+      auth:
+        secretRef:
+          dopplerToken:
+            name: doppler-token-auth-api
+      nameTransformer: dotnet-env

+ 15 - 0
docs/snippets/doppler-parse-json-secret.yaml

@@ -0,0 +1,15 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: auth-api-sa-json
+spec:
+  secretStoreRef:
+    kind: SecretStore
+    name: doppler-auth-api
+
+  target:
+    name: auth-api-sa-json
+
+  dataFrom:
+    - extract:
+        key: SA_JSON

+ 15 - 0
docs/snippets/doppler-secrets-download-external-secret.yaml

@@ -0,0 +1,15 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: auth-api-json-file
+spec:
+  secretStoreRef:
+    kind: SecretStore
+    name: doppler-auth-api-json-file
+
+  target:
+    name: auth-api-json-file
+
+  dataFrom:
+    - find:
+        path: DOPPLER_SECRETS_FILE

+ 13 - 0
docs/snippets/doppler-secrets-download-secret-store.yaml

@@ -0,0 +1,13 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: doppler-auth-api-json-file
+spec:
+  provider:
+    doppler:
+      auth:
+        secretRef:
+          dopplerToken:
+            name: doppler-token-auth-api
+            key: dopplerToken
+      format: json

+ 1 - 1
docs/snippets/fake-provider-es.yaml

@@ -16,4 +16,4 @@ spec:
       version: v1
   dataFrom:
   - extract:
-      key: /foo/baz
+      key: /foo/baz

+ 312 - 21
docs/spec.md

@@ -251,6 +251,24 @@ AkeylessAuthSecretRef
 </em>
 </td>
 <td>
+<em>(Optional)</em>
+<p>Reference to a Secret that contains the details
+to authenticate with Akeyless.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>kubernetesAuth</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.AkeylessKubernetesAuth">
+AkeylessKubernetesAuth
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Kubernetes authenticates with Akeyless by passing the ServiceAccount
+token stored in the named Secret resource.</p>
 </td>
 </tr>
 </tbody>
@@ -306,6 +324,77 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1beta1.AkeylessKubernetesAuth">AkeylessKubernetesAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.AkeylessAuth">AkeylessAuth</a>)
+</p>
+<p>
+<p>Authenticate with Kubernetes ServiceAccount token stored.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>accessID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>the Akeyless Kubernetes auth-method access-id</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>k8sConfName</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Kubernetes-auth configuration name in Akeyless-Gateway</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>serviceAccountRef</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.ServiceAccountSelector
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Optional service account field containing the name of a kubernetes ServiceAccount.
+If the service account is specified, the service account secret token JWT will be used
+for authenticating with Akeyless. If the service account selector is not supplied,
+the secretRef will be used instead.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>secretRef</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Optional secret field containing a Kubernetes ServiceAccount JWT used
+for authenticating with Akeyless. If a name is specified without a key,
+<code>token</code> is the default. If one is not specified, the one bound to
+the controller will be used.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.AkeylessProvider">AkeylessProvider
 </h3>
 <p>
@@ -504,6 +593,35 @@ is ServicePrincipal.</p>
 </td>
 </tr></tbody>
 </table>
+<h3 id="external-secrets.io/v1beta1.AzureEnvironmentType">AzureEnvironmentType
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.AzureKVProvider">AzureKVProvider</a>)
+</p>
+<p>
+<p>AzureEnvironmentType specifies the Azure cloud environment endpoints to use for
+connecting and authenticating with Azure. By default it points to the public cloud AAD endpoint.
+The following endpoints are available, also see here: <a href="https://github.com/Azure/go-autorest/blob/main/autorest/azure/environments.go#L152">https://github.com/Azure/go-autorest/blob/main/autorest/azure/environments.go#L152</a>
+PublicCloud, USGovernmentCloud, ChinaCloud, GermanCloud</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;ChinaCloud&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;GermanCloud&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;PublicCloud&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;USGovernmentCloud&#34;</p></td>
+<td></td>
+</tr></tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.AzureKVAuth">AzureKVAuth
 </h3>
 <p>
@@ -606,6 +724,22 @@ string
 </tr>
 <tr>
 <td>
+<code>environmentType</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.AzureEnvironmentType">
+AzureEnvironmentType
+</a>
+</em>
+</td>
+<td>
+<p>EnvironmentType specifies the Azure cloud environment endpoints to use for
+connecting and authenticating with Azure. By default it points to the public cloud AAD endpoint.
+The following endpoints are available, also see here: <a href="https://github.com/Azure/go-autorest/blob/main/autorest/azure/environments.go#L152">https://github.com/Azure/go-autorest/blob/main/autorest/azure/environments.go#L152</a>
+PublicCloud, USGovernmentCloud, ChinaCloud, GermanCloud</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>authSecretRef</code></br>
 <em>
 <a href="#external-secrets.io/v1beta1.AzureKVAuth">
@@ -649,7 +783,7 @@ string
 </h3>
 <p>
 (<em>Appears on:</em>
-<a href="#external-secrets.io/v1beta1.KubernetesServer">KubernetesServer</a>,
+<a href="#external-secrets.io/v1beta1.KubernetesServer">KubernetesServer</a>, 
 <a href="#external-secrets.io/v1beta1.VaultProvider">VaultProvider</a>)
 </p>
 <p>
@@ -1237,6 +1371,148 @@ SecretStoreStatus
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1beta1.DopplerAuth">DopplerAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.DopplerProvider">DopplerProvider</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="#external-secrets.io/v1beta1.DopplerAuthSecretRef">
+DopplerAuthSecretRef
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.DopplerAuthSecretRef">DopplerAuthSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.DopplerAuth">DopplerAuth</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>dopplerToken</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<p>The DopplerToken is used for authentication.
+See <a href="https://docs.doppler.com/reference/api#authentication">https://docs.doppler.com/reference/api#authentication</a> for auth token types.
+The Key attribute defaults to dopplerToken if not specified.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.DopplerProvider">DopplerProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>DopplerProvider configures a store to sync secrets using the Doppler provider.
+Project and Config are required if not using a Service Token.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.DopplerAuth">
+DopplerAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth configures how the Operator authenticates with the Doppler API</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>project</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Doppler project (required if not using a Service Token)</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>config</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Doppler config (required if not using a Service Token)</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>nameTransformer</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Environment variable compatible name transforms that change secret names to a different format</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>format</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Format enables the downloading of secrets as a file (string)</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.ExternalSecret">ExternalSecret
 </h3>
 <p>
@@ -1388,7 +1664,7 @@ ExternalSecretStatus
 (<code>string</code> alias)</p></h3>
 <p>
 (<em>Appears on:</em>
-<a href="#external-secrets.io/v1beta1.ExternalSecretDataRemoteRef">ExternalSecretDataRemoteRef</a>,
+<a href="#external-secrets.io/v1beta1.ExternalSecretDataRemoteRef">ExternalSecretDataRemoteRef</a>, 
 <a href="#external-secrets.io/v1beta1.ExternalSecretFind">ExternalSecretFind</a>)
 </p>
 <p>
@@ -1533,7 +1809,8 @@ ExternalSecretFind
 </td>
 <td>
 <em>(Optional)</em>
-<p>Used to rewrite secret Keys after getting them from the secret Provider</p>
+<p>Used to rewrite secret Keys after getting them from the secret Provider
+Multiple Rewrite operations can be provided. They are applied in a layered order (first to last)</p>
 </td>
 </tr>
 </tbody>
@@ -1542,7 +1819,7 @@ ExternalSecretFind
 </h3>
 <p>
 (<em>Appears on:</em>
-<a href="#external-secrets.io/v1beta1.ExternalSecretData">ExternalSecretData</a>,
+<a href="#external-secrets.io/v1beta1.ExternalSecretData">ExternalSecretData</a>, 
 <a href="#external-secrets.io/v1beta1.ExternalSecretDataFromRemoteRef">ExternalSecretDataFromRemoteRef</a>)
 </p>
 <p>
@@ -1630,7 +1907,7 @@ ExternalSecretDecodingStrategy
 </td>
 <td>
 <em>(Optional)</em>
-<p>Used to define a conversion Strategy</p>
+<p>Used to define a decoding Strategy</p>
 </td>
 </tr>
 </tbody>
@@ -1639,7 +1916,7 @@ ExternalSecretDecodingStrategy
 (<code>string</code> alias)</p></h3>
 <p>
 (<em>Appears on:</em>
-<a href="#external-secrets.io/v1beta1.ExternalSecretDataRemoteRef">ExternalSecretDataRemoteRef</a>,
+<a href="#external-secrets.io/v1beta1.ExternalSecretDataRemoteRef">ExternalSecretDataRemoteRef</a>, 
 <a href="#external-secrets.io/v1beta1.ExternalSecretFind">ExternalSecretFind</a>)
 </p>
 <p>
@@ -1775,7 +2052,7 @@ ExternalSecretDecodingStrategy
 </td>
 <td>
 <em>(Optional)</em>
-<p>Used to define a conversion Strategy</p>
+<p>Used to define a decoding Strategy</p>
 </td>
 </tr>
 </tbody>
@@ -1828,7 +2105,8 @@ ExternalSecretRewriteRegexp
 </td>
 <td>
 <em>(Optional)</em>
-<p>Rewrite using regular expressions</p>
+<p>Used to rewrite with regular expressions.
+The resulting key will be the output of a regexp.ReplaceAll operation.</p>
 </td>
 </tr>
 </tbody>
@@ -1857,7 +2135,7 @@ string
 </em>
 </td>
 <td>
-<p>Regular expression to use as a re.Compiler.</p>
+<p>Used to define the regular expression of a re.Compiler.</p>
 </td>
 </tr>
 <tr>
@@ -1868,7 +2146,7 @@ string
 </em>
 </td>
 <td>
-<p>Target output for a replace operation.</p>
+<p>Used to define the target pattern of a ReplaceAll operation.</p>
 </td>
 </tr>
 </tbody>
@@ -1877,7 +2155,7 @@ string
 </h3>
 <p>
 (<em>Appears on:</em>
-<a href="#external-secrets.io/v1beta1.ClusterExternalSecretSpec">ClusterExternalSecretSpec</a>,
+<a href="#external-secrets.io/v1beta1.ClusterExternalSecretSpec">ClusterExternalSecretSpec</a>, 
 <a href="#external-secrets.io/v1beta1.ExternalSecret">ExternalSecret</a>)
 </p>
 <p>
@@ -3478,7 +3756,7 @@ SecretStoreStatus
 <a href="#external-secrets.io/v1beta1.SecretStoreSpec">SecretStoreSpec</a>)
 </p>
 <p>
-<p>SecretStoreProvider contains the provider-specific configration.</p>
+<p>SecretStoreProvider contains the provider-specific configuration.</p>
 </p>
 <table>
 <thead>
@@ -3712,6 +3990,20 @@ SenhaseguraProvider
 <p>Senhasegura configures this store to sync secrets using senhasegura provider</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>doppler</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.DopplerProvider">
+DopplerProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Doppler configures this store to sync secrets using the Doppler provider</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef
@@ -3799,7 +4091,7 @@ string
 </h3>
 <p>
 (<em>Appears on:</em>
-<a href="#external-secrets.io/v1beta1.ClusterSecretStore">ClusterSecretStore</a>,
+<a href="#external-secrets.io/v1beta1.ClusterSecretStore">ClusterSecretStore</a>, 
 <a href="#external-secrets.io/v1beta1.SecretStore">SecretStore</a>)
 </p>
 <p>
@@ -3871,7 +4163,7 @@ int
 </h3>
 <p>
 (<em>Appears on:</em>
-<a href="#external-secrets.io/v1beta1.ClusterSecretStore">ClusterSecretStore</a>,
+<a href="#external-secrets.io/v1beta1.ClusterSecretStore">ClusterSecretStore</a>, 
 <a href="#external-secrets.io/v1beta1.SecretStore">SecretStore</a>)
 </p>
 <p>
@@ -3989,8 +4281,7 @@ Kubernetes meta/v1.Time
 <a href="#external-secrets.io/v1beta1.SenhaseguraProvider">SenhaseguraProvider</a>)
 </p>
 <p>
-<pre><code>SenhaseguraAuth tells the controller how to do auth in senhasegura
-</code></pre>
+<p>SenhaseguraAuth tells the controller how to do auth in senhasegura.</p>
 </p>
 <table>
 <thead>
@@ -4029,8 +4320,7 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
 <a href="#external-secrets.io/v1beta1.SenhaseguraProvider">SenhaseguraProvider</a>)
 </p>
 <p>
-<pre><code>SenhaseguraModuleType enum defines senhasegura target module to fetch secrets
-</code></pre>
+<p>SenhaseguraModuleType enum defines senhasegura target module to fetch secrets</p>
 </p>
 <table>
 <thead>
@@ -4053,8 +4343,7 @@ see: https://senhasegura.com/devops
 <a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
 </p>
 <p>
-<pre><code>SenhaseguraProvider setup a store to sync secrets with senhasegura
-</code></pre>
+<p>SenhaseguraProvider setup a store to sync secrets with senhasegura.</p>
 </p>
 <table>
 <thead>
@@ -4717,7 +5006,8 @@ github.com/external-secrets/external-secrets/apis/meta/v1.ServiceAccountSelector
 <em>(Optional)</em>
 <p>Optional audiences field that will be used to request a temporary Kubernetes service
 account token for the service account referenced by <code>serviceAccountRef</code>.
-Defaults to a single audience <code>vault</code> it not specified.</p>
+Defaults to a single audience <code>vault</code> it not specified.
+Deprecated: use serviceAccountRef.Audiences instead</p>
 </td>
 </tr>
 <tr>
@@ -4732,6 +5022,7 @@ int64
 <p>Optional expiration time in seconds that will be used to request a temporary
 Kubernetes service account token for the service account referenced by
 <code>serviceAccountRef</code>.
+Deprecated: this will be removed in the future.
 Defaults to 10 minutes.</p>
 </td>
 </tr>

+ 18 - 16
docs/stability-support.md

@@ -15,29 +15,30 @@ We are currently in beta and support **only the latest release** for the time be
 The following table describes the stability level of each provider and who's responsible.
 
 | Provider                                                                                                   | Stability |                                                                                                                                     Maintainer |
-| ---------------------------------------------------------------------------------------------------------- | :-------: | ---------------------------------------------------------------------------------------------------------------------------------------------: |
-| [AWS Secrets Manager](https://external-secrets.io/latest/provider-aws-secrets-manager/)                    |  stable   |                                                                                        [external-secrets](https://github.com/external-secrets) |
-| [AWS Parameter Store](https://external-secrets.io/latest/provider-aws-parameter-store/)                    |  stable   |                                                                                        [external-secrets](https://github.com/external-secrets) |
-| [Hashicorp Vault](https://external-secrets.io/latest/provider-hashicorp-vault/)                            |  stable   |                                                                                        [external-secrets](https://github.com/external-secrets) |
-| [GCP Secret Manager](https://external-secrets.io/latest/provider-google-secrets-manager/)                  |  stable   |                                                                                        [external-secrets](https://github.com/external-secrets) |
-| [Azure Keyvault](https://external-secrets.io/latest/provider-azure-key-vault/)                             |  stable   |                                                                                        [external-secrets](https://github.com/external-secrets) |
-| [Kubernetes](https://external-secrets.io/latest/provider-kubernetes)                                       |   alpha   |                                                                                        [external-secrets](https://github.com/external-secrets) |
-| [IBM Secrets Manager](https://external-secrets.io/latest/provider-ibm-secrets-manager/)                    |   alpha   | [@knelasevero](https://github.com/knelasevero) [@sebagomez](https://github.com/sebagomez) [@ricardoptcosta](https://github.com/ricardoptcosta) |
-| [Yandex Lockbox](https://external-secrets.io/latest/provider-yandex-lockbox/)                              |   alpha   |                                            [@AndreyZamyslov](https://github.com/AndreyZamyslov) [@knelasevero](https://github.com/knelasevero) |
-| [Gitlab Project Variables](https://external-secrets.io/latest/provider-gitlab-project-variables/)          |   alpha   |                                                                                                         [@Jabray5](https://github.com/Jabray5) |
+|------------------------------------------------------------------------------------------------------------| :-------: |-----------------------------------------------------------------------------------------------------------------------------------------------:|
+| [AWS Secrets Manager](https://external-secrets.io/latest/provider/aws-secrets-manager/)                    |  stable   |                                                                                        [external-secrets](https://github.com/external-secrets) |
+| [AWS Parameter Store](https://external-secrets.io/latest/provider/aws-parameter-store/)                    |  stable   |                                                                                        [external-secrets](https://github.com/external-secrets) |
+| [Hashicorp Vault](https://external-secrets.io/latest/provider/hashicorp-vault/)                            |  stable   |                                                                                        [external-secrets](https://github.com/external-secrets) |
+| [GCP Secret Manager](https://external-secrets.io/latest/provider/google-secrets-manager/)                  |  stable   |                                                                                        [external-secrets](https://github.com/external-secrets) |
+| [Azure Keyvault](https://external-secrets.io/latest/provider/azure-key-vault/)                             |  stable   |                                                                                        [external-secrets](https://github.com/external-secrets) |
+| [Kubernetes](https://external-secrets.io/latest/provider/kubernetes)                                       |   alpha   |                                                                                        [external-secrets](https://github.com/external-secrets) |
+| [IBM Secrets Manager](https://external-secrets.io/latest/provider/ibm-secrets-manager/)                    |   alpha   | [@knelasevero](https://github.com/knelasevero) [@sebagomez](https://github.com/sebagomez) [@ricardoptcosta](https://github.com/ricardoptcosta) |
+| [Yandex Lockbox](https://external-secrets.io/latest/provider/yandex-lockbox/)                              |   alpha   |                                            [@AndreyZamyslov](https://github.com/AndreyZamyslov) [@knelasevero](https://github.com/knelasevero) |
+| [Gitlab Project Variables](https://external-secrets.io/latest/provider/gitlab-project-variables/)          |   alpha   |                                                                                                         [@Jabray5](https://github.com/Jabray5) |
 | Alibaba Cloud KMS                                                                                          |   alpha   |                                                                                                 [@ElsaChelala](https://github.com/ElsaChelala) |
-| [Oracle Vault](https://external-secrets.io/latest/provider-oracle-vault)                                   |   alpha   |                                                        [@KianTigger](https://github.com/KianTigger) [@EladGabay](https://github.com/EladGabay) |
-| [Akeyless](https://external-secrets.io/latest/provider-akeyless)                                           |   alpha   |                                                                                           [@renanaAkeyless](https://github.com/renanaAkeyless) |
-| [1Password](https://external-secrets.io/latest/provider-1password-automation)                              |   alpha   |                                              [@SimSpaceCorp](https://github.com/Simspace) [@snarlysodboxer](https://github.com/snarlysodboxer) |
-| [Generic Webhook](https://external-secrets.io/latest/provider-webhook)                                     |   alpha   |                                                                                                         [@willemm](https://github.com/willemm) |
-| [senhasegura DevOps Secrets Management (DSM)](https://external-secrets.io/latest/provider-senhasegura-dsm) |   alpha   |                                                                                                           [@lfraga](https://github.com/lfraga) |
+| [Oracle Vault](https://external-secrets.io/latest/provider/oracle-vault)                                   |   alpha   |                                                        [@KianTigger](https://github.com/KianTigger) [@EladGabay](https://github.com/EladGabay) |
+| [Akeyless](https://external-secrets.io/latest/provider/akeyless)                                           |   alpha   |                                                                                           [@renanaAkeyless](https://github.com/renanaAkeyless) |
+| [1Password](https://external-secrets.io/latest/provider/1password-automation)                              |   alpha   |                                              [@SimSpaceCorp](https://github.com/Simspace) [@snarlysodboxer](https://github.com/snarlysodboxer) |
+| [Generic Webhook](https://external-secrets.io/latest/provider/webhook)                                     |   alpha   |                                                                                                         [@willemm](https://github.com/willemm) |
+| [senhasegura DevOps Secrets Management (DSM)](https://external-secrets.io/latest/provider/senhasegura-dsm) |   alpha   |                                                                                                           [@lfraga](https://github.com/lfraga) |
+| [Doppler SecretOps Platform](https://external-secrets.io/latest/provider/doppler)                          |   alpha   |                                                [@ryan-blunden](https://github.com/ryan-blunden/) [@nmanoogian](https://github.com/nmanoogian/) |
 
 ## Provider Feature Support
 
 The following table show the support for features across different providers.
 
 | Provider                 | find by name | find by tags | metadataPolicy Fetch | referent authentication | store validation | push secret |
-| ------------------------ | :----------: | :----------: | :------------------: | :---------------------: | :--------------: | :---------: |
+|--------------------------|:------------:| :----------: | :------------------: | :---------------------: | :--------------: | :---------: |
 | AWS Secrets Manager      |      x       |      x       |                      |                         |        x         |             |
 | AWS Parameter Store      |      x       |      x       |                      |                         |        x         |             |
 | Hashicorp Vault          |      x       |      x       |                      |                         |        x         |             |
@@ -53,6 +54,7 @@ The following table show the support for features across different providers.
 | 1Password                |      x       |              |                      |                         |        x         |             |
 | Generic Webhook          |              |              |                      |                         |                  |             |
 | senhasegura DSM          |              |              |                      |                         |        x         |             |
+| Doppler                  |      x       |              |                      |                         |        x         |             |
 
 
 ## Support Policy

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

@@ -61,6 +61,7 @@ nav:
       - Parameter Store: provider/aws-parameter-store.md
     - Azure:
       - Key Vault: provider/azure-key-vault.md
+
     - Google:
       - Secret Manager: provider/google-secrets-manager.md
     - IBM:
@@ -81,6 +82,7 @@ nav:
     - Kubernetes: provider/kubernetes.md
     - senhasegura:
       - DevOps Secrets Management (DSM): provider/senhasegura-dsm.md
+    - Doppler: provider/doppler.md
   - Examples:
     - FluxCD: examples/gitops-using-fluxcd.md
     - Anchore Engine: examples/anchore-engine-credentials.md

+ 221 - 0
pkg/provider/doppler/client.go

@@ -0,0 +1,221 @@
+/*
+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 doppler
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strings"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/find"
+	dClient "github.com/external-secrets/external-secrets/pkg/provider/doppler/client"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+	customBaseURLEnvVar                                = "DOPPLER_BASE_URL"
+	verifyTLSOverrideEnvVar                            = "DOPPLER_VERIFY_TLS"
+	errGetSecret                                       = "could not get secret %s: %s"
+	errGetSecrets                                      = "could not get secrets %s"
+	errUnmarshalSecretMap                              = "unable to unmarshal secret %s: %w"
+	secretsDownloadFileKey                             = "DOPPLER_SECRETS_FILE"
+	errDopplerTokenSecretName                          = "missing auth.secretRef.dopplerToken.name"
+	errInvalidClusterStoreMissingDopplerTokenNamespace = "missing auth.secretRef.dopplerToken.namespace"
+	errFetchDopplerTokenSecret                         = "unable to find find DopplerToken secret: %w"
+	errMissingDopplerToken                             = "auth.secretRef.dopplerToken.key '%s' not found in secret '%s'"
+)
+
+type Client struct {
+	doppler         SecretsClientInterface
+	dopplerToken    string
+	project         string
+	config          string
+	nameTransformer string
+	format          string
+
+	kube      kclient.Client
+	store     *esv1beta1.DopplerProvider
+	namespace string
+	storeKind string
+}
+
+// SecretsClientInterface defines the required Doppler Client methods.
+type SecretsClientInterface interface {
+	BaseURL() *url.URL
+	Authenticate() error
+	GetSecret(request dClient.SecretRequest) (*dClient.SecretResponse, error)
+	GetSecrets(request dClient.SecretsRequest) (*dClient.SecretsResponse, error)
+}
+
+func (c *Client) setAuth(ctx context.Context) error {
+	credentialsSecret := &corev1.Secret{}
+	credentialsSecretName := c.store.Auth.SecretRef.DopplerToken.Name
+	if credentialsSecretName == "" {
+		return fmt.Errorf(errDopplerTokenSecretName)
+	}
+	objectKey := types.NamespacedName{
+		Name:      credentialsSecretName,
+		Namespace: c.namespace,
+	}
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if c.storeKind == esv1beta1.ClusterSecretStoreKind {
+		if c.store.Auth.SecretRef.DopplerToken.Namespace == nil {
+			return fmt.Errorf(errInvalidClusterStoreMissingDopplerTokenNamespace)
+		}
+		objectKey.Namespace = *c.store.Auth.SecretRef.DopplerToken.Namespace
+	}
+
+	err := c.kube.Get(ctx, objectKey, credentialsSecret)
+	if err != nil {
+		return fmt.Errorf(errFetchDopplerTokenSecret, err)
+	}
+
+	dopplerToken := credentialsSecret.Data[c.store.Auth.SecretRef.DopplerToken.Key]
+	if (dopplerToken == nil) || (len(dopplerToken) == 0) {
+		return fmt.Errorf(errMissingDopplerToken, c.store.Auth.SecretRef.DopplerToken.Key, credentialsSecretName)
+	}
+
+	c.dopplerToken = string(dopplerToken)
+
+	return nil
+}
+
+func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
+	timeout := 15 * time.Second
+	clientURL := c.doppler.BaseURL().String()
+
+	if err := utils.NetworkValidate(clientURL, timeout); err != nil {
+		return esv1beta1.ValidationResultError, err
+	}
+
+	if err := c.doppler.Authenticate(); err != nil {
+		return esv1beta1.ValidationResultError, err
+	}
+
+	return esv1beta1.ValidationResultReady, nil
+}
+
+func (c *Client) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	request := dClient.SecretRequest{
+		Name:    ref.Key,
+		Project: c.project,
+		Config:  c.config,
+	}
+
+	secret, err := c.doppler.GetSecret(request)
+	if err != nil {
+		return nil, fmt.Errorf(errGetSecret, ref.Key, err)
+	}
+
+	return []byte(secret.Value), nil
+}
+
+func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	data, err := c.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+
+	kv := make(map[string]json.RawMessage)
+	err = json.Unmarshal(data, &kv)
+	if err != nil {
+		return nil, fmt.Errorf(errUnmarshalSecretMap, 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
+}
+
+func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	secrets, err := c.getSecrets(ctx)
+	selected := map[string][]byte{}
+
+	if err != nil {
+		return nil, err
+	}
+
+	if ref.Name == nil && ref.Path == nil {
+		return secrets, nil
+	}
+
+	var matcher *find.Matcher
+	if ref.Name != nil {
+		m, err := find.New(*ref.Name)
+		if err != nil {
+			return nil, err
+		}
+		matcher = m
+	}
+
+	for key, value := range secrets {
+		if (matcher != nil && !matcher.MatchName(key)) || (ref.Path != nil && !strings.HasPrefix(key, *ref.Path)) {
+			continue
+		}
+		selected[key] = value
+	}
+
+	return selected, nil
+}
+
+func (c *Client) Close(_ context.Context) error {
+	return nil
+}
+
+func (c *Client) getSecrets(_ context.Context) (map[string][]byte, error) {
+	request := dClient.SecretsRequest{
+		Project:         c.project,
+		Config:          c.config,
+		NameTransformer: c.nameTransformer,
+		Format:          c.format,
+	}
+
+	response, err := c.doppler.GetSecrets(request)
+	if err != nil {
+		return nil, fmt.Errorf(errGetSecrets, err)
+	}
+
+	if c.format != "" {
+		return map[string][]byte{
+			secretsDownloadFileKey: response.Body,
+		}, nil
+	}
+
+	return externalSecretsFormat(response.Secrets), nil
+}
+
+func externalSecretsFormat(secrets dClient.Secrets) map[string][]byte {
+	converted := make(map[string][]byte, len(secrets))
+	for key, value := range secrets {
+		converted[key] = []byte(value)
+	}
+	return converted
+}

+ 348 - 0
pkg/provider/doppler/client/client.go

@@ -0,0 +1,348 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package client
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+)
+
+type DopplerClient struct {
+	baseURL      *url.URL
+	DopplerToken string
+	VerifyTLS    bool
+	UserAgent    string
+}
+
+type queryParams map[string]string
+
+type headers map[string]string
+
+type httpRequestBody []byte
+
+type Secrets map[string]string
+
+type RawSecrets map[string]*interface{}
+
+type APIError struct {
+	Err     error
+	Message string
+	Data    string
+}
+
+type apiResponse struct {
+	HTTPResponse *http.Response
+	Body         []byte
+}
+
+type apiErrorResponse struct {
+	Messages []string
+	Success  bool
+}
+
+type SecretRequest struct {
+	Name    string
+	Project string
+	Config  string
+}
+
+type SecretsRequest struct {
+	Project         string
+	Config          string
+	NameTransformer string
+	Format          string
+	ETag            string // Specifying an ETag implies that the caller has implemented response caching
+}
+
+type UpdateSecretsRequest struct {
+	Secrets RawSecrets `json:"secrets,omitempty"`
+	Project string     `json:"project,omitempty"`
+	Config  string     `json:"config,omitempty"`
+}
+
+type secretResponseBody struct {
+	Name  string `json:"name,omitempty"`
+	Value struct {
+		Raw      *string `json:"raw"`
+		Computed *string `json:"computed"`
+	} `json:"value,omitempty"`
+	Messages *[]string `json:"messages,omitempty"`
+	Success  bool      `json:"success"`
+}
+
+type SecretResponse struct {
+	Name  string
+	Value string
+}
+
+type SecretsResponse struct {
+	Secrets  Secrets
+	Body     []byte
+	Modified bool
+	ETag     string
+}
+
+func NewDopplerClient(dopplerToken string) (*DopplerClient, error) {
+	client := &DopplerClient{
+		DopplerToken: dopplerToken,
+		VerifyTLS:    true,
+		UserAgent:    "doppler-external-secrets",
+	}
+
+	if err := client.SetBaseURL("https://api.doppler.com"); err != nil {
+		return nil, &APIError{Err: err, Message: "setting base URL failed"}
+	}
+
+	return client, nil
+}
+
+func (c *DopplerClient) BaseURL() *url.URL {
+	u := *c.baseURL
+	return &u
+}
+
+func (c *DopplerClient) SetBaseURL(urlStr string) error {
+	baseURL, err := url.Parse(strings.TrimSuffix(urlStr, "/"))
+
+	if err != nil {
+		return err
+	}
+
+	if baseURL.Scheme == "" {
+		baseURL.Scheme = "https"
+	}
+
+	c.baseURL = baseURL
+	return nil
+}
+
+func (c *DopplerClient) Authenticate() error {
+	//  Choose projects as a lightweight endpoint for testing authentication
+	if _, err := c.performRequest("/v3/projects", "GET", headers{}, queryParams{}, httpRequestBody{}); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *DopplerClient) GetSecret(request SecretRequest) (*SecretResponse, error) {
+	params := request.buildQueryParams(request.Name)
+	response, err := c.performRequest("/v3/configs/config/secret", "GET", headers{}, params, httpRequestBody{})
+	if err != nil {
+		return nil, err
+	}
+
+	var data secretResponseBody
+	if err := json.Unmarshal(response.Body, &data); err != nil {
+		return nil, &APIError{Err: err, Message: "unable to unmarshal secret payload", Data: string(response.Body)}
+	}
+
+	if data.Value.Computed == nil {
+		return nil, &APIError{Message: fmt.Sprintf("secret '%s' not found", request.Name)}
+	}
+
+	return &SecretResponse{Name: data.Name, Value: *data.Value.Computed}, nil
+}
+
+// GetSecrets should only have an ETag supplied if Secrets are cached as SecretsResponse.Secrets will be nil if 304 (not modified) returned.
+func (c *DopplerClient) GetSecrets(request SecretsRequest) (*SecretsResponse, error) {
+	headers := headers{}
+	if request.ETag != "" {
+		headers["if-none-match"] = request.ETag
+	}
+	if request.Format != "" && request.Format != "json" {
+		headers["accept"] = "text/plain"
+	}
+
+	params := request.buildQueryParams()
+	response, apiErr := c.performRequest("/v3/configs/config/secrets/download", "GET", headers, params, httpRequestBody{})
+	if apiErr != nil {
+		return nil, apiErr
+	}
+
+	if response.HTTPResponse.StatusCode == 304 {
+		return &SecretsResponse{Modified: false, Secrets: nil, ETag: request.ETag}, nil
+	}
+
+	eTag := response.HTTPResponse.Header.Get("etag")
+
+	// Format defeats JSON parsing
+	if request.Format != "" {
+		return &SecretsResponse{Modified: true, Body: response.Body, ETag: eTag}, nil
+	}
+
+	var secrets Secrets
+	if err := json.Unmarshal(response.Body, &secrets); err != nil {
+		return nil, &APIError{Err: err, Message: "unable to unmarshal secrets payload"}
+	}
+	return &SecretsResponse{Modified: true, Secrets: secrets, Body: response.Body, ETag: eTag}, nil
+}
+
+func (c *DopplerClient) UpdateSecrets(request UpdateSecretsRequest) error {
+	body, jsonErr := json.Marshal(request)
+	if jsonErr != nil {
+		return &APIError{Err: jsonErr, Message: "unable to unmarshal update secrets payload"}
+	}
+	_, err := c.performRequest("/v3/configs/config/secrets", "POST", headers{}, queryParams{}, body)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (r *SecretRequest) buildQueryParams(name string) queryParams {
+	params := queryParams{}
+	params["name"] = name
+
+	if r.Project != "" {
+		params["project"] = r.Project
+	}
+
+	if r.Config != "" {
+		params["config"] = r.Config
+	}
+
+	return params
+}
+
+func (r *SecretsRequest) buildQueryParams() queryParams {
+	params := queryParams{}
+
+	if r.Project != "" {
+		params["project"] = r.Project
+	}
+
+	if r.Config != "" {
+		params["config"] = r.Config
+	}
+
+	if r.NameTransformer != "" {
+		params["name_transformer"] = r.NameTransformer
+	}
+
+	if r.Format != "" {
+		params["format"] = r.Format
+	}
+
+	return params
+}
+
+func (c *DopplerClient) performRequest(path, method string, headers headers, params queryParams, body httpRequestBody) (*apiResponse, error) {
+	urlStr := c.BaseURL().String() + path
+	reqURL, err := url.Parse(urlStr)
+	if err != nil {
+		return nil, &APIError{Err: err, Message: fmt.Sprintf("invalid API URL: %s", urlStr)}
+	}
+
+	var bodyReader io.Reader
+	if body != nil {
+		bodyReader = bytes.NewReader(body)
+	} else {
+		bodyReader = http.NoBody
+	}
+
+	req, err := http.NewRequest(method, reqURL.String(), bodyReader)
+	if err != nil {
+		return nil, &APIError{Err: err, Message: "unable to form HTTP request"}
+	}
+
+	if method == "POST" && req.Header.Get("content-type") == "" {
+		req.Header.Set("content-type", "application/json")
+	}
+
+	if req.Header.Get("accept") == "" {
+		req.Header.Set("accept", "application/json")
+	}
+	req.Header.Set("user-agent", c.UserAgent)
+	req.SetBasicAuth(c.DopplerToken, "")
+
+	for key, value := range headers {
+		req.Header.Set(key, value)
+	}
+
+	query := req.URL.Query()
+	for key, value := range params {
+		query.Add(key, value)
+	}
+	req.URL.RawQuery = query.Encode()
+
+	httpClient := &http.Client{Timeout: 10 * time.Second}
+
+	tlsConfig := &tls.Config{
+		MinVersion: tls.VersionTLS12,
+	}
+
+	if !c.VerifyTLS {
+		tlsConfig.InsecureSkipVerify = true
+	}
+
+	httpClient.Transport = &http.Transport{
+		DisableKeepAlives: true,
+		TLSClientConfig:   tlsConfig,
+	}
+
+	r, err := httpClient.Do(req)
+	if err != nil {
+		return nil, &APIError{Err: err, Message: "unable to load response"}
+	}
+	defer r.Body.Close()
+
+	bodyResponse, err := io.ReadAll(r.Body)
+	if err != nil {
+		return &apiResponse{HTTPResponse: r, Body: nil}, &APIError{Err: err, Message: "unable to read entire response body"}
+	}
+
+	response := &apiResponse{HTTPResponse: r, Body: bodyResponse}
+	success := isSuccess(r.StatusCode)
+
+	if !success {
+		if contentType := r.Header.Get("content-type"); strings.HasPrefix(contentType, "application/json") {
+			var errResponse apiErrorResponse
+			err := json.Unmarshal(bodyResponse, &errResponse)
+			if err != nil {
+				return response, &APIError{Err: err, Message: "unable to unmarshal error JSON payload"}
+			}
+			return response, &APIError{Err: nil, Message: strings.Join(errResponse.Messages, "\n")}
+		}
+		return nil, &APIError{Err: fmt.Errorf("%d status code; %d bytes", r.StatusCode, len(bodyResponse)), Message: "unable to load response"}
+	}
+
+	if success && err != nil {
+		return nil, &APIError{Err: err, Message: "unable to load data from successful response"}
+	}
+	return response, nil
+}
+
+func isSuccess(statusCode int) bool {
+	return (statusCode >= 200 && statusCode <= 299) || (statusCode >= 300 && statusCode <= 399)
+}
+
+func (e *APIError) Error() string {
+	message := fmt.Sprintf("Doppler API Client Error: %s", e.Message)
+	if underlyingError := e.Err; underlyingError != nil {
+		message = fmt.Sprintf("%s\n%s", message, underlyingError.Error())
+	}
+	if e.Data != "" {
+		message = fmt.Sprintf("%s\nData: %s", message, e.Data)
+	}
+	return message
+}

+ 281 - 0
pkg/provider/doppler/doppler_test.go

@@ -0,0 +1,281 @@
+/*
+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 doppler
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/doppler/client"
+	"github.com/external-secrets/external-secrets/pkg/provider/doppler/fake"
+)
+
+const (
+	validSecretName   = "API_KEY"
+	validSecretValue  = "3a3ea4f5"
+	dopplerProject    = "DOPPLER_PROJECT"
+	dopplerProjectVal = "auth-api"
+	missingSecret     = "INVALID_NAME"
+	invalidSecret     = "doppler_project"
+	missingSecretErr  = "could not get secret"
+)
+
+type dopplerTestCase struct {
+	label          string
+	fakeClient     *fake.DopplerClient
+	request        client.SecretRequest
+	response       *client.SecretResponse
+	remoteRef      *esv1beta1.ExternalSecretDataRemoteRef
+	apiErr         error
+	expectError    string
+	expectedSecret string
+	expectedData   map[string][]byte
+}
+
+func makeValidAPIRequest() client.SecretRequest {
+	return client.SecretRequest{
+		Name: validSecretName,
+	}
+}
+
+func makeValidAPIOutput() *client.SecretResponse {
+	return &client.SecretResponse{
+		Name:  validSecretName,
+		Value: validSecretValue,
+	}
+}
+
+func makeValidRemoteRef() *esv1beta1.ExternalSecretDataRemoteRef {
+	return &esv1beta1.ExternalSecretDataRemoteRef{
+		Key: validSecretName,
+	}
+}
+
+func makeValidDopplerTestCase() *dopplerTestCase {
+	return &dopplerTestCase{
+		fakeClient:     &fake.DopplerClient{},
+		request:        makeValidAPIRequest(),
+		response:       makeValidAPIOutput(),
+		remoteRef:      makeValidRemoteRef(),
+		apiErr:         nil,
+		expectError:    "",
+		expectedSecret: "",
+		expectedData:   make(map[string][]byte),
+	}
+}
+
+func makeValidDopplerTestCaseCustom(tweaks ...func(pstc *dopplerTestCase)) *dopplerTestCase {
+	pstc := makeValidDopplerTestCase()
+	for _, fn := range tweaks {
+		fn(pstc)
+	}
+	pstc.fakeClient.WithValue(pstc.request, pstc.response, pstc.apiErr)
+	return pstc
+}
+
+func TestGetSecret(t *testing.T) {
+	setSecret := func(pstc *dopplerTestCase) {
+		pstc.label = "set secret"
+		pstc.request.Name = dopplerProject
+		pstc.response.Name = dopplerProject
+		pstc.response.Value = dopplerProjectVal
+		pstc.expectedSecret = dopplerProjectVal
+		pstc.remoteRef.Key = dopplerProject
+	}
+
+	setMissingSecret := func(pstc *dopplerTestCase) {
+		pstc.label = "invalid missing secret"
+		pstc.remoteRef.Key = missingSecret
+		pstc.request.Name = missingSecret
+		pstc.response = nil
+		pstc.expectError = missingSecretErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	setInvalidSecret := func(pstc *dopplerTestCase) {
+		pstc.label = "invalid secret name format"
+		pstc.remoteRef.Key = invalidSecret
+		pstc.request.Name = invalidSecret
+		pstc.response = nil
+		pstc.expectError = missingSecretErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	setClientError := func(pstc *dopplerTestCase) {
+		pstc.label = "invalid client error"
+		pstc.response = &client.SecretResponse{}
+		pstc.expectError = missingSecretErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	testCases := []*dopplerTestCase{
+		makeValidDopplerTestCaseCustom(setSecret),
+		makeValidDopplerTestCaseCustom(setMissingSecret),
+		makeValidDopplerTestCaseCustom(setInvalidSecret),
+		makeValidDopplerTestCaseCustom(setClientError),
+	}
+
+	c := Client{}
+	for k, tc := range testCases {
+		c.doppler = tc.fakeClient
+		out, err := c.GetSecret(context.Background(), *tc.remoteRef)
+		if !ErrorContains(err, tc.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), tc.expectError)
+		}
+		if err == nil && !cmp.Equal(string(out), tc.expectedSecret) {
+			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, tc.expectedSecret, string(out))
+		}
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	simpleJSON := func(pstc *dopplerTestCase) {
+		pstc.label = "valid unmarshalling"
+		pstc.response.Value = `{"API_KEY":"3a3ea4f5"}`
+		pstc.expectedData["API_KEY"] = []byte("3a3ea4f5")
+	}
+
+	complexJSON := func(pstc *dopplerTestCase) {
+		pstc.label = "valid unmarshalling for nested json"
+		pstc.response.Value = `{"API_KEY": "3a3ea4f5", "AUTH_SA": {"appID": "a1ea-48bd-8749-b6f5ec3c5a1f"}}`
+		pstc.expectedData["API_KEY"] = []byte("3a3ea4f5")
+		pstc.expectedData["AUTH_SA"] = []byte(`{"appID": "a1ea-48bd-8749-b6f5ec3c5a1f"}`)
+	}
+
+	setInvalidJSON := func(pstc *dopplerTestCase) {
+		pstc.label = "invalid json"
+		pstc.response.Value = `{"API_KEY": "3a3ea4f`
+		pstc.expectError = "unable to unmarshal secret"
+	}
+
+	setAPIError := func(pstc *dopplerTestCase) {
+		pstc.label = "client error"
+		pstc.response = &client.SecretResponse{}
+		pstc.expectError = missingSecretErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	testCases := []*dopplerTestCase{
+		makeValidDopplerTestCaseCustom(simpleJSON),
+		makeValidDopplerTestCaseCustom(complexJSON),
+		makeValidDopplerTestCaseCustom(setInvalidJSON),
+		makeValidDopplerTestCaseCustom(setAPIError),
+	}
+
+	d := Client{}
+	for k, tc := range testCases {
+		t.Run(tc.label, func(t *testing.T) {
+			d.doppler = tc.fakeClient
+			out, err := d.GetSecretMap(context.Background(), *tc.remoteRef)
+			if !ErrorContains(err, tc.expectError) {
+				t.Errorf("[%d] unexpected error: %q, expected: %q", k, err.Error(), tc.expectError)
+			}
+			if err == nil && !cmp.Equal(out, tc.expectedData) {
+				t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, tc.expectedData, out)
+			}
+		})
+	}
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}
+
+type storeModifier func(*esv1beta1.SecretStore) *esv1beta1.SecretStore
+
+func makeSecretStore(fn ...storeModifier) *esv1beta1.SecretStore {
+	store := &esv1beta1.SecretStore{
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				Doppler: &esv1beta1.DopplerProvider{
+					Auth: &esv1beta1.DopplerAuth{},
+				},
+			},
+		},
+	}
+	for _, f := range fn {
+		store = f(store)
+	}
+	return store
+}
+
+func withAuth(name, key string, namespace *string) storeModifier {
+	return func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+		store.Spec.Provider.Doppler.Auth.SecretRef.DopplerToken = v1.SecretKeySelector{
+			Name:      name,
+			Key:       key,
+			Namespace: namespace,
+		}
+		return store
+	}
+}
+
+type ValidateStoreTestCase struct {
+	label string
+	store *esv1beta1.SecretStore
+	err   error
+}
+
+func TestValidateStore(t *testing.T) {
+	namespace := "ns"
+	secretName := "doppler-token-secret"
+	testCases := []ValidateStoreTestCase{
+		{
+			label: "invalid store missing dopplerToken.name",
+			store: makeSecretStore(withAuth("", "", nil)),
+			err:   fmt.Errorf("invalid store: dopplerToken.name cannot be empty"),
+		},
+		{
+			label: "invalid store namespace not allowed",
+			store: makeSecretStore(withAuth(secretName, "", &namespace)),
+			err:   fmt.Errorf("invalid store: namespace not allowed with namespaced SecretStore"),
+		},
+		{
+			label: "valid provide optional dopplerToken.key",
+			store: makeSecretStore(withAuth(secretName, "customSecretKey", nil)),
+			err:   nil,
+		},
+		{
+			label: "valid namespace not set",
+			store: makeSecretStore(withAuth(secretName, "", nil)),
+			err:   nil,
+		},
+	}
+	p := Provider{}
+	for _, tc := range testCases {
+		t.Run(tc.label, func(t *testing.T) {
+			err := p.ValidateStore(tc.store)
+			if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
+				t.Errorf("test failed! want %v, got %v", tc.err, err)
+			} else if tc.err == nil && err != nil {
+				t.Errorf("want nil got err %v", err)
+			} else if tc.err != nil && err == nil {
+				t.Errorf("want err %v got nil", tc.err)
+			}
+		})
+	}
+}

+ 55 - 0
pkg/provider/doppler/fake/fake.go

@@ -0,0 +1,55 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package fake
+
+import (
+	"fmt"
+	"net/url"
+
+	"github.com/google/go-cmp/cmp"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/doppler/client"
+)
+
+type DopplerClient struct {
+	getSecret func(request client.SecretRequest) (*client.SecretResponse, error)
+}
+
+func (dc *DopplerClient) BaseURL() *url.URL {
+	return &url.URL{Scheme: "https", Host: "api.doppler.com"}
+}
+
+func (dc *DopplerClient) Authenticate() error {
+	return nil
+}
+
+func (dc *DopplerClient) GetSecret(request client.SecretRequest) (*client.SecretResponse, error) {
+	return dc.getSecret(request)
+}
+
+func (dc *DopplerClient) GetSecrets(request client.SecretsRequest) (*client.SecretsResponse, error) {
+	// Not implemented
+	return &client.SecretsResponse{}, nil
+}
+
+func (dc *DopplerClient) WithValue(request client.SecretRequest, response *client.SecretResponse, err error) {
+	if dc != nil {
+		dc.getSecret = func(requestIn client.SecretRequest) (*client.SecretResponse, error) {
+			if !cmp.Equal(requestIn, request) {
+				return nil, fmt.Errorf("unexpected test argument")
+			}
+			return response, err
+		}
+	}
+}

+ 114 - 0
pkg/provider/doppler/provider.go

@@ -0,0 +1,114 @@
+/*
+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 doppler
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"strconv"
+
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	dClient "github.com/external-secrets/external-secrets/pkg/provider/doppler/client"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+	errNewClient    = "unable to create DopplerClient : %s"
+	errInvalidStore = "invalid store: %s"
+	errDopplerStore = "missing or invalid Doppler SecretStore"
+)
+
+// Provider is a Doppler secrets provider implementing NewClient and ValidateStore for the esv1beta1.Provider interface.
+type Provider struct{}
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.SecretsClient = &Client{}
+var _ esv1beta1.Provider = &Provider{}
+
+func init() {
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+		Doppler: &esv1beta1.DopplerProvider{},
+	})
+}
+
+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.Doppler == nil {
+		return nil, fmt.Errorf(errDopplerStore)
+	}
+
+	dopplerStoreSpec := storeSpec.Provider.Doppler
+
+	// Default Key to dopplerToken if not specified
+	if dopplerStoreSpec.Auth.SecretRef.DopplerToken.Key == "" {
+		storeSpec.Provider.Doppler.Auth.SecretRef.DopplerToken.Key = "dopplerToken"
+	}
+
+	client := &Client{
+		kube:      kube,
+		store:     dopplerStoreSpec,
+		namespace: namespace,
+		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+	}
+
+	if err := client.setAuth(ctx); err != nil {
+		return nil, err
+	}
+
+	doppler, err := dClient.NewDopplerClient(client.dopplerToken)
+	if err != nil {
+		return nil, fmt.Errorf(errNewClient, err)
+	}
+
+	if customBaseURL, found := os.LookupEnv(customBaseURLEnvVar); found {
+		if err := doppler.SetBaseURL(customBaseURL); err != nil {
+			return nil, fmt.Errorf(errNewClient, err)
+		}
+	}
+
+	if customVerifyTLS, found := os.LookupEnv(verifyTLSOverrideEnvVar); found {
+		customVerifyTLS, err := strconv.ParseBool(customVerifyTLS)
+		if err == nil {
+			doppler.VerifyTLS = customVerifyTLS
+		}
+	}
+
+	client.doppler = doppler
+	client.project = client.store.Project
+	client.config = client.store.Config
+	client.nameTransformer = client.store.NameTransformer
+	client.format = client.store.Format
+
+	return client, nil
+}
+
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error {
+	storeSpec := store.GetSpec()
+	dopplerStoreSpec := storeSpec.Provider.Doppler
+	dopplerTokenSecretRef := dopplerStoreSpec.Auth.SecretRef.DopplerToken
+	if err := utils.ValidateSecretSelector(store, dopplerTokenSecretRef); err != nil {
+		return fmt.Errorf(errInvalidStore, err)
+	}
+
+	if dopplerTokenSecretRef.Name == "" {
+		return fmt.Errorf(errInvalidStore, "dopplerToken.name cannot be empty")
+	}
+
+	return nil
+}

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

@@ -21,6 +21,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/alibaba"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/aws"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"
+	_ "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/gcp/secretmanager"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"