Ver código fonte

Merge branch 'main' into akv-table-tests

Sebastian Gomez 4 anos atrás
pai
commit
119d4b809b
100 arquivos alterados com 6079 adições e 424 exclusões
  1. 1 0
      .github/PAUL.yaml
  2. 11 11
      .github/workflows/ci.yml
  3. 10 10
      .github/workflows/e2e.yml
  4. 1 1
      .github/workflows/helm.yml
  5. 2 0
      .gitignore
  6. 0 1
      .golangci.yaml
  7. 10 0
      ADOPTERS.md
  8. 1 1
      Dockerfile
  9. 11 5
      README.md
  10. 3 2
      RELEASE.md
  11. 42 0
      apis/externalsecrets/v1alpha1/secretstore_akeyless_types.go
  12. 31 4
      apis/externalsecrets/v1alpha1/secretstore_azurekv_types.go
  13. 10 1
      apis/externalsecrets/v1alpha1/secretstore_gcpsm_types.go
  14. 17 0
      apis/externalsecrets/v1alpha1/secretstore_types.go
  15. 12 2
      apis/externalsecrets/v1alpha1/secretstore_vault_types.go
  16. 101 0
      apis/externalsecrets/v1alpha1/secretstore_webhook_types.go
  17. 8 0
      apis/externalsecrets/v1alpha1/secretstore_yandexlockbox_types.go
  18. 259 2
      apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go
  19. 1 0
      apis/meta/v1/zz_generated.deepcopy.go
  20. 2 2
      deploy/charts/external-secrets/Chart.yaml
  21. 4 2
      deploy/charts/external-secrets/README.md
  22. 8 1
      deploy/charts/external-secrets/templates/deployment.yaml
  23. 9 0
      deploy/charts/external-secrets/values.yaml
  24. 279 8
      deploy/crds/external-secrets.io_clustersecretstores.yaml
  25. 279 8
      deploy/crds/external-secrets.io_secretstores.yaml
  26. 3 3
      docs/guides-all-keys-one-secret.md
  27. 4 0
      docs/guides-getting-started.md
  28. 133 0
      docs/guides-gitops-using-fluxcd.md
  29. 26 0
      docs/guides-using-latest-image.md
  30. 1 1
      docs/index.md
  31. BIN
      docs/pictures/screenshot_gitops_final_directory_tree.png
  32. 68 0
      docs/provider-akeyless.md
  33. 16 2
      docs/provider-azure-key-vault.md
  34. 80 43
      docs/provider-google-secrets-manager.md
  35. 6 4
      docs/provider-hashicorp-vault.md
  36. 118 0
      docs/provider-webhook.md
  37. 12 0
      docs/snippets/akeyless-credentials-secret.yaml
  38. 18 0
      docs/snippets/akeyless-external-secret-json.yaml
  39. 19 0
      docs/snippets/akeyless-external-secret.yaml
  40. 20 0
      docs/snippets/akeyless-secret-store.yaml
  41. 13 0
      docs/snippets/azkv-secret-store-mi.yaml
  42. 18 2
      docs/snippets/full-cluster-secret-store.yaml
  43. 8 2
      docs/snippets/full-secret-store.yaml
  44. 17 0
      docs/snippets/gitops/crs/clusterSecretStore.yaml
  45. 5 0
      docs/snippets/gitops/crs/kustomization.yaml
  46. 13 0
      docs/snippets/gitops/deployment-crds.yaml
  47. 15 0
      docs/snippets/gitops/deployment-crs.yaml
  48. 28 0
      docs/snippets/gitops/deployment.yaml
  49. 20 0
      docs/snippets/gitops/kustomization.yaml
  50. 4 0
      docs/snippets/gitops/namespace.yaml
  51. 20 0
      docs/snippets/gitops/repositories.yaml
  52. 8 0
      docs/snippets/gitops/secret-token.yaml
  53. 1 1
      docs/snippets/template-from-secret.yaml
  54. 2 0
      docs/snippets/vault-jwt-store.yaml
  55. 2 0
      docs/snippets/vault-ldap-store.yaml
  56. 686 8
      docs/spec.md
  57. BIN
      e2e/.DS_Store
  58. 4 4
      e2e/Dockerfile
  59. 4 3
      e2e/Makefile
  60. 5 5
      e2e/entrypoint.sh
  61. 3 1
      e2e/framework/addon/vault.go
  62. 4 4
      e2e/k8s/vault-config/configure-vault.sh
  63. 4 3
      e2e/run.sh
  64. 49 0
      e2e/suite/akeyless/akeyless.go
  65. 227 0
      e2e/suite/akeyless/provider.go
  66. 1 1
      e2e/suite/gcp/provider.go
  67. 1 0
      e2e/suite/vault/provider.go
  68. 179 59
      go.mod
  69. 364 112
      go.sum
  70. 3 0
      hack/api-docs/mkdocs.yml
  71. 6 1
      main.go
  72. 3 1
      pkg/controllers/externalsecret/externalsecret_controller.go
  73. 4 1
      pkg/controllers/externalsecret/suite_test.go
  74. 155 0
      pkg/provider/akeyless/akeyless.go
  75. 250 0
      pkg/provider/akeyless/akeyless_api.go
  76. 168 0
      pkg/provider/akeyless/akeyless_test.go
  77. 103 0
      pkg/provider/akeyless/auth.go
  78. 49 0
      pkg/provider/akeyless/fake/fake.go
  79. 99 0
      pkg/provider/akeyless/utils.go
  80. 8 2
      pkg/provider/aws/secretsmanager/secretsmanager.go
  81. 7 0
      pkg/provider/aws/secretsmanager/secretsmanager_test.go
  82. 57 20
      pkg/provider/azure/keyvault/keyvault.go
  83. 36 10
      pkg/provider/azure/keyvault/keyvault_test.go
  84. 69 50
      pkg/provider/gcp/secretmanager/secretsmanager.go
  85. 8 0
      pkg/provider/gcp/secretmanager/secretsmanager_test.go
  86. 254 0
      pkg/provider/gcp/secretmanager/secretsmanager_workload_identity.go
  87. 392 0
      pkg/provider/gcp/secretmanager/secretsmanager_workload_identity_test.go
  88. 23 0
      pkg/provider/ibm/provider.go
  89. 52 0
      pkg/provider/ibm/provider_test.go
  90. 2 0
      pkg/provider/register/register.go
  91. 21 7
      pkg/provider/vault/vault.go
  92. 138 6
      pkg/provider/vault/vault_test.go
  93. 401 0
      pkg/provider/webhook/webhook.go
  94. 340 0
      pkg/provider/webhook/webhook_test.go
  95. 1 1
      pkg/provider/yandex/lockbox/client/client.go
  96. 1 1
      pkg/provider/yandex/lockbox/client/fake/fake.go
  97. 15 2
      pkg/provider/yandex/lockbox/client/grpc/grpc.go
  98. 30 3
      pkg/provider/yandex/lockbox/lockbox.go
  99. 38 0
      pkg/provider/yandex/lockbox/lockbox_test.go
  100. 5 0
      pkg/template/template.go

+ 1 - 0
.github/PAUL.yaml

@@ -13,6 +13,7 @@ maintainers:
 - Flydiverny
 - gabibeyer
 - ricardoptcosta
+- rodrmartinez
 # Allows for the /label and /remove-label commands
 # usage: /label enhancement
 # usage: /remove-label enhancement

+ 11 - 11
.github/workflows/ci.yml

@@ -10,7 +10,7 @@ on:
 
 env:
   # Common versions
-  GO_VERSION: '1.16'
+  GO_VERSION: '1.17'
   GOLANGCI_VERSION: 'v1.42.1'
   # list of available versions: https://storage.googleapis.com/kubebuilder-tools
   # TODO: 1.21.2 does not shut down properly with controller-runtime 0.9.2
@@ -21,7 +21,7 @@ env:
   # a step 'if env.GHCR_USERNAME' != ""', so we copy these to succinctly test whether
   # credentials have been provided before trying to run steps that need them.
   GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
-  
+
   # Sonar
   SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
 
@@ -61,14 +61,14 @@ jobs:
           echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
 
       - name: Cache the Go Build Cache
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ${{ steps.go.outputs.build-cache }}
           key: ${{ runner.os }}-build-lint-${{ hashFiles('**/go.sum') }}
           restore-keys: ${{ runner.os }}-build-lint-
 
       - name: Cache Go Dependencies
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ${{ steps.go.outputs.mod-cache }}
           key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
@@ -107,14 +107,14 @@ jobs:
           echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
 
       - name: Cache the Go Build Cache
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ${{ steps.go.outputs.build-cache }}
           key: ${{ runner.os }}-build-check-diff-${{ hashFiles('**/go.sum') }}
           restore-keys: ${{ runner.os }}-build-check-diff-
 
       - name: Cache Go Dependencies
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ${{ steps.go.outputs.mod-cache }}
           key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
@@ -151,14 +151,14 @@ jobs:
           echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
 
       - name: Cache the Go Build Cache
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ${{ steps.go.outputs.build-cache }}
           key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }}
           restore-keys: ${{ runner.os }}-build-unit-tests-
 
       - name: Cache Go Dependencies
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ${{ steps.go.outputs.mod-cache }}
           key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
@@ -171,7 +171,7 @@ jobs:
           sudo tar -C /usr/local/kubebuilder --strip-components=1 -zvxf envtest-bins.tar.gz
 
       - name: Cache envtest binaries
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: /usr/local/kubebuilder
           key: ${{ runner.os }}-kubebuilder-${{env.KUBEBUILDER_TOOLS_VERSION}}
@@ -218,14 +218,14 @@ jobs:
           echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
 
       - name: Cache the Go Build Cache
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ${{ steps.go.outputs.build-cache }}
           key: ${{ runner.os }}-build-publish-artifacts-${{ hashFiles('**/go.sum') }}
           restore-keys: ${{ runner.os }}-build-publish-artifacts-
 
       - name: Cache Go Dependencies
-        uses: actions/cache@v2.1.6
+        uses: actions/cache@v2.1.7
         with:
           path: ${{ steps.go.outputs.mod-cache }}
           key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}

+ 10 - 10
.github/workflows/e2e.yml

@@ -6,7 +6,7 @@ on:
 
 env:
   # Common versions
-  GO_VERSION: '1.16'
+  GO_VERSION: '1.17'
   GOLANGCI_VERSION: 'v1.33'
   DOCKER_BUILDX_VERSION: 'v0.4.2'
 
@@ -50,14 +50,14 @@ jobs:
         echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
 
     - name: Cache the Go Build Cache
-      uses: actions/cache@v2.1.6
+      uses: actions/cache@v2.1.7
       with:
         path: ${{ steps.go.outputs.build-cache }}
         key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }}
         restore-keys: ${{ runner.os }}-build-unit-tests-
 
     - name: Cache Go Dependencies
-      uses: actions/cache@v2.1.6
+      uses: actions/cache@v2.1.7
       with:
         path: ${{ steps.go.outputs.mod-cache }}
         key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
@@ -113,14 +113,14 @@ jobs:
         echo "::set-output name=mod-cache::$(go env GOMODCACHE)"
 
     - name: Cache the Go Build Cache
-      uses: actions/cache@v2.1.6
+      uses: actions/cache@v2.1.7
       with:
         path: ${{ steps.go.outputs.build-cache }}
         key: ${{ runner.os }}-build-unit-tests-${{ hashFiles('**/go.sum') }}
         restore-keys: ${{ runner.os }}-build-unit-tests-
 
     - name: Cache Go Dependencies
-      uses: actions/cache@v2.1.6
+      uses: actions/cache@v2.1.7
       with:
         path: ${{ steps.go.outputs.mod-cache }}
         key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
@@ -149,7 +149,7 @@ jobs:
         make test.e2e
 
     # Update check run called "integration-fork"
-    - uses: actions/github-script@v5
+    - uses: actions/github-script@v1
       id: update-check-run
       if: ${{ always() }}
       env:
@@ -165,19 +165,19 @@ jobs:
             pull_number: process.env.number
           });
           const ref = pull.head.sha;
-
+          console.log("\n\nPR sha: " + ref)
           const { data: checks } = await github.checks.listForRef({
             ...context.repo,
             ref
           });
-
+          console.log("\n\nPR CHECKS: " + checks)
           const check = checks.check_runs.filter(c => c.name === process.env.job);
-
+          console.log("\n\nPR Filtered CHECK: " + check)
+          console.log(check)
           const { data: result } = await github.checks.update({
             ...context.repo,
             check_run_id: check[0].id,
             status: 'completed',
             conclusion: process.env.conclusion
           });
-
           return result;

+ 1 - 1
.github/workflows/helm.yml

@@ -35,7 +35,7 @@ jobs:
           python-version: 3.7
 
       - name: Set up chart-testing
-        uses: helm/chart-testing-action@v2.1.0
+        uses: helm/chart-testing-action@v2.2.0
 
       - name: Run chart-testing (list-changed)
         id: list-changed

+ 2 - 0
.gitignore

@@ -1,3 +1,5 @@
+.DS_Store
+
 /bin
 /vendor
 cover.out

+ 0 - 1
.golangci.yaml

@@ -63,7 +63,6 @@ linters:
     - gosimple
     - govet
     - ineffassign
-    - interfacer
     - lll
     - misspell
     - nakedret

+ 10 - 0
ADOPTERS.md

@@ -0,0 +1,10 @@
+# External Secrets Operator Adopters
+
+<!-- Add yourself here if you are using ESO in your company or your project! -->
+
+- [Pento](https://www.pento.io/)
+- [Mixpanel](https://mixpanel.com)
+- [K8S Website Infra](https://k8s.io/)
+
+
+Countless others that can't disclose that information! :)

+ 1 - 1
Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.14.2
+FROM gcr.io/distroless/static
 ARG TARGETOS
 ARG TARGETARCH
 COPY bin/external-secrets-${TARGETOS}-${TARGETARCH} /bin/external-secrets

+ 11 - 5
README.md

@@ -13,6 +13,7 @@ Multiple people and organizations are joining efforts to create a single Externa
 
 - [AWS Secrets Manager](https://external-secrets.io/provider-aws-secrets-manager/)
 - [AWS Parameter Store](https://external-secrets.io/provider-aws-parameter-store/)
+- [Akeyless](https://www.akeyless.io/)
 - [Hashicorp Vault](https://www.vaultproject.io/)
 - [Google Cloud Secrets Manager](https://external-secrets.io/provider-google-secrets-manager/)
 - [Azure Key Vault](https://external-secrets.io/provider-azure-key-vault/)
@@ -20,7 +21,7 @@ Multiple people and organizations are joining efforts to create a single Externa
 - [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/)
 - [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/)
 - [Alibaba Cloud KMS](https://www.alibabacloud.com/product/kms) (Docs still missing, PRs welcomed!)
-- [Oracle Vault]( https://external-secrets.io/provider-oracle-vault) 
+- [Oracle Vault]( https://external-secrets.io/provider-oracle-vault)
 
 ## Stability and Support Level
 
@@ -28,10 +29,10 @@ Multiple people and organizations are joining efforts to create a single Externa
 
 | Provider                                                                 | Stability |                                        Contact |
 | ------------------------------------------------------------------------ | :-------: | ---------------------------------------------: |
-| [AWS SM](https://external-secrets.io/provider-aws-secrets-manager/)      |   alpha   | [ESO Org](https://github.com/external-secrets) |
-| [AWS PS](https://external-secrets.io/provider-aws-parameter-store/)      |   alpha   | [ESO Org](https://github.com/external-secrets) |
-| [Hashicorp Vault](https://external-secrets.io/provider-hashicorp-vault/) |   alpha   | [ESO Org](https://github.com/external-secrets) |
-| [GCP SM](https://external-secrets.io/provider-google-secrets-manager/)   |   alpha   | [ESO Org](https://github.com/external-secrets) |
+| [AWS SM](https://external-secrets.io/provider-aws-secrets-manager/)      |   beta   | [ESO Org](https://github.com/external-secrets) |
+| [AWS PS](https://external-secrets.io/provider-aws-parameter-store/)      |   beta   | [ESO Org](https://github.com/external-secrets) |
+| [Hashicorp Vault](https://external-secrets.io/provider-hashicorp-vault/) |   stable   | [ESO Org](https://github.com/external-secrets) |
+| [GCP SM](https://external-secrets.io/provider-google-secrets-manager/)   |   beta   | [ESO Org](https://github.com/external-secrets) |
 
 ### Community maintained:
 
@@ -43,6 +44,7 @@ Multiple people and organizations are joining efforts to create a single Externa
 | [Gitlab Project Variables](https://external-secrets.io/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/provider-oracle-vault)  |   alpha  | [@KianTigger](https://github.com/KianTigger)                                 |
+| [Akeyless]( https://external-secrets.io/provider-akeyless)  |   alpha  | [@renanaAkeyless](https://github.com/renanaAkeyless)                                 |
 
 
 ## Documentation
@@ -63,6 +65,10 @@ We welcome and encourage contributions to this project! Please read the [Develop
 
 Please report vulnerabilities by email to contact@external-secrets.io, also see our [security policy](SECURITY.md) for details.
 
+## Adopters
+
+Please create a PR and add your company or your project to our [ADOPTERS](ADOPTERS.md) file if you are using our project!
+
 ## Kicked off by
 
 ![](assets/CS_logo_1.png)

+ 3 - 2
RELEASE.md

@@ -8,8 +8,9 @@ The external-secrets project is released on a as-needed basis. Feel free to open
 
 1. Run `Create Release` Action to create a new release, pass in the desired version number to release.
 2. GitHub Release, Changelog will be created by the `release.yml` workflow which also promotes the container image.
-3. (optional) update Helm Chart
-4. Announce the new release in the `#external-secrets` Kubernetes Slack
+3. update Helm Chart, see below
+4. update OLM bundle, see [helm-operator docs](https://github.com/external-secrets/external-secrets-helm-operator/blob/main/docs/release.md#operatorhubio)
+5. Announce the new release in the `#external-secrets` Kubernetes Slack
 
 ## Release Helm Chart
 

+ 42 - 0
apis/externalsecrets/v1alpha1/secretstore_akeyless_types.go

@@ -0,0 +1,42 @@
+/*
+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 v1alpha1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// AkeylessProvider Configures an store to sync secrets using Akeyless KV.
+type AkeylessProvider struct {
+
+	// Akeyless GW API Url from which the secrets to be fetched from.
+	AkeylessGWApiURL *string `json:"akeylessGWApiURL"`
+
+	// Auth configures how the operator authenticates with Akeyless.
+	Auth *AkeylessAuth `json:"authSecretRef"`
+}
+
+type AkeylessAuth struct {
+	SecretRef AkeylessAuthSecretRef `json:"secretRef"`
+}
+
+// AkeylessAuthSecretRef
+//AKEYLESS_ACCESS_TYPE_PARAM: AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY OR KUB_CONFIG_NAME.
+type AkeylessAuthSecretRef struct {
+	// The SecretAccessID is used for authentication
+	AccessID        esmeta.SecretKeySelector `json:"accessID,omitempty"`
+	AccessType      esmeta.SecretKeySelector `json:"accessType,omitempty"`
+	AccessTypeParam esmeta.SecretKeySelector `json:"accessTypeParam,omitempty"`
+}

+ 31 - 4
apis/externalsecrets/v1alpha1/secretstore_azurekv_types.go

@@ -16,14 +16,41 @@ package v1alpha1
 
 import smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 
+// AuthType describes how to authenticate to the Azure Keyvault
+// Only one of the following auth types may be specified.
+// If none of the following auth type is specified, the default one
+// is ServicePrincipal.
+// +kubebuilder:validation:Enum=ServicePrincipal;ManagedIdentity
+type AuthType string
+
+const (
+	// Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret.
+	ServicePrincipal AuthType = "ServicePrincipal"
+
+	// Using Managed Identity to authenticate. Used with aad-pod-identity instelled in the clister.
+	ManagedIdentity AuthType = "ManagedIdentity"
+)
+
 // Configures an store to sync secrets using Azure KV.
 type AzureKVProvider struct {
+	// Auth type defines how to authenticate to the keyvault service.
+	// Valid values are:
+	// - "ServicePrincipal" (default): Using a service principal (tenantId, clientId, clientSecret)
+	// - "ManagedIdentity": Using Managed Identity assigned to the pod (see aad-pod-identity)
+	// +optional
+	// +kubebuilder:default=ServicePrincipal
+	AuthType *AuthType `json:"authType,omitempty"`
 	// Vault Url from which the secrets to be fetched from.
 	VaultURL *string `json:"vaultUrl"`
-	// TenantID configures the Azure Tenant to send requests to.
-	TenantID *string `json:"tenantId"`
-	// Auth configures how the operator authenticates with Azure.
-	AuthSecretRef *AzureKVAuth `json:"authSecretRef"`
+	// TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.
+	// +optional
+	TenantID *string `json:"tenantId,omitempty"`
+	// Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.
+	// +optional
+	AuthSecretRef *AzureKVAuth `json:"authSecretRef,omitempty"`
+	// If multiple Managed Identity is assigned to the pod, you can select the one to be used
+	// +optional
+	IdentityID *string `json:"identityId,omitempty"`
 }
 
 // Configuration used to authenticate with Azure.

+ 10 - 1
apis/externalsecrets/v1alpha1/secretstore_gcpsm_types.go

@@ -19,7 +19,10 @@ import (
 )
 
 type GCPSMAuth struct {
-	SecretRef GCPSMAuthSecretRef `json:"secretRef"`
+	// +optional
+	SecretRef *GCPSMAuthSecretRef `json:"secretRef,omitempty"`
+	// +optional
+	WorkloadIdentity *GCPWorkloadIdentity `json:"workloadIdentity,omitempty"`
 }
 
 type GCPSMAuthSecretRef struct {
@@ -28,6 +31,12 @@ type GCPSMAuthSecretRef struct {
 	SecretAccessKey esmeta.SecretKeySelector `json:"secretAccessKeySecretRef,omitempty"`
 }
 
+type GCPWorkloadIdentity struct {
+	ServiceAccountRef esmeta.ServiceAccountSelector `json:"serviceAccountRef"`
+	ClusterLocation   string                        `json:"clusterLocation"`
+	ClusterName       string                        `json:"clusterName"`
+}
+
 // GCPSMProvider Configures a store to sync secrets using the GCP Secret Manager provider.
 type GCPSMProvider struct {
 	// Auth defines the information necessary to authenticate against GCP

+ 17 - 0
apis/externalsecrets/v1alpha1/secretstore_types.go

@@ -28,6 +28,10 @@ type SecretStoreSpec struct {
 
 	// Used to configure the provider. Only one provider may be set
 	Provider *SecretStoreProvider `json:"provider"`
+
+	// Used to configure http retries if failed
+	// +optional
+	RetrySettings *SecretStoreRetrySettings `json:"retrySettings,omitempty"`
 }
 
 // SecretStoreProvider contains the provider-specific configration.
@@ -42,6 +46,10 @@ type SecretStoreProvider struct {
 	// +optional
 	AzureKV *AzureKVProvider `json:"azurekv,omitempty"`
 
+	// Akeyless configures this store to sync secrets using Akeyless Vault provider
+	// +optional
+	Akeyless *AkeylessProvider `json:"akeyless,omitempty"`
+
 	// Vault configures this store to sync secrets using Hashi provider
 	// +optional
 	Vault *VaultProvider `json:"vault,omitempty"`
@@ -69,6 +77,15 @@ type SecretStoreProvider struct {
 	// Alibaba configures this store to sync secrets using Alibaba Cloud provider
 	// +optional
 	Alibaba *AlibabaProvider `json:"alibaba,omitempty"`
+
+	// Webhook configures this store to sync secrets using a generic templated webhook
+	// +optional
+	Webhook *WebhookProvider `json:"webhook,omitempty"`
+}
+
+type SecretStoreRetrySettings struct {
+	MaxRetries    *int32  `json:"maxRetries,omitempty"`
+	RetryInterval *string `json:"retryInterval,omitempty"`
 }
 
 type SecretStoreConditionType string

+ 12 - 2
apis/externalsecrets/v1alpha1/secretstore_vault_types.go

@@ -46,8 +46,8 @@ type CAProvider struct {
 	Key string `json:"key,omitempty"`
 
 	// The namespace the Provider type is in.
-	// +kubebuilder:default:="Default"
-	Namespace string `json:"namespace"`
+	// +optional
+	Namespace *string `json:"namespace,omitempty"`
 }
 
 // Configures an store to sync secrets using a HashiCorp Vault
@@ -173,6 +173,11 @@ type VaultKubernetesAuth struct {
 // VaultLdapAuth authenticates with Vault using the LDAP authentication method,
 // with the username and password stored in a Kubernetes Secret resource.
 type VaultLdapAuth struct {
+	// Path where the LDAP authentication backend is mounted
+	// in Vault, e.g: "ldap"
+	// +kubebuilder:default=ldap
+	Path string `json:"path"`
+
 	// Username is a LDAP user name used to authenticate using the LDAP Vault
 	// authentication method
 	Username string `json:"username"`
@@ -186,6 +191,11 @@ type VaultLdapAuth struct {
 // VaultJwtAuth authenticates with Vault using the JWT/OIDC authentication
 // method, with the role name and token stored in a Kubernetes Secret resource.
 type VaultJwtAuth struct {
+	// Path where the JWT authentication backend is mounted
+	// in Vault, e.g: "jwt"
+	// +kubebuilder:default=jwt
+	Path string `json:"path"`
+
 	// Role is a JWT role to authenticate using the JWT/OIDC Vault
 	// authentication method
 	// +optional

+ 101 - 0
apis/externalsecrets/v1alpha1/secretstore_webhook_types.go

@@ -0,0 +1,101 @@
+/*
+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 v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// AkeylessProvider Configures an store to sync secrets using Akeyless KV.
+type WebhookProvider struct {
+	// Webhook Method
+	// +optional, default GET
+	Method string `json:"method,omitempty"`
+
+	// Webhook url to call
+	URL string `json:"url"`
+
+	// Headers
+	// +optional
+	Headers map[string]string `json:"headers,omitempty"`
+
+	// Body
+	// +optional
+	Body string `json:"body,omitempty"`
+
+	// Timeout
+	// +optional
+	Timeout *metav1.Duration `json:"timeout,omitempty"`
+
+	// Result formatting
+	Result WebhookResult `json:"result"`
+
+	// Secrets to fill in templates
+	// These secrets will be passed to the templating function as key value pairs under the given name
+	// +optional
+	Secrets []WebhookSecret `json:"secrets,omitempty"`
+
+	// PEM encoded CA bundle used to validate webhook server certificate. Only used
+	// if the Server URL is using HTTPS protocol. This parameter is ignored for
+	// plain HTTP protocol connection. If not set the system root certificates
+	// are used to validate the TLS connection.
+	// +optional
+	CABundle []byte `json:"caBundle,omitempty"`
+
+	// The provider for the CA bundle to use to validate webhook server certificate.
+	// +optional
+	CAProvider *WebhookCAProvider `json:"caProvider,omitempty"`
+}
+
+type WebhookCAProviderType string
+
+const (
+	WebhookCAProviderTypeSecret    WebhookCAProviderType = "Secret"
+	WebhookCAProviderTypeConfigMap WebhookCAProviderType = "ConfigMap"
+)
+
+// Defines a location to fetch the cert for the webhook provider from.
+type WebhookCAProvider struct {
+	// The type of provider to use such as "Secret", or "ConfigMap".
+	// +kubebuilder:validation:Enum="Secret";"ConfigMap"
+	Type WebhookCAProviderType `json:"type"`
+
+	// The name of the object located at the provider type.
+	Name string `json:"name"`
+
+	// The key the value inside of the provider type to use, only used with "Secret" type
+	// +kubebuilder:validation:Optional
+	Key string `json:"key,omitempty"`
+
+	// The namespace the Provider type is in.
+	// +optional
+	Namespace *string `json:"namespace,omitempty"`
+}
+
+type WebhookResult struct {
+	// Json path of return value
+	// +optional
+	JSONPath string `json:"jsonPath,omitempty"`
+}
+
+type WebhookSecret struct {
+	// Name of this secret in templates
+	Name string `json:"name"`
+
+	// Secret ref to fill in credentials
+	SecretRef esmeta.SecretKeySelector `json:"secretRef"`
+}

+ 8 - 0
apis/externalsecrets/v1alpha1/secretstore_yandexlockbox_types.go

@@ -24,6 +24,10 @@ type YandexLockboxAuth struct {
 	AuthorizedKey esmeta.SecretKeySelector `json:"authorizedKeySecretRef,omitempty"`
 }
 
+type YandexLockboxCAProvider struct {
+	Certificate esmeta.SecretKeySelector `json:"certSecretRef,omitempty"`
+}
+
 // YandexLockboxProvider Configures a store to sync secrets using the Yandex Lockbox provider.
 type YandexLockboxProvider struct {
 	// Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
@@ -32,4 +36,8 @@ type YandexLockboxProvider struct {
 
 	// Auth defines the information necessary to authenticate against Yandex Lockbox
 	Auth YandexLockboxAuth `json:"auth"`
+
+	// The provider for the CA bundle to use to validate Yandex.Cloud server certificate.
+	// +optional
+	CAProvider *YandexLockboxCAProvider `json:"caProvider,omitempty"`
 }

+ 259 - 2
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
@@ -102,6 +103,65 @@ func (in *AWSProvider) DeepCopy() *AWSProvider {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AkeylessAuth) DeepCopyInto(out *AkeylessAuth) {
+	*out = *in
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AkeylessAuth.
+func (in *AkeylessAuth) DeepCopy() *AkeylessAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(AkeylessAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AkeylessAuthSecretRef) DeepCopyInto(out *AkeylessAuthSecretRef) {
+	*out = *in
+	in.AccessID.DeepCopyInto(&out.AccessID)
+	in.AccessType.DeepCopyInto(&out.AccessType)
+	in.AccessTypeParam.DeepCopyInto(&out.AccessTypeParam)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AkeylessAuthSecretRef.
+func (in *AkeylessAuthSecretRef) DeepCopy() *AkeylessAuthSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(AkeylessAuthSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AkeylessProvider) DeepCopyInto(out *AkeylessProvider) {
+	*out = *in
+	if in.AkeylessGWApiURL != nil {
+		in, out := &in.AkeylessGWApiURL, &out.AkeylessGWApiURL
+		*out = new(string)
+		**out = **in
+	}
+	if in.Auth != nil {
+		in, out := &in.Auth, &out.Auth
+		*out = new(AkeylessAuth)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AkeylessProvider.
+func (in *AkeylessProvider) DeepCopy() *AkeylessProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(AkeylessProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AlibabaAuth) DeepCopyInto(out *AlibabaAuth) {
 	*out = *in
@@ -183,6 +243,11 @@ func (in *AzureKVAuth) DeepCopy() *AzureKVAuth {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
 	*out = *in
+	if in.AuthType != nil {
+		in, out := &in.AuthType, &out.AuthType
+		*out = new(AuthType)
+		**out = **in
+	}
 	if in.VaultURL != nil {
 		in, out := &in.VaultURL, &out.VaultURL
 		*out = new(string)
@@ -198,6 +263,11 @@ func (in *AzureKVProvider) DeepCopyInto(out *AzureKVProvider) {
 		*out = new(AzureKVAuth)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.IdentityID != nil {
+		in, out := &in.IdentityID, &out.IdentityID
+		*out = new(string)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureKVProvider.
@@ -213,6 +283,11 @@ func (in *AzureKVProvider) DeepCopy() *AzureKVProvider {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CAProvider) DeepCopyInto(out *CAProvider) {
 	*out = *in
+	if in.Namespace != nil {
+		in, out := &in.Namespace, &out.Namespace
+		*out = new(string)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CAProvider.
@@ -527,7 +602,16 @@ func (in *ExternalSecretTemplateMetadata) DeepCopy() *ExternalSecretTemplateMeta
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GCPSMAuth) DeepCopyInto(out *GCPSMAuth) {
 	*out = *in
-	in.SecretRef.DeepCopyInto(&out.SecretRef)
+	if in.SecretRef != nil {
+		in, out := &in.SecretRef, &out.SecretRef
+		*out = new(GCPSMAuthSecretRef)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.WorkloadIdentity != nil {
+		in, out := &in.WorkloadIdentity, &out.WorkloadIdentity
+		*out = new(GCPWorkloadIdentity)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPSMAuth.
@@ -572,6 +656,22 @@ func (in *GCPSMProvider) DeepCopy() *GCPSMProvider {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *GCPWorkloadIdentity) DeepCopyInto(out *GCPWorkloadIdentity) {
+	*out = *in
+	in.ServiceAccountRef.DeepCopyInto(&out.ServiceAccountRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GCPWorkloadIdentity.
+func (in *GCPWorkloadIdentity) DeepCopy() *GCPWorkloadIdentity {
+	if in == nil {
+		return nil
+	}
+	out := new(GCPWorkloadIdentity)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *GitlabAuth) DeepCopyInto(out *GitlabAuth) {
 	*out = *in
@@ -794,6 +894,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(AzureKVProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Akeyless != nil {
+		in, out := &in.Akeyless, &out.Akeyless
+		*out = new(AkeylessProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Vault != nil {
 		in, out := &in.Vault, &out.Vault
 		*out = new(VaultProvider)
@@ -829,6 +934,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(AlibabaProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Webhook != nil {
+		in, out := &in.Webhook, &out.Webhook
+		*out = new(WebhookProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
@@ -856,6 +966,31 @@ func (in *SecretStoreRef) DeepCopy() *SecretStoreRef {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SecretStoreRetrySettings) DeepCopyInto(out *SecretStoreRetrySettings) {
+	*out = *in
+	if in.MaxRetries != nil {
+		in, out := &in.MaxRetries, &out.MaxRetries
+		*out = new(int32)
+		**out = **in
+	}
+	if in.RetryInterval != nil {
+		in, out := &in.RetryInterval, &out.RetryInterval
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreRetrySettings.
+func (in *SecretStoreRetrySettings) DeepCopy() *SecretStoreRetrySettings {
+	if in == nil {
+		return nil
+	}
+	out := new(SecretStoreRetrySettings)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SecretStoreSpec) DeepCopyInto(out *SecretStoreSpec) {
 	*out = *in
@@ -864,6 +999,11 @@ func (in *SecretStoreSpec) DeepCopyInto(out *SecretStoreSpec) {
 		*out = new(SecretStoreProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.RetrySettings != nil {
+		in, out := &in.RetrySettings, &out.RetrySettings
+		*out = new(SecretStoreRetrySettings)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreSpec.
@@ -1126,7 +1266,7 @@ func (in *VaultProvider) DeepCopyInto(out *VaultProvider) {
 	if in.CAProvider != nil {
 		in, out := &in.CAProvider, &out.CAProvider
 		*out = new(CAProvider)
-		**out = **in
+		(*in).DeepCopyInto(*out)
 	}
 }
 
@@ -1140,6 +1280,102 @@ func (in *VaultProvider) DeepCopy() *VaultProvider {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WebhookCAProvider) DeepCopyInto(out *WebhookCAProvider) {
+	*out = *in
+	if in.Namespace != nil {
+		in, out := &in.Namespace, &out.Namespace
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookCAProvider.
+func (in *WebhookCAProvider) DeepCopy() *WebhookCAProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(WebhookCAProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WebhookProvider) DeepCopyInto(out *WebhookProvider) {
+	*out = *in
+	if in.Headers != nil {
+		in, out := &in.Headers, &out.Headers
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	if in.Timeout != nil {
+		in, out := &in.Timeout, &out.Timeout
+		*out = new(v1.Duration)
+		**out = **in
+	}
+	out.Result = in.Result
+	if in.Secrets != nil {
+		in, out := &in.Secrets, &out.Secrets
+		*out = make([]WebhookSecret, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	if in.CABundle != nil {
+		in, out := &in.CABundle, &out.CABundle
+		*out = make([]byte, len(*in))
+		copy(*out, *in)
+	}
+	if in.CAProvider != nil {
+		in, out := &in.CAProvider, &out.CAProvider
+		*out = new(WebhookCAProvider)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookProvider.
+func (in *WebhookProvider) DeepCopy() *WebhookProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(WebhookProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WebhookResult) DeepCopyInto(out *WebhookResult) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookResult.
+func (in *WebhookResult) DeepCopy() *WebhookResult {
+	if in == nil {
+		return nil
+	}
+	out := new(WebhookResult)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WebhookSecret) DeepCopyInto(out *WebhookSecret) {
+	*out = *in
+	in.SecretRef.DeepCopyInto(&out.SecretRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebhookSecret.
+func (in *WebhookSecret) DeepCopy() *WebhookSecret {
+	if in == nil {
+		return nil
+	}
+	out := new(WebhookSecret)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
 	*out = *in
@@ -1156,10 +1392,31 @@ func (in *YandexLockboxAuth) DeepCopy() *YandexLockboxAuth {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *YandexLockboxCAProvider) DeepCopyInto(out *YandexLockboxCAProvider) {
+	*out = *in
+	in.Certificate.DeepCopyInto(&out.Certificate)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxCAProvider.
+func (in *YandexLockboxCAProvider) DeepCopy() *YandexLockboxCAProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(YandexLockboxCAProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *YandexLockboxProvider) DeepCopyInto(out *YandexLockboxProvider) {
 	*out = *in
 	in.Auth.DeepCopyInto(&out.Auth)
+	if in.CAProvider != nil {
+		in, out := &in.CAProvider, &out.CAProvider
+		*out = new(YandexLockboxCAProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxProvider.

+ 1 - 0
apis/meta/v1/zz_generated.deepcopy.go

@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*

+ 2 - 2
deploy/charts/external-secrets/Chart.yaml

@@ -2,8 +2,8 @@ apiVersion: v2
 name: external-secrets
 description: External secret management for Kubernetes
 type: application
-version: "0.3.7"
-appVersion: "v0.3.7"
+version: "0.3.11"
+appVersion: "v0.3.11"
 kubeVersion: ">= 1.11.0-0"
 keywords:
   - kubernetes-external-secrets

+ 4 - 2
deploy/charts/external-secrets/README.md

@@ -4,7 +4,7 @@
 
 [//]: # (README.md generated by gotmpl. DO NOT EDIT.)
 
-![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.7](https://img.shields.io/badge/Version-0.3.7-informational?style=flat-square)
+![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![Version: 0.3.11](https://img.shields.io/badge/Version-0.3.11-informational?style=flat-square)
 
 External secret management for Kubernetes
 
@@ -35,6 +35,8 @@ The command removes all the Kubernetes components associated with the chart and
 | Key | Type | Default | Description |
 |-----|------|---------|-------------|
 | affinity | object | `{}` |  |
+| concurrent | int | `1` | Specifies the number of concurrent ExternalSecret Reconciles external-secret executes at a time. |
+| deploymentAnnotations | object | `{}` | Annotations to add to Deployment |
 | extraArgs | object | `{}` |  |
 | extraEnv | list | `[]` |  |
 | fullnameOverride | string | `""` |  |
@@ -46,7 +48,7 @@ The command removes all the Kubernetes components associated with the chart and
 | leaderElect | bool | `false` | If true, external-secrets will perform leader election between instances to ensure no more than one instance of external-secrets operates at a time. |
 | nameOverride | string | `""` |  |
 | nodeSelector | object | `{}` |  |
-| podAnnotations | object | `{}` |  |
+| podAnnotations | object | `{}` | Annotations to add to Pod |
 | podLabels | object | `{}` |  |
 | podSecurityContext | object | `{}` |  |
 | priorityClassName | string | `""` | Pod priority class name. |

+ 8 - 1
deploy/charts/external-secrets/templates/deployment.yaml

@@ -5,6 +5,10 @@ metadata:
   namespace: {{ .Release.Namespace | quote }}
   labels:
     {{- include "external-secrets.labels" . | nindent 4 }}
+  {{- with .Values.deploymentAnnotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
 spec:
   replicas: {{ .Values.replicaCount }}
   selector:
@@ -39,7 +43,7 @@ spec:
           {{- end }}
           image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
           imagePullPolicy: {{ .Values.image.pullPolicy }}
-          {{- if or (.Values.leaderElect) (.Values.scopedNamespace) (.Values.extraArgs) }}
+          {{- if or (.Values.leaderElect) (.Values.scopedNamespace) (.Values.concurrent) (.Values.extraArgs) }}
           args:
           {{- if .Values.leaderElect }}
           - --enable-leader-election=true
@@ -47,6 +51,9 @@ spec:
           {{- if .Values.scopedNamespace }}
           - --namespace={{ .Values.scopedNamespace }}
           {{- end }}
+          {{- if .Values.concurrent }}
+          - --concurrent={{ .Values.concurrent }}
+          {{- end }}
           {{- range $key, $value := .Values.extraArgs }}
             {{- if $value }}
           - --{{ $key }}={{ $value }}

+ 9 - 0
deploy/charts/external-secrets/values.yaml

@@ -21,6 +21,10 @@ leaderElect: false
 # provided namespace
 scopedNamespace: ""
 
+# -- Specifies the number of concurrent ExternalSecret Reconciles external-secret executes at
+# a time.
+concurrent: 1
+
 serviceAccount:
   # -- Specifies whether a service account should be created.
   create: true
@@ -40,7 +44,12 @@ extraEnv: []
 ## -- Map of extra arguments to pass to container.
 extraArgs: {}
 
+# -- Annotations to add to Deployment
+deploymentAnnotations: {}
+
+# -- Annotations to add to Pod
 podAnnotations: {}
+
 podLabels: {}
 
 podSecurityContext: {}

+ 279 - 8
deploy/crds/external-secrets.io_clustersecretstores.yaml

@@ -54,6 +54,94 @@ spec:
                 maxProperties: 1
                 minProperties: 1
                 properties:
+                  akeyless:
+                    description: Akeyless configures this store to sync secrets using
+                      Akeyless Vault provider
+                    properties:
+                      akeylessGWApiURL:
+                        description: Akeyless GW API Url from which the secrets to
+                          be fetched from.
+                        type: string
+                      authSecretRef:
+                        description: Auth configures how the operator authenticates
+                          with Akeyless.
+                        properties:
+                          secretRef:
+                            description: 'AkeylessAuthSecretRef AKEYLESS_ACCESS_TYPE_PARAM:
+                              AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY OR KUB_CONFIG_NAME.'
+                            properties:
+                              accessID:
+                                description: The SecretAccessID is used for authentication
+                                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
+                              accessType:
+                                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
+                              accessTypeParam:
+                                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
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                    required:
+                    - akeylessGWApiURL
+                    - authSecretRef
+                    type: object
                   alibaba:
                     description: Alibaba configures this store to sync secrets using
                       Alibaba Cloud provider
@@ -222,7 +310,7 @@ spec:
                     properties:
                       authSecretRef:
                         description: Auth configures how the operator authenticates
-                          with Azure.
+                          with Azure. Required for ServicePrincipal auth type.
                         properties:
                           clientId:
                             description: The Azure clientId of the service principle
@@ -266,17 +354,30 @@ spec:
                         - clientId
                         - clientSecret
                         type: object
+                      authType:
+                        default: ServicePrincipal
+                        description: 'Auth type defines how to authenticate to the
+                          keyvault service. Valid values are: - "ServicePrincipal"
+                          (default): Using a service principal (tenantId, clientId,
+                          clientSecret) - "ManagedIdentity": Using Managed Identity
+                          assigned to the pod (see aad-pod-identity)'
+                        enum:
+                        - ServicePrincipal
+                        - ManagedIdentity
+                        type: string
+                      identityId:
+                        description: If multiple Managed Identity is assigned to the
+                          pod, you can select the one to be used
+                        type: string
                       tenantId:
                         description: TenantID configures the Azure Tenant to send
-                          requests to.
+                          requests to. Required for ServicePrincipal auth type.
                         type: string
                       vaultUrl:
                         description: Vault Url from which the secrets to be fetched
                           from.
                         type: string
                     required:
-                    - authSecretRef
-                    - tenantId
                     - vaultUrl
                     type: object
                   gcpsm:
@@ -310,8 +411,33 @@ spec:
                                     type: string
                                 type: object
                             type: object
-                        required:
-                        - secretRef
+                          workloadIdentity:
+                            properties:
+                              clusterLocation:
+                                type: string
+                              clusterName:
+                                type: string
+                              serviceAccountRef:
+                                description: A reference to a ServiceAccount resource.
+                                properties:
+                                  name:
+                                    description: The name of the ServiceAccount 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
+                                required:
+                                - name
+                                type: object
+                            required:
+                            - clusterLocation
+                            - clusterName
+                            - serviceAccountRef
+                            type: object
                         type: object
                       projectID:
                         description: ProjectID project where secret is located
@@ -578,6 +704,11 @@ spec:
                             description: Jwt authenticates with Vault by passing role
                               and JWT token using the JWT/OIDC authentication method
                             properties:
+                              path:
+                                default: jwt
+                                description: 'Path where the JWT authentication backend
+                                  is mounted in Vault, e.g: "jwt"'
+                                type: string
                               role:
                                 description: Role is a JWT role to authenticate using
                                   the JWT/OIDC Vault authentication method
@@ -604,6 +735,8 @@ spec:
                                       the referent.
                                     type: string
                                 type: object
+                            required:
+                            - path
                             type: object
                           kubernetes:
                             description: Kubernetes authenticates with Vault by passing
@@ -674,6 +807,11 @@ spec:
                               username/password pair using the LDAP authentication
                               method
                             properties:
+                              path:
+                                default: ldap
+                                description: 'Path where the LDAP authentication backend
+                                  is mounted in Vault, e.g: "ldap"'
+                                type: string
                               secretRef:
                                 description: SecretRef to a key in a Secret resource
                                   containing password for the LDAP user used to authenticate
@@ -702,6 +840,7 @@ spec:
                                   method
                                 type: string
                             required:
+                            - path
                             - username
                             type: object
                           tokenSecretRef:
@@ -745,7 +884,6 @@ spec:
                               type.
                             type: string
                           namespace:
-                            default: Default
                             description: The namespace the Provider type is in.
                             type: string
                           type:
@@ -757,7 +895,6 @@ spec:
                             type: string
                         required:
                         - name
-                        - namespace
                         - type
                         type: object
                       namespace:
@@ -790,6 +927,106 @@ spec:
                     - path
                     - server
                     type: object
+                  webhook:
+                    description: Webhook configures this store to sync secrets using
+                      a generic templated webhook
+                    properties:
+                      body:
+                        description: Body
+                        type: string
+                      caBundle:
+                        description: PEM encoded CA bundle used to validate webhook
+                          server certificate. Only used if the Server URL is using
+                          HTTPS protocol. This parameter is ignored for plain HTTP
+                          protocol connection. If not set the system root certificates
+                          are used to validate the TLS connection.
+                        format: byte
+                        type: string
+                      caProvider:
+                        description: The provider for the CA bundle to use to validate
+                          webhook server certificate.
+                        properties:
+                          key:
+                            description: The key the value inside of the provider
+                              type to use, only used with "Secret" type
+                            type: string
+                          name:
+                            description: The name of the object located at the provider
+                              type.
+                            type: string
+                          namespace:
+                            description: The namespace the Provider type is in.
+                            type: string
+                          type:
+                            description: The type of provider to use such as "Secret",
+                              or "ConfigMap".
+                            enum:
+                            - Secret
+                            - ConfigMap
+                            type: string
+                        required:
+                        - name
+                        - type
+                        type: object
+                      headers:
+                        additionalProperties:
+                          type: string
+                        description: Headers
+                        type: object
+                      method:
+                        description: Webhook Method
+                        type: string
+                      result:
+                        description: Result formatting
+                        properties:
+                          jsonPath:
+                            description: Json path of return value
+                            type: string
+                        type: object
+                      secrets:
+                        description: Secrets to fill in templates These secrets will
+                          be passed to the templating function as key value pairs
+                          under the given name
+                        items:
+                          properties:
+                            name:
+                              description: Name of this secret in templates
+                              type: string
+                            secretRef:
+                              description: Secret ref to fill in credentials
+                              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:
+                          - name
+                          - secretRef
+                          type: object
+                        type: array
+                      timeout:
+                        description: Timeout
+                        type: string
+                      url:
+                        description: Webhook url to call
+                        type: string
+                    required:
+                    - result
+                    - url
+                    type: object
                   yandexlockbox:
                     description: YandexLockbox configures this store to sync secrets
                       using Yandex Lockbox provider
@@ -820,10 +1057,44 @@ spec:
                                 type: string
                             type: object
                         type: object
+                      caProvider:
+                        description: The provider for the CA bundle to use to validate
+                          Yandex.Cloud server certificate.
+                        properties:
+                          certSecretRef:
+                            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
+                        type: object
                     required:
                     - auth
                     type: object
                 type: object
+              retrySettings:
+                description: Used to configure http retries if failed
+                properties:
+                  maxRetries:
+                    format: int32
+                    type: integer
+                  retryInterval:
+                    type: string
+                type: object
             required:
             - provider
             type: object

+ 279 - 8
deploy/crds/external-secrets.io_secretstores.yaml

@@ -54,6 +54,94 @@ spec:
                 maxProperties: 1
                 minProperties: 1
                 properties:
+                  akeyless:
+                    description: Akeyless configures this store to sync secrets using
+                      Akeyless Vault provider
+                    properties:
+                      akeylessGWApiURL:
+                        description: Akeyless GW API Url from which the secrets to
+                          be fetched from.
+                        type: string
+                      authSecretRef:
+                        description: Auth configures how the operator authenticates
+                          with Akeyless.
+                        properties:
+                          secretRef:
+                            description: 'AkeylessAuthSecretRef AKEYLESS_ACCESS_TYPE_PARAM:
+                              AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY OR KUB_CONFIG_NAME.'
+                            properties:
+                              accessID:
+                                description: The SecretAccessID is used for authentication
+                                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
+                              accessType:
+                                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
+                              accessTypeParam:
+                                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
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                    required:
+                    - akeylessGWApiURL
+                    - authSecretRef
+                    type: object
                   alibaba:
                     description: Alibaba configures this store to sync secrets using
                       Alibaba Cloud provider
@@ -222,7 +310,7 @@ spec:
                     properties:
                       authSecretRef:
                         description: Auth configures how the operator authenticates
-                          with Azure.
+                          with Azure. Required for ServicePrincipal auth type.
                         properties:
                           clientId:
                             description: The Azure clientId of the service principle
@@ -266,17 +354,30 @@ spec:
                         - clientId
                         - clientSecret
                         type: object
+                      authType:
+                        default: ServicePrincipal
+                        description: 'Auth type defines how to authenticate to the
+                          keyvault service. Valid values are: - "ServicePrincipal"
+                          (default): Using a service principal (tenantId, clientId,
+                          clientSecret) - "ManagedIdentity": Using Managed Identity
+                          assigned to the pod (see aad-pod-identity)'
+                        enum:
+                        - ServicePrincipal
+                        - ManagedIdentity
+                        type: string
+                      identityId:
+                        description: If multiple Managed Identity is assigned to the
+                          pod, you can select the one to be used
+                        type: string
                       tenantId:
                         description: TenantID configures the Azure Tenant to send
-                          requests to.
+                          requests to. Required for ServicePrincipal auth type.
                         type: string
                       vaultUrl:
                         description: Vault Url from which the secrets to be fetched
                           from.
                         type: string
                     required:
-                    - authSecretRef
-                    - tenantId
                     - vaultUrl
                     type: object
                   gcpsm:
@@ -310,8 +411,33 @@ spec:
                                     type: string
                                 type: object
                             type: object
-                        required:
-                        - secretRef
+                          workloadIdentity:
+                            properties:
+                              clusterLocation:
+                                type: string
+                              clusterName:
+                                type: string
+                              serviceAccountRef:
+                                description: A reference to a ServiceAccount resource.
+                                properties:
+                                  name:
+                                    description: The name of the ServiceAccount 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
+                                required:
+                                - name
+                                type: object
+                            required:
+                            - clusterLocation
+                            - clusterName
+                            - serviceAccountRef
+                            type: object
                         type: object
                       projectID:
                         description: ProjectID project where secret is located
@@ -578,6 +704,11 @@ spec:
                             description: Jwt authenticates with Vault by passing role
                               and JWT token using the JWT/OIDC authentication method
                             properties:
+                              path:
+                                default: jwt
+                                description: 'Path where the JWT authentication backend
+                                  is mounted in Vault, e.g: "jwt"'
+                                type: string
                               role:
                                 description: Role is a JWT role to authenticate using
                                   the JWT/OIDC Vault authentication method
@@ -604,6 +735,8 @@ spec:
                                       the referent.
                                     type: string
                                 type: object
+                            required:
+                            - path
                             type: object
                           kubernetes:
                             description: Kubernetes authenticates with Vault by passing
@@ -674,6 +807,11 @@ spec:
                               username/password pair using the LDAP authentication
                               method
                             properties:
+                              path:
+                                default: ldap
+                                description: 'Path where the LDAP authentication backend
+                                  is mounted in Vault, e.g: "ldap"'
+                                type: string
                               secretRef:
                                 description: SecretRef to a key in a Secret resource
                                   containing password for the LDAP user used to authenticate
@@ -702,6 +840,7 @@ spec:
                                   method
                                 type: string
                             required:
+                            - path
                             - username
                             type: object
                           tokenSecretRef:
@@ -745,7 +884,6 @@ spec:
                               type.
                             type: string
                           namespace:
-                            default: Default
                             description: The namespace the Provider type is in.
                             type: string
                           type:
@@ -757,7 +895,6 @@ spec:
                             type: string
                         required:
                         - name
-                        - namespace
                         - type
                         type: object
                       namespace:
@@ -790,6 +927,106 @@ spec:
                     - path
                     - server
                     type: object
+                  webhook:
+                    description: Webhook configures this store to sync secrets using
+                      a generic templated webhook
+                    properties:
+                      body:
+                        description: Body
+                        type: string
+                      caBundle:
+                        description: PEM encoded CA bundle used to validate webhook
+                          server certificate. Only used if the Server URL is using
+                          HTTPS protocol. This parameter is ignored for plain HTTP
+                          protocol connection. If not set the system root certificates
+                          are used to validate the TLS connection.
+                        format: byte
+                        type: string
+                      caProvider:
+                        description: The provider for the CA bundle to use to validate
+                          webhook server certificate.
+                        properties:
+                          key:
+                            description: The key the value inside of the provider
+                              type to use, only used with "Secret" type
+                            type: string
+                          name:
+                            description: The name of the object located at the provider
+                              type.
+                            type: string
+                          namespace:
+                            description: The namespace the Provider type is in.
+                            type: string
+                          type:
+                            description: The type of provider to use such as "Secret",
+                              or "ConfigMap".
+                            enum:
+                            - Secret
+                            - ConfigMap
+                            type: string
+                        required:
+                        - name
+                        - type
+                        type: object
+                      headers:
+                        additionalProperties:
+                          type: string
+                        description: Headers
+                        type: object
+                      method:
+                        description: Webhook Method
+                        type: string
+                      result:
+                        description: Result formatting
+                        properties:
+                          jsonPath:
+                            description: Json path of return value
+                            type: string
+                        type: object
+                      secrets:
+                        description: Secrets to fill in templates These secrets will
+                          be passed to the templating function as key value pairs
+                          under the given name
+                        items:
+                          properties:
+                            name:
+                              description: Name of this secret in templates
+                              type: string
+                            secretRef:
+                              description: Secret ref to fill in credentials
+                              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:
+                          - name
+                          - secretRef
+                          type: object
+                        type: array
+                      timeout:
+                        description: Timeout
+                        type: string
+                      url:
+                        description: Webhook url to call
+                        type: string
+                    required:
+                    - result
+                    - url
+                    type: object
                   yandexlockbox:
                     description: YandexLockbox configures this store to sync secrets
                       using Yandex Lockbox provider
@@ -820,10 +1057,44 @@ spec:
                                 type: string
                             type: object
                         type: object
+                      caProvider:
+                        description: The provider for the CA bundle to use to validate
+                          Yandex.Cloud server certificate.
+                        properties:
+                          certSecretRef:
+                            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
+                        type: object
                     required:
                     - auth
                     type: object
                 type: object
+              retrySettings:
+                description: Used to configure http retries if failed
+                properties:
+                  maxRetries:
+                    format: int32
+                    type: integer
+                  retryInterval:
+                    type: string
+                type: object
             required:
             - provider
             type: object

+ 3 - 3
docs/guides-all-keys-one-secret.md

@@ -22,6 +22,6 @@ Now, when creating our ExternalSecret resource, instead of using the data field,
 To check both values we can run:
 
 ```
-kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.username}' | base64 -d
-kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.surname}' | base64 -d
-```
+kubectl get secret secret-to-be-created -n <namespace> -o jsonpath='{.data.username}' | base64 -d
+kubectl get secret secret-to-be-created -n <namespace> -o jsonpath='{.data.surname}' | base64 -d
+```

+ 4 - 0
docs/guides-getting-started.md

@@ -77,6 +77,10 @@ Events:                    <none>
 For more advanced examples, please read the other
 [guides](guides-introduction.md).
 
+## Installing with OLM
+
+External-secrets can be managed by [Operator Lifecycle Manager](https://olm.operatorframework.io/) (OLM) via an installer operator. It is made available through [OperatorHub.io](https://operatorhub.io/), this installation method is suited best for OpenShift. See installation instructions on the [external-secrets-operator](https://operatorhub.io/operator/external-secrets-operator) package.
+
 ## Uninstalling
 
 Before continuing, ensure that all external-secret resources that have been created by users have been deleted.

+ 133 - 0
docs/guides-gitops-using-fluxcd.md

@@ -0,0 +1,133 @@
+# GitOps using FluxCD (v2)
+
+FluxCD is a GitOps operator for Kubernetes. It synchronizes the status of the cluster from manifests allocated in
+different repositories (Git or Helm). This approach fits perfectly with External Secrets on clusters which are dynamically
+created, to get credentials with no manual intervention from the beginning.
+
+## Advantages
+
+This approach has several advantages as follows:
+
+* **Homogenize environments** allowing developers to use the same toolset in Kind in the same way they do in the cloud
+  provider distributions such as EKS or GKE. This accelerates the development
+* **Reduce security risks**, because credentials can be easily obtained, so temptation to store them locally is reduced.
+* **Application compatibility increase**: Applications are deployed in different ways, and sometimes they need to share
+  credentials. This can be done using External Secrets as a wire for them at real time.
+* **Automation by default** oh, come on!
+
+## The approach
+
+FluxCD is composed by several controllers dedicated to manage different custom resources. The most important
+ones are **Kustomization** (to clarify, Flux one, not Kubernetes' one) and **HelmRelease** to deploy using the approaches
+of the same names.
+
+External Secrets can be deployed using Helm [as explained here](guides-getting-started.md). The deployment includes the
+CRDs if enabled on the `values.yaml`, but after this, you need to deploy some `SecretStore` to start
+getting credentials from your secrets manager with External Secrets.
+
+> The idea of this guide is to deploy the whole stack, using flux, needed by developers not to worry about the credentials,
+> but only about the application and its code.
+
+## The problem
+
+This can sound easy, but External Secrets is deployed using Helm, which is managed by the HelmController,
+and your custom resources, for example a `ClusterSecretStore` and the related `Secret`, are often deployed using a
+`kustomization.yaml`, which is deployed by the KustomizeController.
+
+Both controllers manage the resources independently, at different moments, with no possibility to wait each other.
+This means that we have a wonderful race condition where sometimes the CRs (`SecretStore`,`ClusterSecretStore`...) tries
+to be deployed before than the CRDs needed to recognize them.
+
+## The solution
+
+Let's see the conditions to start working on a solution:
+
+* The External Secrets operator is deployed with Helm, and admits disabling the CRDs deployment
+* The race condition only affects the deployment of `CustomResourceDefinition` and the CRs needed later
+* CRDs can be deployed directly from the Git repository of the project using a Flux `Kustomization`
+* Required CRs can be deployed using a Flux `Kustomization` too, allowing dependency between CRDs and CRs
+* All previous manifests can be applied with a Kubernetes `kustomization`
+
+## Create the main kustomization
+
+To have a better view of things needed later, the first manifest to be created is the `kustomization.yaml`
+
+```yaml
+{% include 'gitops/kustomization.yaml' %}
+```
+
+## Create the secret
+
+To access your secret manager, External Secrets needs some credentials. They are stored inside a Secret, which is intended
+to be deployed by automation as a good practise. This time, a placeholder called `secret-token.yaml` is show as an example:
+
+```yaml
+# The namespace.yaml first
+{% include 'gitops/namespace.yaml' %}
+```
+
+```yaml
+{% include 'gitops/secret-token.yaml' %}
+```
+
+## Creating the references to repositories
+
+Create a manifest called `repositories.yaml` to store the references to external repositories for Flux
+
+```yaml
+{% include 'gitops/repositories.yaml' %}
+```
+
+## Deploy the CRDs
+
+As mentioned, CRDs can be deployed using the official Helm package, but to solve the race condition, they will be deployed
+from our git repository using a Kustomization manifest called `deployment-crds.yaml` as follows:
+
+```yaml
+{% include 'gitops/deployment-crds.yaml' %}
+```
+
+## Deploy the operator
+
+The operator is deployed using a HelmRelease manifest to deploy the Helm package, but due to the special race condition,
+the deployment must be disabled in the `values` of the manifest called `deployment.yaml`, as follows:
+
+```yaml
+{% include 'gitops/deployment.yaml' %}
+```
+
+## Deploy the CRs
+
+Now, be ready for the arcane magic. Create a Kustomization manifest called `deployment-crs.yaml` with the following content:
+
+```yaml
+{% include 'gitops/deployment-crs.yaml' %}
+```
+
+There are several interesting details to see here, that finally solves the race condition:
+
+1. First one is the field `dependsOn`, which points to a previous Kustomization called `external-secrets-crds`. This
+   dependency forces this deployment to wait for the other to be ready, before start being deployed.
+2. The reference to the place where to find the CRs
+   ```yaml
+   path: ./infrastructure/external-secrets/crs
+   sourceRef:
+    kind: GitRepository
+    name: flux-system
+   ```
+   Custom Resources will be searched in the relative path `./infrastructure/external-secrets/crs` of the GitRepository
+   called `flux-system`, which is a reference to the same repository that FluxCD watches to synchronize the cluster.
+   With fewer words, a reference to itself, but going to another directory called `crs`
+
+Of course, allocate inside the mentioned path `./infrastructure/external-secrets/crs`, all the desired CRs to be deployed,
+for example, a manifest `clusterSecretStore.yaml` to reach your Hashicorp Vault as follows:
+
+```yaml
+{% include 'gitops/crs/clusterSecretStore.yaml' %}
+```
+
+## Results
+
+At the end, the required files tree is shown in the following picture:
+
+![FluxCD files tree](./pictures/screenshot_gitops_final_directory_tree.png)

+ 26 - 0
docs/guides-using-latest-image.md

@@ -0,0 +1,26 @@
+You can test a feature that was not yet released using the following method, use it at your own discretion:
+
+1. Create a `values.yaml` file with the following content:
+   
+```
+replicaCount: 1
+
+image:
+  repository: ghcr.io/external-secrets/external-secrets
+  pullPolicy: IfNotPresent
+  # -- The image tag to use. The default is the chart appVersion.
+  tag: "main"
+
+# -- If set, install and upgrade CRDs through helm chart.
+installCRDs: false
+```
+
+2. Install the crds
+```
+make crds.install
+```
+
+3. Install the external-secrets Helm chart indicating the values file created before:
+```
+helm install external-secrets external-secrets/external-secrets -f values.yaml
+``` 

+ 1 - 1
docs/index.md

@@ -25,7 +25,7 @@ lifecycle of the secrets for you.
 To get started, please read through [API overview](api-overview.md) this should
 give you a high-level overview to understand the API and use-cases. After that
 please follow one of our [guides](guides-introduction.md) to get a jump start
-using the operator.
+using the operator. See our [getting started guide](guides-getting-started.md) for installation instructions.
 
 For a complete reference of the API types please refer to our [API
 Reference](spec.md).

BIN
docs/pictures/screenshot_gitops_final_directory_tree.png


+ 68 - 0
docs/provider-akeyless.md

@@ -0,0 +1,68 @@
+## Akeyless Vault
+
+External Secrets Operator integrates with [Akeyless API](https://docs.akeyless.io/reference#v2).
+
+### Authentication
+
+The API requires an access-id, access-type and access-Type-param.
+
+The supported auth-methods and their params are:
+
+| accessType  | accessTypeParam                                                                                                                                                                                                                      |
+| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `api_key`      | The access key.                                                                                                                                     |
+| `k8s`         | The k8s configuration name |
+| `aws_iam` |   -                                                         |
+| `gcp` |      The gcp audience                                                      |
+| `azure_ad` |  azure object id  (optional)                                                          |
+
+form more information about [Akeyless Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods)
+
+### Akeless credentials secret
+
+Create a secret containing your credentials:
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+  name: akeylss-secret-creds
+type: Opaque
+stringData:
+  accessId: "p-XXXX"
+  accessType:  # k8s/aws_iam/gcp/azure_ad/api_key
+  accessTypeParam:  # can be one of the following: k8s-conf-name/gcp-audience/azure-obj-id/access-key
+```
+
+### Update secret store
+Be sure the `akeyless` provider is listed in the `Kind=SecretStore` and the `akeylessGWApiURL` is set (def: "https://api.akeless.io".
+
+```yaml
+{% include 'akeyless-secret-store.yaml' %}
+```
+
+### Creating external secret
+
+To get a secret from Akeyless and secret it on the Kubernetes cluster, a `Kind=ExternalSecret` is needed.
+
+```yaml
+{% include 'akeyless-external-secret.yaml' %}
+```
+
+#### Using DataFrom
+
+DataFrom can be used to get a secret as a JSON string and attempt to parse it.
+
+```yaml
+{% include 'akeyless-external-secret-json.yaml' %}
+```
+
+### Getting the Kubernetes secret
+The operator will fetch the secret and inject it as a `Kind=Secret`.
+```
+kubectl get secret akeyless-secret-to-create -o jsonpath='{.data.secretKey}' | base64 -d
+```
+
+```
+kubectl get secret akeyless-secret-to-create-json -o jsonpath='{.data}'
+```

+ 16 - 2
docs/provider-azure-key-vault.md

@@ -7,23 +7,37 @@ External Secrets Operator integrates with [Azure Key vault](https://azure.micros
 
 ### Authentication
 
-At the moment, we only support [service principals](https://docs.microsoft.com/en-us/azure/key-vault/general/authentication) authentication.
+We support Service Principals and Managed Identity [authentication](https://docs.microsoft.com/en-us/azure/key-vault/general/authentication).
+
+To use Managed Identity authentication, you should use [aad-pod-identity](https://azure.github.io/aad-pod-identity/docs/) to assign the identity to external-secrets operator. To add the selector to external-secrets operator, use `podLabels` in your values.yaml in case of Helm installation of external-secrets.
 
 #### Service Principal key authentication
 
 A service Principal client and Secret is created and the JSON keyfile is stored in a `Kind=Secret`. The `ClientID` and `ClientSecret` should be configured for the secret. This service principal should have proper access rights to the keyvault to be managed by the operator
 
+#### Managed Identity authentication
+
+A Managed Identity should be created in Azure, and that Identity should have proper rights to the keyvault to be managed by the operator.
+
+If there are multiple Managed Identitites for different keyvaults, the operator should have been assigned all identities via [aad-pod-identity](https://azure.github.io/aad-pod-identity/docs/), then the SecretStore configuration should include the Id of the idenetity to be used via the `identityId` field.
+
 ```yaml
 {% include 'azkv-credentials-secret.yaml' %}
 ```
 
 ### Update secret store
-Be sure the `azkv` provider is listed in the `Kind=SecretStore`
+Be sure the `azurekv` provider is listed in the `Kind=SecretStore`
 
 ```yaml
 {% include 'azkv-secret-store.yaml' %}
 ```
 
+Or in case of Managed Idenetity authentication:
+
+```yaml
+{% include 'azkv-secret-store-mi.yaml' %}
+```
+
 ### Object Types
 
 Azure KeyVault manages different [object types](https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#object-types), we support `keys`, `secrets` and `certificates`. Simply prefix the key with `key`, `secret` or `cert` to retrieve the desired type (defaults to secret).

+ 80 - 43
docs/provider-google-secrets-manager.md

@@ -2,39 +2,67 @@
 
 External Secrets Operator integrates with [GCP Secret Manager](https://cloud.google.com/secret-manager) for secret management.
 
-### Service account key authentication
+## Authentication
 
-A service account key is created and the JSON keyfile is stored in a `Kind=Secret`. The `project_id` and `private_key` should be configured for the project.
+### Workload Identity
 
-```yaml
-{% include 'gcpsm-credentials-secret.yaml' %}
-```
+Your Google Kubernetes Engine (GKE) applications can consume GCP services like Secrets Manager without using static, long-lived authentication tokens. This is our recommended approach of handling credentials in GCP. ESO offers two options for integrating with GKE workload identity: **pod-based workload identity** and **using service accounts directly**. Before using either way you need to create a service account - this is covered below.
 
-### Update secret store
-Be sure the `gcpsm` provider is listed in the `Kind=SecretStore`
+#### Creating Workload Identity Service Accounts
 
-```yaml
-{% include 'gcpsm-secret-store.yaml' %}
-```
+You can find the documentation for Workload Identity [here](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). We will walk you through how to navigate it here.
 
-### Creating external secret
+Search [the documment](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) for this editable values and change them to your values:
 
-To create a kubernetes secret from the GCP Secret Manager secret a `Kind=ExternalSecret` is needed.
+- `CLUSTER_NAME`: The name of your cluster
+- `PROJECT_ID`: Your project ID (not your Project number nor your Project name)
+- `K8S_NAMESPACE`: For us folowing these steps here it will be `es`, but this will be the namespace where you deployed the external-secrets operator
+- `KSA_NAME`: external-secrets (if you are not creating a new one to attach to the deployemnt)
+- `GSA_NAME`: external-secrets for simplicity, or something else if you have to follow different naming convetions for cloud resources
+- `ROLE_NAME`: should be `roles/secretmanager.secretAccessor` - so you make the pod only be able to access secrets on Secret Manager
+
+#### Using Service Accounts directly
+
+Let's assume you have created a service account correctly and attached a appropriate workload identity. It should roughly look like this:
 
 ```yaml
-{% include 'gcpsm-external-secret.yaml' %}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: team-a
+  namespace: team-a
+  annotations:
+    iam.gke.io/gcp-service-account: example-team-a@my-project.iam.gserviceaccount.com
 ```
 
-The operator will fetch the GCP Secret Manager secret and inject it as a `Kind=Secret`
-```
-kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.dev-secret-test}' | base64 -d
+You can reference this particular ServiceAccount in a `SecretStore` or `ClusterSecretStore`. It's important that you also set the `projectID`, `clusterLocation` and `clusterName`. The Namespace on the `serviceAccountRef` is ignored when using a `SecretStore` resource. This is needed to isolate the namespaces properly.
+
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: ClusterSecretStore
+metadata:
+  name: gcp-wi
+spec:
+  provider:
+    gcpsm:
+      projectID: my-project
+      auth:
+        workloadIdentity:
+          # name of the cluster region
+          clusterLocation: europe-central2
+          # name of the GKE cluster
+          clusterName: example-workload-identity
+          # reference the sa from above
+          serviceAccountRef:
+            name: team-a
+            namespace: team-a
 ```
 
-## Authentication with Workload Identity
+#### Using Pod-based Workload Identity
 
-This makes it possible for your Google Kubernetes Engine (GKE) applications to consume services provided by Google APIs, namely Secrets Manager service in this case.
+You can attach a Workload Identity directly to the ESO pod. ESO then has access to all the APIs defined in the attached service account policy. You attach the workload identity by (1) creating a service account with a attached workload identity (described above) and (2) using this particular service account in the pod's `serviceAccountName` field.
 
-Here we will assume that you installed ESO using helm and that you named the chart installation `external-secrets` and the namespace where it lives `es` like:
+For this example we will assume that you installed ESO using helm and that you named the chart installation `external-secrets` and the namespace where it lives `es` like:
 
 ```sh
 helm install external-secrets external-secrets/external-secrets --namespace es
@@ -42,7 +70,7 @@ helm install external-secrets external-secrets/external-secrets --namespace es
 
 Then most of the resources would have this name, the important one here being the k8s service account attached to the external-secrets operator deployment:
 
-```
+```yaml
 # ...
       containers:
       - image: ghcr.io/external-secrets/external-secrets:vVERSION
@@ -56,36 +84,45 @@ Then most of the resources would have this name, the important one here being th
       serviceAccountName: external-secrets # <--- here
 ```
 
-### Following the documentation
+The pod now has the identity. Now you need to configure the `SecretStore`.
+You just need to set the `projectID`, all other fields can be omitted.
+
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: SecretStore
+metadata:
+  name: example
+spec:
+  provider:
+    gcpsm:
+      projectID: pid
+```
 
-You can find the documentation for Workload Identity under [this url](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity). We will walk you through how to navigate it here.
+### GCP Service Account authentication
 
-#### Changing Values
+You can use [GCP Service Account](https://cloud.google.com/iam/docs/service-accounts) to authenticate with GCP. These are static, long-lived credentials. A GCP Service Account is a JSON file that needs to be stored in a `Kind=Secret`. ESO will use that Secret to authenticate with GCP. See here how you [manage GCP Service Accounts](https://cloud.google.com/iam/docs/creating-managing-service-accounts).
 
-Search [the documment](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) for this editable values and change them to your values:
+```yaml
+{% include 'gcpsm-credentials-secret.yaml' %}
+```
 
-- CLUSTER_NAME: The name of your cluster
-- PROJECT_ID: Your project ID (not your Project number nor your Project name)
-- K8S_NAMESPACE: For us folowing these steps here it will be `es`, but this will be the namespace where you deployed the external-secrets operator
-- KSA_NAME: external-secrets (if you are not creating a new one to attach to the deployemnt)
-- GSA_NAME: external-secrets for simplicity, or something else if you have to follow different naming convetions for cloud resources
-- ROLE_NAME: roles/secretmanager.secretAccessor so you make the pod only be able to access secrets on Secret Manager
+#### Update secret store
+Be sure the `gcpsm` provider is listed in the `Kind=SecretStore`
 
-#### Following through
+```yaml
+{% include 'gcpsm-secret-store.yaml' %}
+```
 
-You can follow through the documentation and adapt it to your specific use case. If you want to just use the serviceaccount that we deployed with the helm chart, for example, you don't need to create a new service account on 2 of [Authenticating to Google Cloud](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#authenticating_to).
+#### Creating external secret
 
-#### SecretStore with WorkloadIdentity
+To create a kubernetes secret from the GCP Secret Manager secret a `Kind=ExternalSecret` is needed.
 
-To use workload identity you can just omit the auth field of the secret store and let the operator client fall back to defaults using the roles attached to your service account.
+```yaml
+{% include 'gcpsm-external-secret.yaml' %}
+```
 
+The operator will fetch the GCP Secret Manager secret and inject it as a `Kind=Secret`
 ```
-apiVersion: external-secrets.io/v1alpha1
-kind: SecretStore
-metadata:
-  name: example
-spec:
-  provider:
-    gcpsm:
-      projectID: pid
-```
+kubectl get secret secret-to-be-created -n <namespace> | -o jsonpath='{.data.dev-secret-test}' | base64 -d
+```
+

+ 6 - 4
docs/provider-hashicorp-vault.md

@@ -54,7 +54,7 @@ spec:
   refreshInterval: "15s"
   secretStoreRef:
     name: vault-backend
-    kind: ClusterSecretStore
+    kind: SecretStore
   target:
     name: example-sync
   data:
@@ -77,10 +77,12 @@ Vault supports only simple key/value pairs - nested objects are not supported. H
 
 ### Authentication
 
-We support three different modes for authentication:
+We support five different modes for authentication:
 [token-based](https://www.vaultproject.io/docs/auth/token),
-[appRole](https://www.vaultproject.io/docs/auth/approle) and
-[kubernetes-native](https://www.vaultproject.io/docs/auth/kubernetes), each one comes with it's own
+[appRole](https://www.vaultproject.io/docs/auth/approle),
+[kubernetes-native](https://www.vaultproject.io/docs/auth/kubernetes),
+[ldap](https://www.vaultproject.io/docs/auth/ldap) and
+[jwt/odic](https://www.vaultproject.io/docs/auth/jwt), each one comes with it's own
 trade-offs. Depending on the authentication method you need to adapt your environment.
 
 #### Token-based authentication

+ 118 - 0
docs/provider-webhook.md

@@ -0,0 +1,118 @@
+## Generic Webhook
+
+External Secrets Operator can integrate with simple web apis by specifying the endpoint
+
+### Example
+
+First, create a SecretStore with a webhook backend.  We'll use a static user/password `root`:
+
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: SecretStore
+metadata:
+  name: webhook-backend
+spec:
+  provider:
+    webhook:
+      url: "http://httpbin.org/get?parameter={{ .remoteRef.key }}"
+      result:
+        jsonPath: "$.args.parameter"
+      headers:
+        Content-Type: application/json
+        Authorization: Basic {{ print .auth.username ":" .auth.password | b64enc }}
+      secrets:
+      - name: auth
+        secretRef:
+          name: webhook-credentials
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: webhook-credentials
+data:
+  username: dGVzdA== # "test"
+  password: dGVzdA== # "test"
+```
+
+NB: This is obviously not practical because it just returns the key as the result, but it shows how it works
+
+Now create an ExternalSecret that uses the above SecretStore:
+
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: webhook-example
+spec:
+  refreshInterval: "15s"
+  secretStoreRef:
+    name: webhook-backend
+    kind: SecretStore
+  target:
+    name: example-sync
+  data:
+  - secretKey: foobar
+    remoteRef:
+      key: secret
+---
+# will create a secret with:
+kind: Secret
+metadata:
+  name: example-sync
+data:
+  foobar: c2VjcmV0
+```
+
+#### Limitations
+
+Webhook does not support authorization, other than what can be sent by generating http headers
+
+### Templating
+
+Generic WebHook provider uses the templating engine to generate the API call.  It can be used in the url, headers, body and result.jsonPath fields.
+
+The provider inserts the secret to be retrieved in the object named `remoteRef`.
+
+In addition, secrets can be added as named objects, for example to use in authorization headers.
+Each secret has a `name` property which determines the name of the object in the templating engine.
+
+### All Parameters
+
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: ClusterSecretStore
+metadata:
+  name: statervault
+spec:
+  provider:
+    webhook:
+      # Url to call.  Use templating engine to fill in the request parameters
+      url: <url>
+      # http method, defaults to GET
+      method: <method>
+      # Timeout in duration (1s, 1m, etc)
+      timeout: 1s
+      result:
+        # [jsonPath](https://jsonpath.com) syntax, which also can be templated
+        jsonPath: <jsonPath>
+      # Map of headers, can be templated
+      headers:
+        <Header-Name>: <header contents>
+      # Body to sent as request, can be templated (optional)
+      body: <body>
+      # List of secrets to expose to the templating engine
+      secrets:
+      # Use this name to refer to this secret in templating, above
+      - name: <name>
+        secretRef:
+          namespace: <namespace>
+          name: <name>
+      # Add CAs here for the TLS handshake
+      caBundle: <base64 encoded cabundle>
+      caProvider:
+        type: Secret or COnfigMap
+        name: <name of secret or configmap>
+        namespace: <namespace>
+        key: <key inside secret>
+```
+

+ 12 - 0
docs/snippets/akeyless-credentials-secret.yaml

@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: akeylss-secret-creds
+type: Opaque
+stringData:
+  accessId: "p-XXXX"
+  accessType:  # k8s/aws_iam/gcp/azure_ad/api_key
+  accessTypeParam: # can be one of the following: k8s-conf-name/gcp-audience/azure-obj-id/access-key
+
+
+

+ 18 - 0
docs/snippets/akeyless-external-secret-json.yaml

@@ -0,0 +1,18 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: akeyless-external-secret-example-json
+spec:
+  refreshInterval: 1h
+
+  secretStoreRef:
+    kind: SecretStore
+    name: akeyless-secret-store # Must match SecretStore on the cluster
+
+  target:
+    name: akeyless-secret-to-create-json # Name for the secret to be created on the cluster
+    creationPolicy: Owner
+
+  # for json formatted secrets: each key in the json will be used as the secret key in the SECRET k8s target object
+  dataFrom:
+  - key: secret-name # Full path of the secret on Akeyless

+ 19 - 0
docs/snippets/akeyless-external-secret.yaml

@@ -0,0 +1,19 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: akeyless-external-secret-example
+spec:
+  refreshInterval: 1h
+
+  secretStoreRef:
+    kind: SecretStore
+    name: akeyless-secret-store # Must match SecretStore on the cluster
+
+  target:
+    name: akeyless-secret-to-create # Name for the secret to be created on the cluster
+    creationPolicy: Owner
+
+  data:
+    - secretKey: secretKey # Key given to the secret to be created on the cluster
+      remoteRef:
+        key: secret-name # Full path of the secret on Akeyless

+ 20 - 0
docs/snippets/akeyless-secret-store.yaml

@@ -0,0 +1,20 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: SecretStore
+metadata:
+  name: akeyless-secret-store
+spec:
+  provider:
+    akeyless:
+      # URL of your akeyless API
+      akeylessGWApiURL: "https://api.akeyless.io"
+      authSecretRef:
+        secretRef:
+          accessID:
+            name: akeylss-secret-creds
+            key: accessId
+          accessType:
+            name: akeylss-secret-creds
+            key: accessType
+          accessTypeParam:
+            name: akeylss-secret-creds
+            key: accessTypeParam

+ 13 - 0
docs/snippets/azkv-secret-store-mi.yaml

@@ -0,0 +1,13 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: SecretStore
+metadata:
+  name: example-secret-store
+spec:
+  provider:
+    # provider type: azure keyvault
+    azurekv:
+      authType: ManagedIdentity
+      # Optionally set the Id of the Managed Identity, if multiple identities is assignet to external-secrets operator
+      identityId: "<MI_clientId>"
+      # URL of your vault instance, see: https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates
+      vaultUrl: "https://my-keyvault-name.vault.azure.net"

+ 18 - 2
docs/snippets/full-cluster-secret-store.yaml

@@ -22,9 +22,9 @@ spec:
       role: iam-role
       # AWS Region to be used for the provider
       region: eu-central-1
-      # Auth defines the information necessary to authenticate against AWS by
-      # getting the accessKeyID and secretAccessKey from an already created Kubernetes Secret
+      # Auth defines the information necessary to authenticate against AWS
       auth:
+        # Getting the accessKeyID and secretAccessKey from an already created Kubernetes Secret
         secretRef:
           accessKeyID:
             name: awssm-secret
@@ -32,6 +32,12 @@ spec:
           secretAccessKey:
             name: awssm-secret
             key: secret-access-key
+        # IAM roles for service accounts
+        # https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html
+        jwt:
+          serviceAccountRef:
+            name: my-serviceaccount
+            namespace: sa-namespace
 
     vault:
       server: "https://vault.acme.org"
@@ -42,7 +48,17 @@ spec:
       version: "v2"
       # vault enterprise namespace: https://www.vaultproject.io/docs/enterprise/namespaces
       namespace: "a-team"
+      # base64 encoded string of certificate
       caBundle: "..."
+      # Instead of caBundle you can also specify a caProvider
+      # this will retrieve the cert from a Secret or ConfigMap
+      caProvider:
+        # Can be Secret or ConfigMap
+        type: "Secret"
+        # This is mandatory for ClusterSecretStore and not relevant for SecretStore
+        namespace: "my-cert-secret-namespace"
+        name: "my-cert-secret"
+        key: "cert-key"
       auth:
         # static token: https://www.vaultproject.io/docs/auth/token
         tokenSecretRef:

+ 8 - 2
docs/snippets/full-secret-store.yaml

@@ -11,6 +11,14 @@ spec:
   # Optional
   controller: dev
 
+  # You can specify retry settings for the http connection
+  # these fields allow you to set a maxRetries before failure, and
+  # an interval between the retries.
+  # Current supported providers: IBM
+  retrySettings:
+    maxRetries: 5
+    retryInterval: "10s"
+
   # provider field contains the configuration to access the provider
   # which contains the secret exactly one provider must be configured.
   provider:
@@ -50,8 +58,6 @@ spec:
       caProvider:
         # Can be Secret or ConfigMap
         type: "Secret"
-        # This is optional, if not specified will be 'Default'
-        namespace: "my-cert-secret-namespace"
         name: "my-cert-secret"
         key: "cert-key"
 

+ 17 - 0
docs/snippets/gitops/crs/clusterSecretStore.yaml

@@ -0,0 +1,17 @@
+apiVersion: external-secrets.io/v1alpha1
+kind: ClusterSecretStore
+metadata:
+  name: vault-backend-global
+spec:
+  provider:
+    vault:
+      server: "https://vault.your-domain.com"
+      path: secret
+      version: v2
+      auth:
+        # points to a secret that contains a vault token
+        # https://www.vaultproject.io/docs/auth/token
+        tokenSecretRef:
+          name: "vault-token-global"
+          key: "token"
+          namespace: external-secrets

+ 5 - 0
docs/snippets/gitops/crs/kustomization.yaml

@@ -0,0 +1,5 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+- clusterSecretStore.yaml

+ 13 - 0
docs/snippets/gitops/deployment-crds.yaml

@@ -0,0 +1,13 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
+kind: Kustomization
+metadata:
+  name: external-secrets-crds
+  namespace: flux-system
+spec:
+  interval: 10m
+  path: ./deploy/crds
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: external-secrets

+ 15 - 0
docs/snippets/gitops/deployment-crs.yaml

@@ -0,0 +1,15 @@
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
+kind: Kustomization
+metadata:
+  name: external-secrets-crs
+  namespace: flux-system
+spec:
+  dependsOn:
+    - name: external-secrets-crds
+  interval: 10m
+  path: ./infrastructure/external-secrets/crs
+  prune: true
+  sourceRef:
+    kind: GitRepository
+    name: flux-system

+ 28 - 0
docs/snippets/gitops/deployment.yaml

@@ -0,0 +1,28 @@
+# How to manage values files. Ref: https://fluxcd.io/docs/guides/helmreleases/#refer-to-values-inside-the-chart
+# How to inject values: https://fluxcd.io/docs/guides/helmreleases/#cloud-storage
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta1
+kind: HelmRelease
+metadata:
+  name: external-secrets
+  namespace: flux-system
+spec:
+  # Override Release name to avoid the pattern Namespace-Release
+  # Ref: https://fluxcd.io/docs/components/helm/api/#helm.toolkit.fluxcd.io/v2beta1.HelmRelease
+  releaseName: external-secrets
+  targetNamespace: external-secrets
+  interval: 10m
+  chart:
+    spec:
+      chart: external-secrets
+      version: 0.3.9
+      sourceRef:
+        kind: HelmRepository
+        name: external-secrets
+        namespace: flux-system
+  values:
+    installCRDs: false
+
+  # Ref: https://fluxcd.io/docs/components/helm/api/#helm.toolkit.fluxcd.io/v2beta1.Install
+  install:
+    createNamespace: true

+ 20 - 0
docs/snippets/gitops/kustomization.yaml

@@ -0,0 +1,20 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+# Deploy the Vault access secret
+- namespace.yaml
+- secret-token.yaml
+
+# Deploy the repositories
+- repositories.yaml
+
+# Deploy the CRDs
+- deployment-crds.yaml
+
+# Deploy the operator
+- deployment.yaml
+
+# Deploy default Custom Resources from 'crs' directory
+# INFO: This depends on the CRDs deployment. Will happen after it
+- deployment-crs.yaml

+ 4 - 0
docs/snippets/gitops/namespace.yaml

@@ -0,0 +1,4 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: external-secrets

+ 20 - 0
docs/snippets/gitops/repositories.yaml

@@ -0,0 +1,20 @@
+# Reference to Helm repository
+apiVersion: source.toolkit.fluxcd.io/v1beta1
+kind: HelmRepository
+metadata:
+  name: external-secrets
+  namespace: flux-system
+spec:
+  interval: 10m
+  url: https://charts.external-secrets.io
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta1
+kind: GitRepository
+metadata:
+  name: external-secrets
+  namespace: flux-system
+spec:
+  interval: 10m
+  ref:
+    branch: main
+  url: http://github.com/external-secrets/external-secrets

+ 8 - 0
docs/snippets/gitops/secret-token.yaml

@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: vault-token-global
+  namespace: external-secrets
+stringData:
+  # This token must be patched by overlays. Not here for security reasons
+  token: change-me-placeholder

+ 1 - 1
docs/snippets/template-from-secret.yaml

@@ -1,5 +1,5 @@
 {% raw %}
-# define your tempalte in a config map
+# define your template in a config map
 apiVersion: v1
 kind: ConfigMap
 metadata:

+ 2 - 0
docs/snippets/vault-jwt-store.yaml

@@ -13,6 +13,8 @@ spec:
         # VaultJwt authenticates with Vault using the JWT/OIDC auth mechanism
         # https://www.vaultproject.io/docs/auth/jwt
         jwt:
+          # Path where the JWT authentication backend is mounted
+          path: "jwt"
           # JWT role configured in a Vault server, optional.
           role: "vault-jwt-role"
           secretRef:

+ 2 - 0
docs/snippets/vault-ldap-store.yaml

@@ -13,6 +13,8 @@ spec:
         # VaultLdap authenticates with Vault using the LDAP auth mechanism
         # https://www.vaultproject.io/docs/auth/ldap
         ldap:
+          # Path where the LDAP authentication backend is mounted
+          path: "ldap"
           # LDAP username
           username: "username"
           secretRef:

+ 686 - 8
docs/spec.md

@@ -225,6 +225,282 @@ see: <a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.
 </td>
 </tr></tbody>
 </table>
+<h3 id="external-secrets.io/v1alpha1.AkeylessAuth">AkeylessAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.AkeylessProvider">AkeylessProvider</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/v1alpha1.AkeylessAuthSecretRef">
+AkeylessAuthSecretRef
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.AkeylessAuthSecretRef">AkeylessAuthSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.AkeylessAuth">AkeylessAuth</a>)
+</p>
+<p>
+<p>AkeylessAuthSecretRef
+AKEYLESS_ACCESS_TYPE_PARAM: AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY OR KUB_CONFIG_NAME.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>accessID</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<p>The SecretAccessID is used for authentication</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>accessType</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>accessTypeParam</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.AkeylessProvider">AkeylessProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>AkeylessProvider Configures an store to sync secrets using Akeyless KV.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>akeylessGWApiURL</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Akeyless GW API Url from which the secrets to be fetched from.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>authSecretRef</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.AkeylessAuth">
+AkeylessAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth configures how the operator authenticates with Akeyless.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.AlibabaAuth">AlibabaAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.AlibabaProvider">AlibabaProvider</a>)
+</p>
+<p>
+<p>AlibabaAuth contains a secretRef for credentials.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>secretRef</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.AlibabaAuthSecretRef">
+AlibabaAuthSecretRef
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.AlibabaAuthSecretRef">AlibabaAuthSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.AlibabaAuth">AlibabaAuth</a>)
+</p>
+<p>
+<p>AlibabaAuthSecretRef holds secret references for Alibaba credentials.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>accessKeyIDSecretRef</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<p>The AccessKeyID is used for authentication</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>accessKeySecretSecretRef</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<p>The AccessKeySecret is used for authentication</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.AlibabaProvider">AlibabaProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>AlibabaProvider configures a store to sync secrets using the Alibaba Secret Manager provider.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.AlibabaAuth">
+AlibabaAuth
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>endpoint</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
+<tr>
+<td>
+<code>regionID</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Alibaba Region to be used for the provider</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.AuthType">AuthType
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.AzureKVProvider">AzureKVProvider</a>)
+</p>
+<p>
+<p>AuthType describes how to authenticate to the Azure Keyvault
+Only one of the following auth types may be specified.
+If none of the following auth type is specified, the default one
+is ServicePrincipal.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;ManagedIdentity&#34;</p></td>
+<td><p>Using Managed Identity to authenticate. Used with aad-pod-identity instelled in the clister.</p>
+</td>
+</tr><tr><td><p>&#34;ServicePrincipal&#34;</p></td>
+<td><p>Using service principal to authenticate, which needs a tenantId, a clientId and a clientSecret.</p>
+</td>
+</tr></tbody>
+</table>
 <h3 id="external-secrets.io/v1alpha1.AzureKVAuth">AzureKVAuth
 </h3>
 <p>
@@ -285,41 +561,158 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
 <tbody>
 <tr>
 <td>
+<code>authType</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.AuthType">
+AuthType
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Auth type defines how to authenticate to the keyvault service.
+Valid values are:
+- &ldquo;ServicePrincipal&rdquo; (default): Using a service principal (tenantId, clientId, clientSecret)
+- &ldquo;ManagedIdentity&rdquo;: Using Managed Identity assigned to the pod (see aad-pod-identity)</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>vaultUrl</code></br>
 <em>
 string
 </em>
 </td>
 <td>
-<p>Vault Url from which the secrets to be fetched from.</p>
+<p>Vault Url from which the secrets to be fetched from.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>tenantId</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>TenantID configures the Azure Tenant to send requests to. Required for ServicePrincipal auth type.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>authSecretRef</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.AzureKVAuth">
+AzureKVAuth
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Auth configures how the operator authenticates with Azure. Required for ServicePrincipal auth type.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>identityId</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>If multiple Managed Identity is assigned to the pod, you can select the one to be used</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.CAProvider">CAProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.VaultProvider">VaultProvider</a>)
+</p>
+<p>
+<p>Defines a location to fetch the cert for the vault provider from.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>type</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.CAProviderType">
+CAProviderType
+</a>
+</em>
+</td>
+<td>
+<p>The type of provider to use such as &ldquo;Secret&rdquo;, or &ldquo;ConfigMap&rdquo;.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>name</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>The name of the object located at the provider type.</p>
 </td>
 </tr>
 <tr>
 <td>
-<code>tenantId</code></br>
+<code>key</code></br>
 <em>
 string
 </em>
 </td>
 <td>
-<p>TenantID configures the Azure Tenant to send requests to.</p>
+<p>The key the value inside of the provider type to use, only used with &ldquo;Secret&rdquo; type</p>
 </td>
 </tr>
 <tr>
 <td>
-<code>authSecretRef</code></br>
+<code>namespace</code></br>
 <em>
-<a href="#external-secrets.io/v1alpha1.AzureKVAuth">
-AzureKVAuth
-</a>
+string
 </em>
 </td>
 <td>
-<p>Auth configures how the operator authenticates with Azure.</p>
+<p>The namespace the Provider type is in.</p>
 </td>
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1alpha1.CAProviderType">CAProviderType
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.CAProvider">CAProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;ConfigMap&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;Secret&#34;</p></td>
+<td></td>
+</tr></tbody>
+</table>
 <h3 id="external-secrets.io/v1alpha1.ClusterSecretStore">ClusterSecretStore
 </h3>
 <p>
@@ -386,6 +779,20 @@ SecretStoreProvider
 <p>Used to configure the provider. Only one provider may be set</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>retrySettings</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreRetrySettings">
+SecretStoreRetrySettings
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to configure http retries if failed</p>
+</td>
+</tr>
 </table>
 </td>
 </tr>
@@ -953,6 +1360,18 @@ ExternalSecretTemplate
 <p>Template defines a blueprint for the created Secret resource.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>immutable</code></br>
+<em>
+bool
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Immutable defines if the final secret will be immutable</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1alpha1.ExternalSecretTemplate">ExternalSecretTemplate
@@ -1393,6 +1812,143 @@ string
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1alpha1.OracleAuth">OracleAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.OracleProvider">OracleProvider</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/v1alpha1.OracleSecretRef">
+OracleSecretRef
+</a>
+</em>
+</td>
+<td>
+<p>SecretRef to pass through sensitive information.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.OracleProvider">OracleProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>Configures an store to sync secrets using a Oracle Vault
+backend.</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/v1alpha1.OracleAuth">
+OracleAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth configures how secret-manager authenticates with the Oracle Vault.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>user</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>User is an access OCID specific to the account.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>tenancy</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>projectID is an access token specific to the secret.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>region</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>projectID is an access token specific to the secret.</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1alpha1.OracleSecretRef">OracleSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.OracleAuth">OracleAuth</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>privatekey</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<p>The Access Token is used for authentication</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>fingerprint</code></br>
+<em>
+github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
+</em>
+</td>
+<td>
+<p>projectID is an access token specific to the secret.</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1alpha1.SecretStore">SecretStore
 </h3>
 <p>
@@ -1459,6 +2015,20 @@ SecretStoreProvider
 <p>Used to configure the provider. Only one provider may be set</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>retrySettings</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreRetrySettings">
+SecretStoreRetrySettings
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to configure http retries if failed</p>
+</td>
+</tr>
 </table>
 </td>
 </tr>
@@ -1542,6 +2112,20 @@ AzureKVProvider
 </tr>
 <tr>
 <td>
+<code>akeyless</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.AkeylessProvider">
+AkeylessProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Akeyless configures this store to sync secrets using Akeyless Vault provider</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>vault</code></br>
 <em>
 <a href="#external-secrets.io/v1alpha1.VaultProvider">
@@ -1570,6 +2154,20 @@ GCPSMProvider
 </tr>
 <tr>
 <td>
+<code>oracle</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.OracleProvider">
+OracleProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Oracle configures this store to sync secrets using Oracle Vault provider</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>ibm</code></br>
 <em>
 <a href="#external-secrets.io/v1alpha1.IBMProvider">
@@ -1610,6 +2208,20 @@ GitlabProvider
 <p>GItlab configures this store to sync secrets using Gitlab Variables provider</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>alibaba</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.AlibabaProvider">
+AlibabaProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Alibaba configures this store to sync secrets using Alibaba Cloud provider</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1alpha1.SecretStoreRef">SecretStoreRef
@@ -1655,6 +2267,44 @@ Defaults to <code>SecretStore</code></p>
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1alpha1.SecretStoreRetrySettings">SecretStoreRetrySettings
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreSpec">SecretStoreSpec</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>maxRetries</code></br>
+<em>
+int32
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>retryInterval</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1alpha1.SecretStoreSpec">SecretStoreSpec
 </h3>
 <p>
@@ -1699,6 +2349,20 @@ SecretStoreProvider
 <p>Used to configure the provider. Only one provider may be set</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>retrySettings</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.SecretStoreRetrySettings">
+SecretStoreRetrySettings
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Used to configure http retries if failed</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1alpha1.SecretStoreStatus">SecretStoreStatus
@@ -2416,6 +3080,20 @@ plain HTTP protocol connection. If not set the system root certificates
 are used to validate the TLS connection.</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>caProvider</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.CAProvider">
+CAProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>The provider for the CA bundle to use to validate Vault server certificate.</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1alpha1.YandexLockboxAuth">YandexLockboxAuth

BIN
e2e/.DS_Store


+ 4 - 4
e2e/Dockerfile

@@ -1,8 +1,8 @@
-ARG GO_VERSION=1.16
+ARG GO_VERSION=1.17
 FROM golang:$GO_VERSION-buster as builder
 
-ENV KUBECTL_VERSION="v1.19.2"
-ENV HELM_VERSION="v3.3.4"
+ENV KUBECTL_VERSION="v1.21.2"
+ENV HELM_VERSION="v3.7.1"
 
 RUN go get -u github.com/onsi/ginkgo/ginkgo
 RUN wget -q https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl -O /usr/local/bin/kubectl && \
@@ -10,7 +10,7 @@ RUN wget -q https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_
     wget -q https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz -O - | tar -xzO linux-amd64/helm > /usr/local/bin/helm && \
     chmod +x /usr/local/bin/helm
 
-FROM alpine:3.12
+FROM alpine:3.15.0
 RUN apk add -U --no-cache \
     ca-certificates \
     bash \

+ 4 - 3
e2e/Makefile

@@ -4,7 +4,7 @@ SHELL       := /bin/bash
 
 IMG_TAG     = test
 IMG         = local/external-secrets-e2e:$(IMG_TAG)
-KIND_IMG    = "kindest/node:v1.20.7@sha256:cbeaf907fc78ac97ce7b625e4bf0de16e3ea725daf6b04f930bd14c67c671ff9"
+KIND_IMG    = "kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6"
 BUILD_ARGS  ?=
 export FOCUS := $(FOCUS)
 
@@ -19,13 +19,14 @@ test: e2e-image ## Run e2e tests against current kube context
 	$(MAKE) -C ../ docker.build \
 		IMAGE_REGISTRY=local/external-secrets \
 		VERSION=$(IMG_TAG) \
-		ARCH=amd64
+		ARCH=amd64 \
+		BUILD_ARGS="${BUILD_ARGS} --build-arg TARGETARCH=amd64 --build-arg TARGETOS=linux"
 	kind load docker-image --name="external-secrets" local/external-secrets:$(IMG_TAG)
 	kind load docker-image --name="external-secrets" $(IMG)
 	./run.sh
 
 e2e-bin:
-	CGO_ENABLED=0 ginkgo build .
+	CGO_ENABLED=0 go run github.com/onsi/ginkgo/ginkgo build .
 
 e2e-image: e2e-bin
 	-rm -rf ./k8s/deploy

+ 5 - 5
e2e/entrypoint.sh

@@ -14,7 +14,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-set -e
+set -euo pipefail
 
 NC='\e[0m'
 BGREEN='\e[32m'
@@ -46,8 +46,8 @@ ginkgo_args=(
 kubectl apply -f /k8s/deploy/crds
 
 echo -e "${BGREEN}Running e2e test suite (FOCUS=${FOCUS})...${NC}"
-ginkgo "${ginkgo_args[@]}"               \
-  -focus="${FOCUS}"                      \
-  -skip="\[Serial\]|\[MemoryLeak\]"      \
-  -nodes="${E2E_NODES}"                  \
+ACK_GINKGO_RC=true ginkgo "${ginkgo_args[@]}" \
+  -focus="${FOCUS}"                           \
+  -skip="\[Serial\]|\[MemoryLeak\]"           \
+  -nodes="${E2E_NODES}"                       \
   /e2e.test

+ 3 - 1
e2e/framework/addon/vault.go

@@ -28,7 +28,7 @@ import (
 	"os"
 	"time"
 
-	"github.com/golang-jwt/jwt"
+	"github.com/golang-jwt/jwt/v4"
 	vault "github.com/hashicorp/vault/api"
 
 	// nolint
@@ -57,6 +57,7 @@ type Vault struct {
 	JWTPrivKey         []byte
 	JWTToken           string
 	JWTRole            string
+	JWTPath            string
 	KubernetesAuthPath string
 	KubernetesAuthRole string
 
@@ -160,6 +161,7 @@ func (l *Vault) initVault() error {
 	l.JWTPrivKey = jwtPrivkey
 	l.JWTPubkey = jwtPubkey
 	l.JWTToken = jwtToken
+	l.JWTPath = "myjwt"                                // see configure-vault.sh
 	l.JWTRole = "external-secrets-operator"            // see configure-vault.sh
 	l.KubernetesAuthPath = "mykubernetes"              // see configure-vault.sh
 	l.KubernetesAuthRole = "external-secrets-operator" // see configure-vault.sh

+ 4 - 4
e2e/k8s/vault-config/configure-vault.sh

@@ -51,17 +51,17 @@ vault write auth/myapprole/role/eso-e2e-role \
     secret_id_num_uses=40
 
 # ------------------
-#   App Role AUTH
+#   JWT AUTH
 #   https://www.vaultproject.io/docs/auth/jwt
 # ------------------
-vault auth enable jwt
+vault auth enable -path=myjwt jwt
 
-vault write auth/jwt/config \
+vault write auth/myjwt/config \
    jwt_validation_pubkeys=@/etc/vault-config/jwt-pubkey.pem \
    bound_issuer="example.iss" \
    default_role="external-secrets-operator"
 
-vault write auth/jwt/role/external-secrets-operator \
+vault write auth/myjwt/role/external-secrets-operator \
     role_type="jwt" \
     bound_subject="vault@example" \
     bound_audiences="vault.client" \

+ 4 - 3
e2e/run.sh

@@ -13,9 +13,7 @@
 # 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.
-set -o errexit
-set -o nounset
-set -o pipefail
+set -euo pipefail
 
 if ! command -v kind --version &> /dev/null; then
   echo "kind is not installed. Use the package manager or visit the official site https://kind.sigs.k8s.io/"
@@ -56,6 +54,9 @@ kubectl run --rm \
   --env="GCP_PROJECT_ID=${GCP_PROJECT_ID:-}" \
   --env="AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}" \
   --env="AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET:-}" \
+  --env="AKEYLESS_ACCESS_ID=${AKEYLESS_ACCESS_ID:-}" \
+  --env="AKEYLESS_ACCESS_TYPE=${AKEYLESS_ACCESS_TYPE:-}" \
+  --env="AKEYLESS_ACCESS_TYPE_PARAM=${AKEYLESS_ACCESS_TYPE_PARAM:-}" \
   --env="TENANT_ID=${TENANT_ID:-}" \
   --env="VAULT_URL=${VAULT_URL:-}" \
   --env="GITLAB_TOKEN=${GITLAB_TOKEN:-}" \

+ 49 - 0
e2e/suite/akeyless/akeyless.go

@@ -0,0 +1,49 @@
+/*
+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 akeyless
+
+import (
+	"os"
+
+	// nolint
+	. "github.com/onsi/ginkgo"
+	// nolint
+	. "github.com/onsi/ginkgo/extensions/table"
+
+	"github.com/external-secrets/external-secrets/e2e/framework"
+	"github.com/external-secrets/external-secrets/e2e/suite/common"
+)
+
+var _ = Describe("[akeyless] ", func() {
+	f := framework.New("eso-akeyless")
+	accessID := os.Getenv("AKEYLESS_ACCESS_ID")
+	accessType := os.Getenv("AKEYLESS_ACCESS_TYPE")
+	accessTypeParam := os.Getenv("AKEYLESS_ACCESS_TYPE_PARAM")
+	prov := newAkeylessProvider(f, accessID, accessType, accessTypeParam)
+
+	DescribeTable("sync secrets", framework.TableFunc(f, prov),
+		Entry(common.SimpleDataSync(f)),
+		Entry(common.NestedJSONWithGJSON(f)),
+		Entry(common.JSONDataFromSync(f)),
+		Entry(common.JSONDataWithProperty(f)),
+		Entry(common.JSONDataWithTemplate(f)),
+		Entry(common.DockerJSONConfig(f)),
+		Entry(common.DataPropertyDockerconfigJSON(f)),
+		Entry(common.SSHKeySync(f)),
+		Entry(common.SSHKeySyncDataProperty(f)),
+		Entry(common.SyncWithoutTargetName(f)),
+		Entry(common.JSONDataWithoutTargetName(f)),
+	)
+})

+ 227 - 0
e2e/suite/akeyless/provider.go

@@ -0,0 +1,227 @@
+/*
+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 akeyless
+
+import (
+	"context"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+
+	aws_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws"
+	azure_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/azure"
+	gcp_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/gcp"
+	"github.com/akeylesslabs/akeyless-go/v2"
+
+	//nolint
+	. "github.com/onsi/ginkgo"
+
+	//nolint
+	. "github.com/onsi/gomega"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/e2e/framework"
+)
+
+type akeylessProvider struct {
+	accessID        string
+	accessType      string
+	accessTypeParam string
+	framework       *framework.Framework
+	restAPIClient   *akeyless.V2ApiService
+}
+
+var apiErr akeyless.GenericOpenAPIError
+
+const DefServiceAccountFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
+
+func newAkeylessProvider(f *framework.Framework, accessID, accessType, accessTypeParam string) *akeylessProvider {
+	prov := &akeylessProvider{
+		accessID:        accessID,
+		accessType:      accessType,
+		accessTypeParam: accessTypeParam,
+		framework:       f,
+	}
+
+	restAPIClient := akeyless.NewAPIClient(&akeyless.Configuration{
+		Servers: []akeyless.ServerConfiguration{
+			{
+				URL: "https://api.akeyless.io",
+			},
+		},
+	}).V2Api
+
+	prov.restAPIClient = restAPIClient
+
+	BeforeEach(prov.BeforeEach)
+	return prov
+}
+
+// CreateSecret creates a secret.
+func (a *akeylessProvider) CreateSecret(key, val string) {
+	token, err := a.GetToken()
+	Expect(err).ToNot(HaveOccurred())
+
+	ctx := context.Background()
+	gsvBody := akeyless.CreateSecret{
+		Name:  key,
+		Value: val,
+		Token: &token,
+	}
+
+	_, _, err = a.restAPIClient.CreateSecret(ctx).Body(gsvBody).Execute()
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (a *akeylessProvider) DeleteSecret(key string) {
+	token, err := a.GetToken()
+	Expect(err).ToNot(HaveOccurred())
+
+	ctx := context.Background()
+	gsvBody := akeyless.DeleteItem{
+		Name:  key,
+		Token: &token,
+	}
+
+	_, _, err = a.restAPIClient.DeleteItem(ctx).Body(gsvBody).Execute()
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (a *akeylessProvider) BeforeEach() {
+	// Creating an Akeyless secret
+	akeylessCreds := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "provider-secret",
+			Namespace: a.framework.Namespace.Name,
+		},
+		StringData: map[string]string{
+			"access-id":         a.accessID,
+			"access-type":       a.accessType,
+			"access-type-param": a.accessTypeParam,
+		},
+	}
+	err := a.framework.CRClient.Create(context.Background(), akeylessCreds)
+	Expect(err).ToNot(HaveOccurred())
+
+	// Creating Akeyless secret store
+	secretStore := &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      a.framework.Namespace.Name,
+			Namespace: a.framework.Namespace.Name,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				Akeyless: &esv1alpha1.AkeylessProvider{
+					Auth: &esv1alpha1.AkeylessAuth{
+						SecretRef: esv1alpha1.AkeylessAuthSecretRef{
+							AccessID: esmeta.SecretKeySelector{
+								Name: "access-id-secret",
+								Key:  "access-id",
+							},
+							AccessType: esmeta.SecretKeySelector{
+								Name: "access-type-secret",
+								Key:  "access-type",
+							},
+							AccessTypeParam: esmeta.SecretKeySelector{
+								Name: "access-type-param-secert",
+								Key:  "access-type-param",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+	err = a.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}
+
+func (a *akeylessProvider) GetToken() (string, error) {
+	ctx := context.Background()
+	authBody := akeyless.NewAuthWithDefaults()
+	authBody.AccessId = akeyless.PtrString(a.accessID)
+
+	if a.accessType == "api_key" {
+		authBody.AccessKey = akeyless.PtrString(a.accessTypeParam)
+	} else if a.accessType == "k8s" {
+		jwtString, err := readK8SServiceAccountJWT()
+		if err != nil {
+			return "", fmt.Errorf("failed to read JWT with Kubernetes Auth from %v. error: %w", DefServiceAccountFile, err)
+		}
+		K8SAuthConfigName := a.accessTypeParam
+		authBody.AccessType = akeyless.PtrString(a.accessType)
+		authBody.K8sServiceAccountToken = akeyless.PtrString(jwtString)
+		authBody.K8sAuthConfigName = akeyless.PtrString(K8SAuthConfigName)
+	} else {
+		cloudID, err := a.getCloudID(a.accessType, a.accessTypeParam)
+		if err != nil {
+			return "", fmt.Errorf("Require Cloud ID " + err.Error())
+		}
+		authBody.AccessType = akeyless.PtrString(a.accessType)
+		authBody.CloudId = akeyless.PtrString(cloudID)
+	}
+
+	authOut, _, err := a.restAPIClient.Auth(ctx).Body(*authBody).Execute()
+	if err != nil {
+		if errors.As(err, &apiErr) {
+			return "", fmt.Errorf("authentication failed: %v", string(apiErr.Body()))
+		}
+		return "", fmt.Errorf("authentication failed: %w", err)
+	}
+
+	token := authOut.GetToken()
+	return token, nil
+}
+
+func (a *akeylessProvider) getCloudID(provider, accTypeParam string) (string, error) {
+	var cloudID string
+	var err error
+
+	switch provider {
+	case "azure_ad":
+		cloudID, err = azure_cloud_id.GetCloudId(accTypeParam)
+	case "aws_iam":
+		cloudID, err = aws_cloud_id.GetCloudId()
+	case "gcp":
+		cloudID, err = gcp_cloud_id.GetCloudID(accTypeParam)
+	default:
+		return "", fmt.Errorf("unable to determine provider: %s", provider)
+	}
+	return cloudID, err
+}
+
+// readK8SServiceAccountJWT reads the JWT data for the Agent to submit to Akeyless Gateway.
+func readK8SServiceAccountJWT() (string, error) {
+	data, err := os.Open(DefServiceAccountFile)
+	if err != nil {
+		return "", err
+	}
+	defer data.Close()
+
+	contentBytes, err := ioutil.ReadAll(data)
+	if err != nil {
+		return "", err
+	}
+
+	a := strings.TrimSpace(string(contentBytes))
+
+	return base64.StdEncoding.EncodeToString([]byte(a)), nil
+}

+ 1 - 1
e2e/suite/gcp/provider.go

@@ -123,7 +123,7 @@ func (s *gcpProvider) BeforeEach() {
 				GCPSM: &esv1alpha1.GCPSMProvider{
 					ProjectID: s.projectID,
 					Auth: esv1alpha1.GCPSMAuth{
-						SecretRef: esv1alpha1.GCPSMAuthSecretRef{
+						SecretRef: &esv1alpha1.GCPSMAuthSecretRef{
 							SecretAccessKey: esmeta.SecretKeySelector{
 								Name: "provider-secret",
 								Key:  "secret-access-credentials",

+ 1 - 0
e2e/suite/vault/provider.go

@@ -242,6 +242,7 @@ func (s vaultProvider) CreateJWTStore(v *addon.Vault, ns string) {
 	secretStore := makeStore(jwtProviderName, ns, v)
 	secretStore.Spec.Provider.Vault.Auth = esv1alpha1.VaultAuth{
 		Jwt: &esv1alpha1.VaultJwtAuth{
+			Path: v.JWTPath,
 			Role: v.JWTRole,
 			SecretRef: esmeta.SecretKeySelector{
 				Name: "jwt-provider",

+ 179 - 59
go.mod

@@ -1,89 +1,209 @@
 module github.com/external-secrets/external-secrets
 
-go 1.16
+go 1.17
 
 replace (
 	github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1 => ./apis/externalsecrets/v1alpha1
+	github.com/external-secrets/external-secrets/e2e/framework/log => ./e2e/framework/log
 	github.com/external-secrets/external-secrets/pkg/provider/gitlab => ./pkg/provider/gitlab
-	google.golang.org/grpc => google.golang.org/grpc v1.27.0
-	k8s.io/api => k8s.io/api v0.21.2
-	k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.21.2
-	k8s.io/apimachinery => k8s.io/apimachinery v0.21.2
-	k8s.io/apiserver => k8s.io/apiserver v0.21.2
-	k8s.io/cli-runtime => k8s.io/cli-runtime v0.21.2
-	k8s.io/client-go => k8s.io/client-go v0.21.2
-	k8s.io/cloud-provider => k8s.io/cloud-provider v0.21.2
-	k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.21.2
-	k8s.io/code-generator => k8s.io/code-generator v0.21.2
-	k8s.io/component-base => k8s.io/component-base v0.21.2
-	k8s.io/component-helpers => k8s.io/component-helpers v0.21.2
-	k8s.io/controller-manager => k8s.io/controller-manager v0.21.2
-	k8s.io/cri-api => k8s.io/cri-api v0.21.2
-	k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.21.2
-	k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.21.2
-	k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.21.2
-	k8s.io/kube-proxy => k8s.io/kube-proxy v0.21.2
-	k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.21.2
-	k8s.io/kubectl => k8s.io/kubectl v0.21.2
-	k8s.io/kubelet => k8s.io/kubelet v0.21.2
-	k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.21.2
-	k8s.io/metrics => k8s.io/metrics v0.21.2
-	k8s.io/mount-utils => k8s.io/mount-utils v0.21.2
-	k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.21.2
+	k8s.io/api => k8s.io/api v0.23.0
+	k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.23.0
+	k8s.io/apimachinery => k8s.io/apimachinery v0.23.0
+	k8s.io/apiserver => k8s.io/apiserver v0.23.0
+	k8s.io/cli-runtime => k8s.io/cli-runtime v0.23.0
+	k8s.io/client-go => k8s.io/client-go v0.23.0
+	k8s.io/cloud-provider => k8s.io/cloud-provider v0.23.0
+	k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.23.0
+	k8s.io/code-generator => k8s.io/code-generator v0.23.0
+	k8s.io/component-base => k8s.io/component-base v0.23.0
+	k8s.io/component-helpers => k8s.io/component-helpers v0.23.0
+	k8s.io/controller-manager => k8s.io/controller-manager v0.23.0
+	k8s.io/cri-api => k8s.io/cri-api v0.23.0
+	k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.23.0
+	k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.23.0
+	k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.23.0
+	k8s.io/kube-proxy => k8s.io/kube-proxy v0.23.0
+	k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.23.0
+	k8s.io/kubectl => k8s.io/kubectl v0.23.0
+	k8s.io/kubelet => k8s.io/kubelet v0.23.0
+	k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.23.0
+	k8s.io/metrics => k8s.io/metrics v0.23.0
+	k8s.io/mount-utils => k8s.io/mount-utils v0.23.0
+	k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.23.0
 )
 
 require (
-	cloud.google.com/go v0.65.0
-	github.com/Azure/azure-sdk-for-go v54.1.0+incompatible
+	cloud.google.com/go v0.99.0
+	cloud.google.com/go/secretmanager v1.0.0
+	github.com/Azure/azure-sdk-for-go v61.1.0+incompatible
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.7
-	github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
-	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
 	github.com/IBM/go-sdk-core/v5 v5.5.0
 	github.com/IBM/secrets-manager-go-sdk v1.0.23
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver v1.5.0 // indirect
+	github.com/Masterminds/sprig v2.22.0+incompatible
+	github.com/PaesslerAG/jsonpath v0.1.1
+	github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2
+	github.com/akeylesslabs/akeyless-go/v2 v2.5.11
 	github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192
 	github.com/aws/aws-sdk-go v1.38.6
-	github.com/crossplane/crossplane-runtime v0.13.0
-	github.com/fatih/color v1.10.0 // indirect
-	github.com/frankban/quicktest v1.10.0 // indirect
-	github.com/go-logr/logr v0.4.0
-	github.com/golang-jwt/jwt v3.2.2+incompatible
-	github.com/google/go-cmp v0.5.5
-	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/crossplane/crossplane-runtime v0.15.1
+	github.com/go-logr/logr v1.2.0
+	github.com/golang-jwt/jwt/v4 v4.2.0
+	github.com/google/go-cmp v0.5.6
 	github.com/google/uuid v1.2.0
 	github.com/googleapis/gax-go v1.0.3
-	github.com/hashicorp/go-hclog v0.14.1 // indirect
-	github.com/hashicorp/go-multierror v1.1.1 // indirect
-	github.com/hashicorp/hcl v1.0.1-vault // indirect
 	github.com/hashicorp/vault/api v1.0.5-0.20210224012239-b540be4b7ec4
+	github.com/huandu/xstrings v1.3.2 // indirect
 	github.com/kr/pretty v0.2.1 // indirect
 	github.com/lestrrat-go/jwx v1.2.1
-	github.com/onsi/ginkgo v1.16.4
-	github.com/onsi/gomega v1.16.0
+	github.com/onsi/ginkgo v1.16.5
+	github.com/onsi/gomega v1.17.0
 	github.com/oracle/oci-go-sdk/v45 v45.2.0
-	github.com/pierrec/lz4 v2.5.2+incompatible // indirect
 	github.com/prometheus/client_golang v1.11.0
 	github.com/prometheus/client_model v0.2.0
-	github.com/spf13/cobra v1.1.3 // indirect
 	github.com/stretchr/testify v1.7.0
-	github.com/tidwall/gjson v1.7.5
+	github.com/tidwall/gjson v1.12.1
 	github.com/xanzy/go-gitlab v0.50.1
 	github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588
 	github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa
 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
-	go.uber.org/zap v1.17.0
-	golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
-	golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
-	golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c
-	golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 // indirect
-	google.golang.org/api v0.30.0
-	google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a
-	google.golang.org/grpc v1.31.0
-	honnef.co/go/tools v0.1.4 // indirect
-	k8s.io/api v0.21.3
-	k8s.io/apimachinery v0.21.3
-	k8s.io/client-go v0.21.2
-	k8s.io/utils v0.0.0-20210527160623-6fdb442a123b
-	sigs.k8s.io/controller-runtime v0.9.3
+	go.uber.org/zap v1.19.1
+	golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
+	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
+	google.golang.org/api v0.61.0
+	google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0
+	google.golang.org/grpc v1.43.0
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
+	grpc.go4.org v0.0.0-20170609214715-11d0a25b4919
+	k8s.io/api v0.23.0
+	k8s.io/apimachinery v0.23.0
+	k8s.io/client-go v0.23.0
+	k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b
+	sigs.k8s.io/controller-runtime v0.11.0
 	sigs.k8s.io/controller-tools v0.5.0
 	software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
 )
+
+require (
+	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
+	github.com/Azure/go-autorest/autorest v0.11.18 // indirect
+	github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect
+	github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect
+	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
+	github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
+	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
+	github.com/Azure/go-autorest/logger v0.2.1 // indirect
+	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
+	github.com/BurntSushi/toml v0.3.1 // indirect
+	github.com/PaesslerAG/gval v1.0.0 // indirect
+	github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
+	github.com/aws/aws-sdk-go-v2 v0.23.0 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/census-instrumentation/opencensus-proto v0.2.1 // indirect
+	github.com/cespare/xxhash/v2 v2.1.1 // indirect
+	github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
+	github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 // indirect
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
+	github.com/dimchansky/utfbom v1.1.1 // indirect
+	github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021 // indirect
+	github.com/envoyproxy/protoc-gen-validate v0.1.0 // indirect
+	github.com/evanphx/json-patch v4.12.0+incompatible // indirect
+	github.com/fatih/color v1.10.0 // indirect
+	github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
+	github.com/frankban/quicktest v1.10.0 // indirect
+	github.com/fsnotify/fsnotify v1.5.1 // indirect
+	github.com/ghodss/yaml v1.0.0 // indirect
+	github.com/go-logr/zapr v1.2.0 // indirect
+	github.com/go-openapi/errors v0.19.8 // indirect
+	github.com/go-openapi/strfmt v0.20.1 // indirect
+	github.com/go-playground/locales v0.13.0 // indirect
+	github.com/go-playground/universal-translator v0.17.0 // indirect
+	github.com/go-stack/stack v1.8.0 // indirect
+	github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
+	github.com/gobuffalo/flect v0.2.2 // indirect
+	github.com/goccy/go-json v0.4.8 // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/golang/snappy v0.0.3 // indirect
+	github.com/google/go-querystring v1.0.0 // indirect
+	github.com/google/gofuzz v1.2.0 // indirect
+	github.com/googleapis/gax-go/v2 v2.1.1 // indirect
+	github.com/googleapis/gnostic v0.5.5 // indirect
+	github.com/hashicorp/errwrap v1.0.0 // indirect
+	github.com/hashicorp/go-cleanhttp v0.5.1 // indirect
+	github.com/hashicorp/go-hclog v0.14.1 // indirect
+	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/hashicorp/go-retryablehttp v0.6.8 // indirect
+	github.com/hashicorp/go-rootcerts v1.0.2 // indirect
+	github.com/hashicorp/go-sockaddr v1.0.2 // indirect
+	github.com/hashicorp/hcl v1.0.1-vault // indirect
+	github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267 // indirect
+	github.com/imdario/mergo v0.3.12 // indirect
+	github.com/inconshreveable/mousetrap v1.0.0 // indirect
+	github.com/jmespath/go-jmespath v0.4.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/leodido/go-urn v1.2.0 // indirect
+	github.com/lestrrat-go/backoff/v2 v2.0.7 // indirect
+	github.com/lestrrat-go/blackmagic v1.0.0 // indirect
+	github.com/lestrrat-go/httpcc v1.0.0 // indirect
+	github.com/lestrrat-go/iter v1.0.1 // indirect
+	github.com/lestrrat-go/option v1.0.0 // indirect
+	github.com/mattn/go-colorable v0.1.8 // indirect
+	github.com/mattn/go-isatty v0.0.12 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
+	github.com/mitchellh/copystructure v1.0.0 // indirect
+	github.com/mitchellh/go-homedir v1.1.0 // indirect
+	github.com/mitchellh/mapstructure v1.4.1 // indirect
+	github.com/mitchellh/reflectwalk v1.0.0 // indirect
+	github.com/moby/spdystream v0.2.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/nxadm/tail v1.4.8 // indirect
+	github.com/oklog/ulid v1.3.1 // indirect
+	github.com/pierrec/lz4 v2.5.2+incompatible // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/prometheus/common v0.28.0 // indirect
+	github.com/prometheus/procfs v0.6.0 // indirect
+	github.com/ryanuber/go-glob v1.0.0 // indirect
+	github.com/spf13/cobra v1.2.1 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/stretchr/objx v0.2.0 // indirect
+	github.com/tidwall/match v1.1.1 // indirect
+	github.com/tidwall/pretty v1.2.0 // indirect
+	go.mongodb.org/mongo-driver v1.5.1 // indirect
+	go.opencensus.io v0.23.0 // indirect
+	go.uber.org/atomic v1.7.0 // indirect
+	go.uber.org/multierr v1.6.0 // indirect
+	golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect
+	golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
+	golang.org/x/mod v0.4.2 // indirect
+	golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect
+	golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect
+	golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
+	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
+	golang.org/x/tools v0.1.7 // indirect
+	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
+	google.golang.org/appengine v1.6.7 // indirect
+	google.golang.org/protobuf v1.27.1 // indirect
+	gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
+	gopkg.in/inf.v0 v0.9.1 // indirect
+	gopkg.in/ini.v1 v1.62.0 // indirect
+	gopkg.in/square/go-jose.v2 v2.5.1 // indirect
+	gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	honnef.co/go/tools v0.1.4 // indirect
+	k8s.io/apiextensions-apiserver v0.23.0 // indirect
+	k8s.io/component-base v0.23.0 // indirect
+	k8s.io/klog/v2 v2.30.0 // indirect
+	k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
+	sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect
+	sigs.k8s.io/yaml v1.3.0 // indirect
+)

Diferenças do arquivo suprimidas por serem muito extensas
+ 364 - 112
go.sum


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

@@ -35,6 +35,8 @@ nav:
     - Common K8S Secret Types: guides-common-k8s-secret-types.md
     - Multi Tenancy: guides-multi-tenancy.md
     - Metrics: guides-metrics.md
+    - Using Latest Image: guides-using-latest-image.md
+    - GitOps using FluxCD: guides-gitops-using-fluxcd.md
   - Provider:
     - AWS:
       - Secrets Manager: provider-aws-secrets-manager.md
@@ -45,6 +47,7 @@ nav:
       - Secrets Manager: provider-google-secrets-manager.md
     - IBM:
       - Secrets Manager: provider-ibm-secrets-manager.md
+    - Akeyless: provider-akeyless.md
     - HashiCorp Vault: provider-hashicorp-vault.md
     - Yandex:
         - Lockbox: provider-yandex-lockbox.md

+ 6 - 1
main.go

@@ -24,6 +24,7 @@ import (
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@@ -45,6 +46,7 @@ func main() {
 	var metricsAddr string
 	var controllerClass string
 	var enableLeaderElection bool
+	var concurrent int
 	var loglevel string
 	var namespace string
 	flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.")
@@ -52,6 +54,7 @@ func main() {
 	flag.BoolVar(&enableLeaderElection, "enable-leader-election", false,
 		"Enable leader election for controller manager. "+
 			"Enabling this will ensure there is only one active controller manager.")
+	flag.IntVar(&concurrent, "concurrent", 1, "The number of concurrent ExternalSecret reconciles.")
 	flag.StringVar(&loglevel, "loglevel", "info", "loglevel to use, one of: debug, info, warn, error, dpanic, panic, fatal")
 	flag.StringVar(&namespace, "namespace", "", "watch external secrets scoped in the provided namespace only")
 	flag.Parse()
@@ -93,7 +96,9 @@ func main() {
 		Scheme:          mgr.GetScheme(),
 		ControllerClass: controllerClass,
 		RequeueInterval: time.Hour,
-	}).SetupWithManager(mgr); err != nil {
+	}).SetupWithManager(mgr, controller.Options{
+		MaxConcurrentReconciles: concurrent,
+	}); err != nil {
 		setupLog.Error(err, "unable to create controller", "controller", "ExternalSecret")
 		os.Exit(1)
 	}

+ 3 - 1
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -28,6 +28,7 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@@ -409,8 +410,9 @@ func (r *Reconciler) getProviderSecretData(ctx context.Context, providerClient p
 }
 
 // SetupWithManager returns a new controller builder that will be started by the provided Manager.
-func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
+func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
 	return ctrl.NewControllerManagedBy(mgr).
+		WithOptions(opts).
 		For(&esv1alpha1.ExternalSecret{}).
 		Owns(&v1.Secret{}).
 		Complete(r)

+ 4 - 1
pkg/controllers/externalsecret/suite_test.go

@@ -26,6 +26,7 @@ import (
 	"k8s.io/client-go/rest"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/envtest"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
 	"sigs.k8s.io/controller-runtime/pkg/log/zap"
@@ -79,7 +80,9 @@ var _ = BeforeSuite(func() {
 		Scheme:          k8sManager.GetScheme(),
 		Log:             ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
 		RequeueInterval: time.Second,
-	}).SetupWithManager(k8sManager)
+	}).SetupWithManager(k8sManager, controller.Options{
+		MaxConcurrentReconciles: 1,
+	})
 	Expect(err).ToNot(HaveOccurred())
 
 	go func() {

+ 155 - 0
pkg/provider/akeyless/akeyless.go

@@ -0,0 +1,155 @@
+/*
+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 akeyless
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strconv"
+
+	"github.com/akeylesslabs/akeyless-go/v2"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/pkg/provider"
+	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+	defaultAPIUrl = "https://api.akeyless.io"
+)
+
+// Provider satisfies the provider interface.
+type Provider struct{}
+
+// akeylessBase satisfies the provider.SecretsClient interface.
+type akeylessBase struct {
+	kube      client.Client
+	store     esv1alpha1.GenericStore
+	namespace string
+
+	akeylessGwAPIURL string
+	RestAPI          *akeyless.V2ApiService
+}
+
+type Akeyless struct {
+	Client akeylessVaultInterface
+}
+
+type akeylessVaultInterface interface {
+	GetSecretByType(secretName, token string, version int32) (string, error)
+	TokenFromSecretRef(ctx context.Context) (string, error)
+}
+
+func init() {
+	schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{
+		Akeyless: &esv1alpha1.AkeylessProvider{},
+	})
+}
+
+// NewClient constructs a new secrets client based on the provided store.
+func (p *Provider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+	return newClient(ctx, store, kube, namespace)
+}
+
+func newClient(_ context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+	akl := &akeylessBase{
+		kube:      kube,
+		store:     store,
+		namespace: namespace,
+	}
+
+	spec, err := GetAKeylessProvider(store)
+	if err != nil {
+		return nil, err
+	}
+	akeylessGwAPIURL := defaultAPIUrl
+	if spec != nil && spec.AkeylessGWApiURL != nil && *spec.AkeylessGWApiURL != "" {
+		akeylessGwAPIURL = getV2Url(*spec.AkeylessGWApiURL)
+	}
+
+	if spec.Auth == nil {
+		return nil, fmt.Errorf("missing Auth in store config")
+	}
+
+	RestAPIClient := akeyless.NewAPIClient(&akeyless.Configuration{
+		Servers: []akeyless.ServerConfiguration{
+			{
+				URL: akeylessGwAPIURL,
+			},
+		},
+	}).V2Api
+
+	akl.akeylessGwAPIURL = akeylessGwAPIURL
+	akl.RestAPI = RestAPIClient
+	return &Akeyless{Client: akl}, nil
+}
+
+func (a *Akeyless) Close(ctx context.Context) error {
+	return nil
+}
+
+// Implements store.Client.GetSecret Interface.
+// Retrieves a secret with the secret name defined in ref.Name.
+func (a *Akeyless) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	if utils.IsNil(a.Client) {
+		return nil, fmt.Errorf(errUninitalizedAkeylessProvider)
+	}
+
+	token, err := a.Client.TokenFromSecretRef(ctx)
+	if err != nil {
+		return nil, err
+	}
+	version := int32(0)
+	if ref.Version != "" {
+		i, err := strconv.ParseInt(ref.Version, 10, 32)
+		if err == nil {
+			version = int32(i)
+		}
+	}
+	value, err := a.Client.GetSecretByType(ref.Key, token, version)
+	if err != nil {
+		return nil, err
+	}
+	return []byte(value), nil
+}
+
+// Implements store.Client.GetSecretMap Interface.
+// New version of GetSecretMap.
+func (a *Akeyless) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	if utils.IsNil(a.Client) {
+		return nil, fmt.Errorf(errUninitalizedAkeylessProvider)
+	}
+
+	val, err := a.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+	// Maps the json data to a string:string map
+	kv := make(map[string]string)
+	err = json.Unmarshal(val, &kv)
+	if err != nil {
+		return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
+	}
+
+	// Converts values in K:V pairs into bytes, while leaving keys as strings
+	secretData := make(map[string][]byte)
+	for k, v := range kv {
+		secretData[k] = []byte(v)
+	}
+	return secretData, nil
+}

+ 250 - 0
pkg/provider/akeyless/akeyless_api.go

@@ -0,0 +1,250 @@
+/*
+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 akeyless
+
+import (
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+
+	aws_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws"
+	azure_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/azure"
+	gcp_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/gcp"
+	"github.com/akeylesslabs/akeyless-go/v2"
+)
+
+var apiErr akeyless.GenericOpenAPIError
+
+const DefServiceAccountFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
+
+func (a *akeylessBase) GetToken(accessID, accType, accTypeParam string) (string, error) {
+	ctx := context.Background()
+	authBody := akeyless.NewAuthWithDefaults()
+	authBody.AccessId = akeyless.PtrString(accessID)
+	if accType == "api_key" || accType == "access_key" {
+		authBody.AccessKey = akeyless.PtrString(accTypeParam)
+	} else if accType == "k8s" {
+		jwtString, err := readK8SServiceAccountJWT()
+		if err != nil {
+			return "", fmt.Errorf("failed to read JWT with Kubernetes Auth from %v. error: %w", DefServiceAccountFile, err)
+		}
+		K8SAuthConfigName := accTypeParam
+		authBody.AccessType = akeyless.PtrString(accType)
+		authBody.K8sServiceAccountToken = akeyless.PtrString(jwtString)
+		authBody.K8sAuthConfigName = akeyless.PtrString(K8SAuthConfigName)
+	} else {
+		cloudID, err := a.getCloudID(accType, accTypeParam)
+		if err != nil {
+			return "", fmt.Errorf("Require Cloud ID " + err.Error())
+		}
+		authBody.AccessType = akeyless.PtrString(accType)
+		authBody.CloudId = akeyless.PtrString(cloudID)
+	}
+
+	authOut, _, err := a.RestAPI.Auth(ctx).Body(*authBody).Execute()
+	if err != nil {
+		if errors.As(err, &apiErr) {
+			return "", fmt.Errorf("authentication failed: %v", string(apiErr.Body()))
+		}
+		return "", fmt.Errorf("authentication failed: %w", err)
+	}
+
+	token := authOut.GetToken()
+	return token, nil
+}
+
+func (a *akeylessBase) GetSecretByType(secretName, token string, version int32) (string, error) {
+	item, err := a.DescribeItem(secretName, token)
+	if err != nil {
+		return "", err
+	}
+	secretType := item.GetItemType()
+
+	switch secretType {
+	case "STATIC_SECRET":
+		return a.GetStaticSecret(secretName, token, version)
+	case "DYNAMIC_SECRET":
+		return a.GetDynamicSecrets(secretName, token)
+	case "ROTATED_SECRET":
+		return a.GetRotatedSecrets(secretName, token, version)
+	default:
+		return "", fmt.Errorf("invalid item type: %v", secretType)
+	}
+}
+
+func (a *akeylessBase) DescribeItem(itemName, token string) (*akeyless.Item, error) {
+	ctx := context.Background()
+
+	body := akeyless.DescribeItem{
+		Name: itemName,
+	}
+	if strings.HasPrefix(token, "u-") {
+		body.UidToken = &token
+	} else {
+		body.Token = &token
+	}
+	gsvOut, _, err := a.RestAPI.DescribeItem(ctx).Body(body).Execute()
+	if err != nil {
+		if errors.As(err, &apiErr) {
+			return nil, fmt.Errorf("can't describe item: %v", string(apiErr.Body()))
+		}
+		return nil, fmt.Errorf("can't describe item: %w", err)
+	}
+
+	return &gsvOut, nil
+}
+
+func (a *akeylessBase) GetRotatedSecrets(secretName, token string, version int32) (string, error) {
+	ctx := context.Background()
+
+	body := akeyless.GetRotatedSecretValue{
+		Names:   secretName,
+		Version: &version,
+	}
+	if strings.HasPrefix(token, "u-") {
+		body.UidToken = &token
+	} else {
+		body.Token = &token
+	}
+
+	gsvOut, _, err := a.RestAPI.GetRotatedSecretValue(ctx).Body(body).Execute()
+	if err != nil {
+		if errors.As(err, &apiErr) {
+			return "", fmt.Errorf("can't get rotated secret value: %v", string(apiErr.Body()))
+		}
+		return "", fmt.Errorf("can't get rotated secret value: %w", err)
+	}
+
+	val, ok := gsvOut["value"]
+	if ok {
+		if _, ok := val["payload"]; ok {
+			return fmt.Sprintf("%v", val["payload"]), nil
+		} else if _, ok := val["target_value"]; ok {
+			out, err := json.Marshal(val["target_value"])
+			if err != nil {
+				return "", fmt.Errorf("can't marshal rotated secret value: %w", err)
+			}
+			return string(out), nil
+		} else {
+			out, err := json.Marshal(val)
+			if err != nil {
+				return "", fmt.Errorf("can't marshal rotated secret value: %w", err)
+			}
+			return string(out), nil
+		}
+	}
+	out, err := json.Marshal(gsvOut)
+	if err != nil {
+		return "", fmt.Errorf("can't marshal rotated secret value: %w", err)
+	}
+	return string(out), nil
+}
+
+func (a *akeylessBase) GetDynamicSecrets(secretName, token string) (string, error) {
+	ctx := context.Background()
+
+	body := akeyless.GetDynamicSecretValue{
+		Name: secretName,
+	}
+	if strings.HasPrefix(token, "u-") {
+		body.UidToken = &token
+	} else {
+		body.Token = &token
+	}
+
+	gsvOut, _, err := a.RestAPI.GetDynamicSecretValue(ctx).Body(body).Execute()
+	if err != nil {
+		if errors.As(err, &apiErr) {
+			return "", fmt.Errorf("can't get dynamic secret value: %v", string(apiErr.Body()))
+		}
+		return "", fmt.Errorf("can't get dynamic secret value: %w", err)
+	}
+
+	out, err := json.Marshal(gsvOut)
+	if err != nil {
+		return "", fmt.Errorf("can't marshal dynamic secret value: %w", err)
+	}
+
+	return string(out), nil
+}
+
+func (a *akeylessBase) GetStaticSecret(secretName, token string, version int32) (string, error) {
+	ctx := context.Background()
+
+	gsvBody := akeyless.GetSecretValue{
+		Names:   []string{secretName},
+		Version: &version,
+	}
+
+	if strings.HasPrefix(token, "u-") {
+		gsvBody.UidToken = &token
+	} else {
+		gsvBody.Token = &token
+	}
+
+	gsvOut, _, err := a.RestAPI.GetSecretValue(ctx).Body(gsvBody).Execute()
+	if err != nil {
+		if errors.As(err, &apiErr) {
+			return "", fmt.Errorf("can't get secret value: %v", string(apiErr.Body()))
+		}
+		return "", fmt.Errorf("can't get secret value: %w", err)
+	}
+	val, ok := gsvOut[secretName]
+	if !ok {
+		return "", fmt.Errorf("can't get secret: %v", secretName)
+	}
+
+	return val, nil
+}
+
+func (a *akeylessBase) getCloudID(provider, accTypeParam string) (string, error) {
+	var cloudID string
+	var err error
+
+	switch provider {
+	case "azure_ad":
+		cloudID, err = azure_cloud_id.GetCloudId(accTypeParam)
+	case "aws_iam":
+		cloudID, err = aws_cloud_id.GetCloudId()
+	case "gcp":
+		cloudID, err = gcp_cloud_id.GetCloudID(accTypeParam)
+	default:
+		return "", fmt.Errorf("unable to determine provider: %s", provider)
+	}
+	return cloudID, err
+}
+
+// readK8SServiceAccountJWT reads the JWT data for the Agent to submit to Akeyless Gateway.
+func readK8SServiceAccountJWT() (string, error) {
+	data, err := os.Open(DefServiceAccountFile)
+	if err != nil {
+		return "", err
+	}
+	defer data.Close()
+
+	contentBytes, err := ioutil.ReadAll(data)
+	if err != nil {
+		return "", err
+	}
+
+	a := strings.TrimSpace(string(contentBytes))
+
+	return base64.StdEncoding.EncodeToString([]byte(a)), nil
+}

+ 168 - 0
pkg/provider/akeyless/akeyless_test.go

@@ -0,0 +1,168 @@
+/*
+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 akeyless
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	fakeakeyless "github.com/external-secrets/external-secrets/pkg/provider/akeyless/fake"
+)
+
+type akeylessTestCase struct {
+	mockClient     *fakeakeyless.AkeylessMockClient
+	apiInput       *fakeakeyless.Input
+	apiOutput      *fakeakeyless.Output
+	ref            *esv1alpha1.ExternalSecretDataRemoteRef
+	expectError    string
+	expectedSecret string
+	// for testing secretmap
+	expectedData map[string][]byte
+}
+
+func makeValidAkeylessTestCase() *akeylessTestCase {
+	smtc := akeylessTestCase{
+		mockClient:     &fakeakeyless.AkeylessMockClient{},
+		apiInput:       makeValidInput(),
+		ref:            makeValidRef(),
+		apiOutput:      makeValidOutput(),
+		expectError:    "",
+		expectedSecret: "",
+		expectedData:   map[string][]byte{},
+	}
+	smtc.mockClient.WithValue(smtc.apiInput, smtc.apiOutput)
+	return &smtc
+}
+
+func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef {
+	return &esv1alpha1.ExternalSecretDataRemoteRef{
+		Key:     "test-secret",
+		Version: "1",
+	}
+}
+
+func makeValidInput() *fakeakeyless.Input {
+	return &fakeakeyless.Input{
+		SecretName: "name",
+		Version:    0,
+		Token:      "token",
+	}
+}
+
+func makeValidOutput() *fakeakeyless.Output {
+	return &fakeakeyless.Output{
+		Value: "secret-val",
+		Err:   nil,
+	}
+}
+
+func makeValidAkeylessTestCaseCustom(tweaks ...func(smtc *akeylessTestCase)) *akeylessTestCase {
+	smtc := makeValidAkeylessTestCase()
+	for _, fn := range tweaks {
+		fn(smtc)
+	}
+	smtc.mockClient.WithValue(smtc.apiInput, smtc.apiOutput)
+	return smtc
+}
+
+// This case can be shared by both GetSecret and GetSecretMap tests.
+// bad case: set apiErr.
+var setAPIErr = func(smtc *akeylessTestCase) {
+	smtc.apiOutput.Err = fmt.Errorf("oh no")
+	smtc.expectError = "oh no"
+}
+
+var setNilMockClient = func(smtc *akeylessTestCase) {
+	smtc.mockClient = nil
+	smtc.expectError = errUninitalizedAkeylessProvider
+}
+
+func TestAkeylessGetSecret(t *testing.T) {
+	secretValue := "changedvalue"
+	// good case: default version is set
+	// key is passed in, output is sent back
+	setSecretString := func(smtc *akeylessTestCase) {
+		smtc.apiOutput = &fakeakeyless.Output{
+			Value: secretValue,
+			Err:   nil,
+		}
+		smtc.expectedSecret = secretValue
+	}
+
+	successCases := []*akeylessTestCase{
+		makeValidAkeylessTestCaseCustom(setAPIErr),
+		makeValidAkeylessTestCaseCustom(setSecretString),
+		makeValidAkeylessTestCaseCustom(setNilMockClient),
+	}
+
+	sm := Akeyless{}
+	for k, v := range successCases {
+		sm.Client = v.mockClient
+		fmt.Println(*v.ref)
+		out, err := sm.GetSecret(context.Background(), *v.ref)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if string(out) != v.expectedSecret {
+			t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out))
+		}
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	// good case: default version & deserialization
+	setDeserialization := func(smtc *akeylessTestCase) {
+		smtc.apiOutput.Value = `{"foo":"bar"}`
+		smtc.expectedData["foo"] = []byte("bar")
+	}
+
+	// bad case: invalid json
+	setInvalidJSON := func(smtc *akeylessTestCase) {
+		smtc.apiOutput.Value = `-----------------`
+		smtc.expectError = "unable to unmarshal secret"
+	}
+
+	successCases := []*akeylessTestCase{
+		makeValidAkeylessTestCaseCustom(setDeserialization),
+		makeValidAkeylessTestCaseCustom(setInvalidJSON),
+		makeValidAkeylessTestCaseCustom(setAPIErr),
+		makeValidAkeylessTestCaseCustom(setNilMockClient),
+	}
+
+	sm := Akeyless{}
+	for k, v := range successCases {
+		sm.Client = v.mockClient
+		out, err := sm.GetSecretMap(context.Background(), *v.ref)
+		if !ErrorContains(err, v.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)
+		}
+		if err == nil && !reflect.DeepEqual(out, v.expectedData) {
+			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out)
+		}
+	}
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}

+ 103 - 0
pkg/provider/akeyless/auth.go

@@ -0,0 +1,103 @@
+/*
+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 akeyless
+
+import (
+	"context"
+	"fmt"
+
+	v1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+const (
+	errInvalidClusterStoreMissingAKIDNamespace = "invalid ClusterSecretStore: missing Akeyless AccessID Namespace"
+	errInvalidClusterStoreMissingSAKNamespace  = "invalid ClusterSecretStore: missing Akeyless AccessType Namespace"
+	errFetchAKIDSecret                         = "could not fetch accessID secret: %w"
+	errFetchSAKSecret                          = "could not fetch AccessType secret: %w"
+	errMissingSAK                              = "missing SecretAccessKey"
+	errMissingAKID                             = "missing AccessKeyID"
+)
+
+func (a *akeylessBase) TokenFromSecretRef(ctx context.Context) (string, error) {
+	prov, err := GetAKeylessProvider(a.store)
+	if err != nil {
+		return "", err
+	}
+
+	ke := client.ObjectKey{
+		Name:      prov.Auth.SecretRef.AccessID.Name,
+		Namespace: a.namespace, // default to ExternalSecret namespace
+	}
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
+		if prov.Auth.SecretRef.AccessID.Namespace == nil {
+			return "", fmt.Errorf(errInvalidClusterStoreMissingAKIDNamespace)
+		}
+		ke.Namespace = *prov.Auth.SecretRef.AccessID.Namespace
+	}
+	accessIDSecret := v1.Secret{}
+	err = a.kube.Get(ctx, ke, &accessIDSecret)
+	if err != nil {
+		return "", fmt.Errorf(errFetchAKIDSecret, err)
+	}
+	ke = client.ObjectKey{
+		Name:      prov.Auth.SecretRef.AccessType.Name,
+		Namespace: a.namespace, // default to ExternalSecret namespace
+	}
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
+		if prov.Auth.SecretRef.AccessType.Namespace == nil {
+			return "", fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
+		}
+		ke.Namespace = *prov.Auth.SecretRef.AccessType.Namespace
+	}
+	accessTypeSecret := v1.Secret{}
+	err = a.kube.Get(ctx, ke, &accessTypeSecret)
+	if err != nil {
+		return "", fmt.Errorf(errFetchSAKSecret, err)
+	}
+
+	ke = client.ObjectKey{
+		Name:      prov.Auth.SecretRef.AccessTypeParam.Name,
+		Namespace: a.namespace, // default to ExternalSecret namespace
+	}
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
+		if prov.Auth.SecretRef.AccessType.Namespace == nil {
+			return "", fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
+		}
+		ke.Namespace = *prov.Auth.SecretRef.AccessType.Namespace
+	}
+	accessTypeParamSecret := v1.Secret{}
+	err = a.kube.Get(ctx, ke, &accessTypeParamSecret)
+	if err != nil {
+		return "", fmt.Errorf(errFetchSAKSecret, err)
+	}
+	accessID := string(accessIDSecret.Data[prov.Auth.SecretRef.AccessID.Key])
+	accessType := string(accessTypeSecret.Data[prov.Auth.SecretRef.AccessType.Key])
+	accessTypeParam := string(accessTypeSecret.Data[prov.Auth.SecretRef.AccessTypeParam.Key])
+
+	if accessID == "" {
+		return "", fmt.Errorf(errMissingSAK)
+	}
+	if accessType == "" {
+		return "", fmt.Errorf(errMissingAKID)
+	}
+
+	return a.GetToken(accessID, accessType, accessTypeParam)
+}

+ 49 - 0
pkg/provider/akeyless/fake/fake.go

@@ -0,0 +1,49 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package fake
+
+import (
+	"context"
+)
+
+type AkeylessMockClient struct {
+	getSecret func(secretName, token string, version int32) (string, error)
+}
+
+func (mc *AkeylessMockClient) TokenFromSecretRef(ctx context.Context) (string, error) {
+	return "newToken", nil
+}
+
+func (mc *AkeylessMockClient) GetSecretByType(secretName, token string, version int32) (string, error) {
+	return mc.getSecret(secretName, token, version)
+}
+
+func (mc *AkeylessMockClient) WithValue(in *Input, out *Output) {
+	if mc != nil {
+		mc.getSecret = func(secretName, token string, version int32) (string, error) {
+			return out.Value, out.Err
+		}
+	}
+}
+
+type Input struct {
+	SecretName string
+	Token      string
+	Version    int32
+}
+
+type Output struct {
+	Value string
+	Err   error
+}

+ 99 - 0
pkg/provider/akeyless/utils.go

@@ -0,0 +1,99 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package akeyless
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+const (
+	errNilStore                     = "found nil store"
+	errMissingStoreSpec             = "store is missing spec"
+	errMissingProvider              = "storeSpec is missing provider"
+	errInvalidProvider              = "invalid provider spec. Missing Akeyless field in store %s"
+	errJSONSecretUnmarshal          = "unable to unmarshal secret: %w"
+	errUninitalizedAkeylessProvider = "provider akeyless is not initialized"
+)
+
+// GetAKeylessProvider does the necessary nil checks and returns the akeyless provider or an error.
+func GetAKeylessProvider(store esv1alpha1.GenericStore) (*esv1alpha1.AkeylessProvider, error) {
+	if store == nil {
+		return nil, fmt.Errorf(errNilStore)
+	}
+	spc := store.GetSpec()
+	if spc == nil {
+		return nil, fmt.Errorf(errMissingStoreSpec)
+	}
+	if spc.Provider == nil {
+		return nil, fmt.Errorf(errMissingProvider)
+	}
+	prov := spc.Provider.Akeyless
+	if prov == nil {
+		return nil, fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String())
+	}
+	return prov, nil
+}
+
+func getV2Url(path string) string {
+	// add check if not v2
+	rebody := sendReq(path)
+	if strings.Contains(rebody, "unknown command") {
+		return path
+	}
+
+	if strings.HasSuffix(path, "/v2") {
+		return path
+	}
+	url, err := url.Parse(path)
+	if err != nil {
+		return path
+	}
+	if strings.HasSuffix(url.Host, "/v2") {
+		return path
+	}
+	url.Host += "/v2"
+	p := url.Scheme + "://" + url.Host
+	if url.Port() != "" {
+		p = p + ":" + url.Port()
+	}
+
+	return p
+}
+
+func sendReq(url string) string {
+	req, err := http.NewRequest("POST", url, nil)
+	if err != nil {
+		return ""
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	client := &http.Client{
+		Timeout: 10 * time.Second,
+	}
+	resp, err := client.Do(req)
+	if err != nil {
+		return ""
+	}
+	defer resp.Body.Close()
+
+	body, _ := ioutil.ReadAll(resp.Body)
+	return string(body)
+}

+ 8 - 2
pkg/provider/aws/secretsmanager/secretsmanager.go

@@ -111,14 +111,20 @@ func (sm *SecretsManager) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
 	if err != nil {
 		return nil, err
 	}
-	kv := make(map[string]string)
+	kv := make(map[string]json.RawMessage)
 	err = json.Unmarshal(data, &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 {
-		secretData[k] = []byte(v)
+		var strVal string
+		err = json.Unmarshal(v, &strVal)
+		if err == nil {
+			secretData[k] = []byte(strVal)
+		} else {
+			secretData[k] = v
+		}
 	}
 	return secretData, nil
 }

+ 7 - 0
pkg/provider/aws/secretsmanager/secretsmanager_test.go

@@ -243,6 +243,12 @@ func TestGetSecretMap(t *testing.T) {
 		smtc.expectedData["foo"] = []byte("bar")
 	}
 
+	// good case: nested json
+	setNestedJSON := func(smtc *secretsManagerTestCase) {
+		smtc.apiOutput.SecretString = aws.String(`{"foobar":{"baz":"nestedval"}}`)
+		smtc.expectedData["foobar"] = []byte("{\"baz\":\"nestedval\"}")
+	}
+
 	// good case: caching
 	cachedMap := func(smtc *secretsManagerTestCase) {
 		smtc.apiOutput.SecretString = aws.String(`{"foo":"bar", "plus": "one"}`)
@@ -259,6 +265,7 @@ func TestGetSecretMap(t *testing.T) {
 
 	successCases := []*secretsManagerTestCase{
 		makeValidSecretsManagerTestCaseCustom(setDeserialization),
+		makeValidSecretsManagerTestCaseCustom(setNestedJSON),
 		makeValidSecretsManagerTestCaseCustom(setAPIErr),
 		makeValidSecretsManagerTestCaseCustom(setInvalidJSON),
 		makeValidSecretsManagerTestCaseCustom(cachedMap),

+ 57 - 20
pkg/provider/azure/keyvault/keyvault.go

@@ -35,6 +35,7 @@ import (
 
 const (
 	defaultObjType = "secret"
+	vaultResource  = "https://vault.azure.net"
 )
 
 // interface to keyvault.BaseClient.
@@ -70,15 +71,18 @@ func newClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.C
 		store:     store,
 		namespace: namespace,
 	}
-	azClient, vaultURL, err := anAzure.newAzureClient(ctx)
 
-	if err != nil {
-		return nil, err
+	clientSet, err := anAzure.setAzureClientWithManagedIdentity()
+	if clientSet {
+		return anAzure, err
+	}
+
+	clientSet, err = anAzure.setAzureClientWithServicePrincipal(ctx)
+	if clientSet {
+		return anAzure, err
 	}
 
-	anAzure.baseClient = azClient
-	anAzure.vaultURL = vaultURL
-	return anAzure, nil
+	return nil, fmt.Errorf("cannot initialize Azure Client: no valid authType was specified")
 }
 
 // Implements store.Client.GetSecret Interface.
@@ -164,42 +168,75 @@ func (a *Azure) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretD
 	return nil, fmt.Errorf("unknown Azure Keyvault object Type for %s", secretName)
 }
 
-func (a *Azure) newAzureClient(ctx context.Context) (*keyvault.BaseClient, string, error) {
+func (a *Azure) setAzureClientWithManagedIdentity() (bool, error) {
 	spec := *a.store.GetSpec().Provider.AzureKV
-	tenantID := *spec.TenantID
-	vaultURL := *spec.VaultURL
 
+	if *spec.AuthType != esv1alpha1.ManagedIdentity {
+		return false, nil
+	}
+
+	msiConfig := kvauth.NewMSIConfig()
+	msiConfig.Resource = vaultResource
+	if spec.IdentityID != nil {
+		msiConfig.ClientID = *spec.IdentityID
+	}
+	authorizer, err := msiConfig.Authorizer()
+	if err != nil {
+		return true, err
+	}
+
+	basicClient := keyvault.New()
+	basicClient.Authorizer = authorizer
+
+	a.baseClient = basicClient
+	a.vaultURL = *spec.VaultURL
+
+	return true, nil
+}
+
+func (a *Azure) setAzureClientWithServicePrincipal(ctx context.Context) (bool, error) {
+	spec := *a.store.GetSpec().Provider.AzureKV
+
+	if *spec.AuthType != esv1alpha1.ServicePrincipal {
+		return false, nil
+	}
+
+	if spec.TenantID == nil {
+		return true, fmt.Errorf("missing tenantID in store config")
+	}
 	if spec.AuthSecretRef == nil {
-		return nil, "", fmt.Errorf("missing clientID/clientSecret in store config")
+		return true, fmt.Errorf("missing clientID/clientSecret in store config")
+	}
+	if spec.AuthSecretRef.ClientID == nil || spec.AuthSecretRef.ClientSecret == nil {
+		return true, fmt.Errorf("missing accessKeyID/secretAccessKey in store config")
 	}
 	clusterScoped := false
 	if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
 		clusterScoped = true
 	}
-	if spec.AuthSecretRef.ClientID == nil || spec.AuthSecretRef.ClientSecret == nil {
-		return nil, "", fmt.Errorf("missing accessKeyID/secretAccessKey in store config")
-	}
 	cid, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientID, clusterScoped)
 	if err != nil {
-		return nil, "", err
+		return true, err
 	}
 	csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientSecret, clusterScoped)
 	if err != nil {
-		return nil, "", err
+		return true, err
 	}
 
-	clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, tenantID)
-	// the default resource api is the management URL and not the vault URL which we need for keyvault operations
-	clientCredentialsConfig.Resource = "https://vault.azure.net"
+	clientCredentialsConfig := kvauth.NewClientCredentialsConfig(cid, csec, *spec.TenantID)
+	clientCredentialsConfig.Resource = vaultResource
 	authorizer, err := clientCredentialsConfig.Authorizer()
 	if err != nil {
-		return nil, "", err
+		return true, err
 	}
 
 	basicClient := keyvault.New()
 	basicClient.Authorizer = authorizer
 
-	return &basicClient, vaultURL, nil
+	a.baseClient = &basicClient
+	a.vaultURL = *spec.VaultURL
+
+	return true, nil
 }
 
 func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) {

+ 36 - 10
pkg/provider/azure/keyvault/keyvault_test.go

@@ -82,15 +82,46 @@ func makeValidSecretManagerTestCaseCustom(tweaks ...func(smtc *secretManagerTest
 	return smtc
 }
 
+func TestNewClientManagedIdentityNoNeedForCredentials(t *testing.T) {
+	namespace := "internal"
+	vaultURL := "https://local.vault.url"
+	identityID := "1234"
+	authType := esv1alpha1.ManagedIdentity
+	store := esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: namespace,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{Provider: &esv1alpha1.SecretStoreProvider{AzureKV: &esv1alpha1.AzureKVProvider{
+			AuthType:   &authType,
+			IdentityID: &identityID,
+			VaultURL:   &vaultURL,
+		}}},
+	}
+
+	provider, err := schema.GetProvider(&store)
+	tassert.Nil(t, err, "the return err should be nil")
+	k8sClient := clientfake.NewClientBuilder().Build()
+	secretClient, err := provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	if err != nil {
+		// On non Azure environment, MSI auth not available, so this error should be returned
+		tassert.EqualError(t, err, "failed to get oauth token from MSI: MSI not available")
+	} else {
+		// On Azure (where GitHub Actions are running) a secretClient is returned, as only an Authorizer is configured, but no token is requested for MI
+		tassert.NotNil(t, secretClient)
+	}
+}
+
 func TestNewClientNoCreds(t *testing.T) {
 	namespace := "internal"
 	vaultURL := "https://local.vault.url"
 	tenantID := "1234"
+	authType := esv1alpha1.ServicePrincipal
 	store := esv1alpha1.SecretStore{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: namespace,
 		},
 		Spec: esv1alpha1.SecretStoreSpec{Provider: &esv1alpha1.SecretStoreProvider{AzureKV: &esv1alpha1.AzureKVProvider{
+			AuthType: &authType,
 			VaultURL: &vaultURL,
 			TenantID: &tenantID,
 		}}},
@@ -98,32 +129,27 @@ func TestNewClientNoCreds(t *testing.T) {
 	provider, err := schema.GetProvider(&store)
 	tassert.Nil(t, err, "the return err should be nil")
 	k8sClient := clientfake.NewClientBuilder().Build()
-	secretClient, err := provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
 	tassert.EqualError(t, err, "missing clientID/clientSecret in store config")
-	tassert.Nil(t, secretClient)
 
 	store.Spec.Provider.AzureKV.AuthSecretRef = &esv1alpha1.AzureKVAuth{}
-	secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
 	tassert.EqualError(t, err, "missing accessKeyID/secretAccessKey in store config")
-	tassert.Nil(t, secretClient)
 
 	store.Spec.Provider.AzureKV.AuthSecretRef.ClientID = &v1.SecretKeySelector{Name: "user"}
-	secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
 	tassert.EqualError(t, err, "missing accessKeyID/secretAccessKey in store config")
-	tassert.Nil(t, secretClient)
 
 	store.Spec.Provider.AzureKV.AuthSecretRef.ClientSecret = &v1.SecretKeySelector{Name: "password"}
-	secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
 	tassert.EqualError(t, err, "could not find secret internal/user: secrets \"user\" not found")
-	tassert.Nil(t, secretClient)
 	store.TypeMeta.Kind = esv1alpha1.ClusterSecretStoreKind
 	store.TypeMeta.APIVersion = esv1alpha1.ClusterSecretStoreKindAPIVersion
 	ns := "default"
 	store.Spec.Provider.AzureKV.AuthSecretRef.ClientID.Namespace = &ns
 	store.Spec.Provider.AzureKV.AuthSecretRef.ClientSecret.Namespace = &ns
-	secretClient, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
+	_, err = provider.NewClient(context.Background(), &store, k8sClient, namespace)
 	tassert.EqualError(t, err, "could not find secret default/user: secrets \"user\" not found")
-	tassert.Nil(t, secretClient)
 }
 
 const (

+ 69 - 50
pkg/provider/gcp/secretmanager/secretsmanager.go

@@ -21,10 +21,11 @@ import (
 	secretmanager "cloud.google.com/go/secretmanager/apiv1"
 	"github.com/googleapis/gax-go"
 	"github.com/tidwall/gjson"
+	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
 	"google.golang.org/api/option"
 	secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
-	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/types"
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
@@ -40,11 +41,12 @@ const (
 
 	errGCPSMStore                             = "received invalid GCPSM SecretStore resource"
 	errClientClose                            = "unable to close SecretManager client: %w"
+	errMissingStoreSpec                       = "invalid: missing store spec"
 	errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing GCP SecretAccessKey Namespace"
+	errInvalidClusterStoreMissingSANamespace  = "invalid ClusterSecretStore: missing GCP Service Account Namespace"
 	errFetchSAKSecret                         = "could not fetch SecretAccessKey secret: %w"
 	errMissingSAK                             = "missing SecretAccessKey"
 	errUnableProcessJSONCredentials           = "failed to process the provided JSON credentials: %w"
-	errUnableProcessDefaultCredentials        = "failed to process the default credentials: %w"
 	errUnableCreateGCPSMClient                = "failed to create GCP secretmanager client: %w"
 	errUninitalizedGCPProvider                = "provider GCP is not initialized"
 	errClientGetSecretAccess                  = "unable to access Secret from SecretManager Client: %w"
@@ -63,43 +65,64 @@ type ProviderGCP struct {
 }
 
 type gClient struct {
-	kube        kclient.Client
-	store       *esv1alpha1.GCPSMProvider
-	namespace   string
-	storeKind   string
-	credentials []byte
+	kube             kclient.Client
+	store            *esv1alpha1.GCPSMProvider
+	namespace        string
+	storeKind        string
+	workloadIdentity *workloadIdentity
 }
 
-func (c *gClient) setAuth(ctx context.Context) error {
-	credentialsSecret := &corev1.Secret{}
-	credentialsSecretName := c.store.Auth.SecretRef.SecretAccessKey.Name
+func (c *gClient) getTokenSource(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
+	ts, err := serviceAccountTokenSource(ctx, store, kube, namespace)
+	if ts != nil || err != nil {
+		return ts, err
+	}
+	ts, err = c.workloadIdentity.TokenSource(ctx, store, kube, namespace)
+	if ts != nil || err != nil {
+		return ts, err
+	}
+
+	return google.DefaultTokenSource(ctx, CloudPlatformRole)
+}
+
+func serviceAccountTokenSource(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
+	spec := store.GetSpec()
+	if spec == nil || spec.Provider.GCPSM == nil {
+		return nil, fmt.Errorf(errMissingStoreSpec)
+	}
+	sr := spec.Provider.GCPSM.Auth.SecretRef
+	if sr == nil {
+		return nil, nil
+	}
+	storeKind := store.GetObjectKind().GroupVersionKind().Kind
+	credentialsSecret := &v1.Secret{}
+	credentialsSecretName := sr.SecretAccessKey.Name
 	objectKey := types.NamespacedName{
 		Name:      credentialsSecretName,
-		Namespace: c.namespace,
+		Namespace: namespace,
 	}
 
 	// only ClusterStore is allowed to set namespace (and then it's required)
-	if c.storeKind == esv1alpha1.ClusterSecretStoreKind {
-		if credentialsSecretName != "" && c.store.Auth.SecretRef.SecretAccessKey.Namespace == nil {
-			return fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
+	if storeKind == esv1alpha1.ClusterSecretStoreKind {
+		if credentialsSecretName != "" && sr.SecretAccessKey.Namespace == nil {
+			return nil, fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
 		} else if credentialsSecretName != "" {
-			objectKey.Namespace = *c.store.Auth.SecretRef.SecretAccessKey.Namespace
+			objectKey.Namespace = *sr.SecretAccessKey.Namespace
 		}
 	}
-	if credentialsSecretName == "" {
-		c.credentials = nil
-		return nil
-	}
-	err := c.kube.Get(ctx, objectKey, credentialsSecret)
+	err := kube.Get(ctx, objectKey, credentialsSecret)
 	if err != nil {
-		return fmt.Errorf(errFetchSAKSecret, err)
+		return nil, fmt.Errorf(errFetchSAKSecret, err)
 	}
-
-	c.credentials = credentialsSecret.Data[c.store.Auth.SecretRef.SecretAccessKey.Key]
-	if (c.credentials == nil) || (len(c.credentials) == 0) {
-		return fmt.Errorf(errMissingSAK)
+	credentials := credentialsSecret.Data[sr.SecretAccessKey.Key]
+	if (credentials == nil) || (len(credentials) == 0) {
+		return nil, fmt.Errorf(errMissingSAK)
 	}
-	return nil
+	config, err := google.JWTConfigFromJSON(credentials, CloudPlatformRole)
+	if err != nil {
+		return nil, fmt.Errorf(errUnableProcessJSONCredentials, err)
+	}
+	return config.TokenSource(ctx), nil
 }
 
 // NewClient constructs a GCP Provider.
@@ -110,36 +133,26 @@ func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1alpha1.GenericSt
 	}
 	storeSpecGCPSM := storeSpec.Provider.GCPSM
 
-	cliStore := gClient{
-		kube:      kube,
-		store:     storeSpecGCPSM,
-		namespace: namespace,
-		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+	wi, err := newWorkloadIdentity(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("unable to initialize workload identity")
 	}
 
-	if err := cliStore.setAuth(ctx); err != nil {
-		return nil, err
+	cliStore := gClient{
+		kube:             kube,
+		store:            storeSpecGCPSM,
+		namespace:        namespace,
+		storeKind:        store.GetObjectKind().GroupVersionKind().Kind,
+		workloadIdentity: wi,
 	}
 
 	sm.projectID = cliStore.store.ProjectID
 
-	if cliStore.credentials != nil {
-		config, err := google.JWTConfigFromJSON(cliStore.credentials, CloudPlatformRole)
-		if err != nil {
-			return nil, fmt.Errorf(errUnableProcessJSONCredentials, err)
-		}
-		ts := config.TokenSource(ctx)
-		clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
-		if err != nil {
-			return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
-		}
-		sm.SecretManagerClient = clientGCPSM
-		return sm, nil
-	}
-	ts, err := google.DefaultTokenSource(ctx, CloudPlatformRole)
+	ts, err := cliStore.getTokenSource(ctx, store, kube, namespace)
 	if err != nil {
-		return nil, fmt.Errorf(errUnableProcessDefaultCredentials, err)
+		return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
 	}
+
 	clientGCPSM, err := secretmanager.NewClient(ctx, option.WithTokenSource(ts))
 	if err != nil {
 		return nil, fmt.Errorf(errUnableCreateGCPSMClient, err)
@@ -197,7 +210,7 @@ func (sm *ProviderGCP) GetSecretMap(ctx context.Context, ref esv1alpha1.External
 		return nil, err
 	}
 
-	kv := make(map[string]string)
+	kv := make(map[string]json.RawMessage)
 	err = json.Unmarshal(data, &kv)
 	if err != nil {
 		return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
@@ -205,7 +218,13 @@ func (sm *ProviderGCP) GetSecretMap(ctx context.Context, ref esv1alpha1.External
 
 	secretData := make(map[string][]byte)
 	for k, v := range kv {
-		secretData[k] = []byte(v)
+		var strVal string
+		err = json.Unmarshal(v, &strVal)
+		if err == nil {
+			secretData[k] = []byte(strVal)
+		} else {
+			secretData[k] = v
+		}
 	}
 
 	return secretData, nil

+ 8 - 0
pkg/provider/gcp/secretmanager/secretsmanager_test.go

@@ -172,11 +172,19 @@ func TestGetSecretMap(t *testing.T) {
 		smtc.expectError = "unable to unmarshal secret"
 	}
 
+	// good case: deserialize nested json as []byte, if it's a string, decode the string
+	setNestedJSON := func(smtc *secretManagerTestCase) {
+		smtc.apiOutput.Payload.Data = []byte(`{"foo":{"bar":"baz"}, "qux": "qu\"z"}`)
+		smtc.expectedData["foo"] = []byte(`{"bar":"baz"}`)
+		smtc.expectedData["qux"] = []byte("qu\"z")
+	}
+
 	successCases := []*secretManagerTestCase{
 		makeValidSecretManagerTestCaseCustom(setDeserialization),
 		makeValidSecretManagerTestCaseCustom(setAPIErr),
 		makeValidSecretManagerTestCaseCustom(setNilMockClient),
 		makeValidSecretManagerTestCaseCustom(setInvalidJSON),
+		makeValidSecretManagerTestCaseCustom(setNestedJSON),
 	}
 
 	sm := ProviderGCP{}

+ 254 - 0
pkg/provider/gcp/secretmanager/secretsmanager_workload_identity.go

@@ -0,0 +1,254 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package secretmanager
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"time"
+
+	iam "cloud.google.com/go/iam/credentials/apiv1"
+	secretmanager "cloud.google.com/go/secretmanager/apiv1"
+	"github.com/googleapis/gax-go"
+	"golang.org/x/oauth2"
+	"google.golang.org/api/option"
+	credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"grpc.go4.org/credentials/oauth"
+	authenticationv1 "k8s.io/api/authentication/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/kubernetes"
+	clientcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+)
+
+const (
+	gcpSAAnnotation = "iam.gke.io/gcp-service-account"
+
+	errFetchPodToken  = "unable to fetch pod token: %w"
+	errFetchIBToken   = "unable to fetch identitybindingtoken: %w"
+	errGenAccessToken = "unable to generate gcp access token: %w"
+)
+
+// workloadIdentity holds all clients and generators needed
+// to create a gcp oauth token.
+type workloadIdentity struct {
+	iamClient            IamClient
+	idBindTokenGenerator idBindTokenGenerator
+	saTokenGenerator     saTokenGenerator
+}
+
+// interface to GCP IAM API.
+type IamClient interface {
+	GenerateAccessToken(ctx context.Context, req *credentialspb.GenerateAccessTokenRequest, opts ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
+}
+
+// interface to securetoken/identitybindingtoken API.
+type idBindTokenGenerator interface {
+	Generate(context.Context, *http.Client, string, string, string) (*oauth2.Token, error)
+}
+
+// interface to kubernetes serviceaccount token request API.
+type saTokenGenerator interface {
+	Generate(context.Context, string, string, string) (*authenticationv1.TokenRequest, error)
+}
+
+func newWorkloadIdentity(ctx context.Context) (*workloadIdentity, error) {
+	iamc, err := newIAMClient(ctx)
+	if err != nil {
+		return nil, err
+	}
+	satg, err := newSATokenGenerator()
+	if err != nil {
+		return nil, err
+	}
+	return &workloadIdentity{
+		iamClient:            iamc,
+		idBindTokenGenerator: newIDBindTokenGenerator(),
+		saTokenGenerator:     satg,
+	}, nil
+}
+
+func (w *workloadIdentity) TokenSource(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
+	spec := store.GetSpec()
+	if spec == nil || spec.Provider == nil || spec.Provider.GCPSM == nil {
+		return nil, fmt.Errorf(errMissingStoreSpec)
+	}
+	wi := spec.Provider.GCPSM.Auth.WorkloadIdentity
+	if wi == nil {
+		return nil, nil
+	}
+	storeKind := store.GetObjectKind().GroupVersionKind().Kind
+	saKey := types.NamespacedName{
+		Name:      wi.ServiceAccountRef.Name,
+		Namespace: namespace,
+	}
+
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if storeKind == esv1alpha1.ClusterSecretStoreKind {
+		if wi.ServiceAccountRef.Namespace == nil {
+			return nil, fmt.Errorf(errInvalidClusterStoreMissingSANamespace)
+		}
+		saKey.Namespace = *wi.ServiceAccountRef.Namespace
+	}
+
+	sa := &v1.ServiceAccount{}
+	err := kube.Get(ctx, saKey, sa)
+	if err != nil {
+		return nil, err
+	}
+
+	idProvider := fmt.Sprintf("https://container.googleapis.com/v1/projects/%s/locations/%s/clusters/%s",
+		spec.Provider.GCPSM.ProjectID,
+		wi.ClusterLocation,
+		wi.ClusterName)
+	idPool := fmt.Sprintf("%s.svc.id.goog", spec.Provider.GCPSM.ProjectID)
+	gcpSA := sa.Annotations[gcpSAAnnotation]
+
+	resp, err := w.saTokenGenerator.Generate(ctx, idPool, saKey.Name, saKey.Namespace)
+	if err != nil {
+		return nil, fmt.Errorf(errFetchPodToken, err)
+	}
+
+	idBindToken, err := w.idBindTokenGenerator.Generate(ctx, http.DefaultClient, resp.Status.Token, idPool, idProvider)
+	if err != nil {
+		return nil, fmt.Errorf(errFetchIBToken, err)
+	}
+
+	// If no `iam.gke.io/gcp-service-account` annotation is present the
+	// identitybindingtoken will be used directly, allowing bindings on secrets
+	// of the form "serviceAccount:<project>.svc.id.goog[<namespace>/<sa>]".
+	if gcpSA == "" {
+		return oauth2.StaticTokenSource(idBindToken), nil
+	}
+	gcpSAResp, err := w.iamClient.GenerateAccessToken(ctx, &credentialspb.GenerateAccessTokenRequest{
+		Name:  fmt.Sprintf("projects/-/serviceAccounts/%s", gcpSA),
+		Scope: secretmanager.DefaultAuthScopes(),
+	}, gax.WithGRPCOptions(grpc.PerRPCCredentials(oauth.TokenSource{TokenSource: oauth2.StaticTokenSource(idBindToken)})))
+	if err != nil {
+		return nil, fmt.Errorf(errGenAccessToken, err)
+	}
+	return oauth2.StaticTokenSource(&oauth2.Token{
+		AccessToken: gcpSAResp.GetAccessToken(),
+	}), nil
+}
+
+func newIAMClient(ctx context.Context) (IamClient, error) {
+	iamOpts := []option.ClientOption{
+		option.WithUserAgent("external-secrets-operator"),
+		// tell the secretmanager library to not add transport-level ADC since
+		// we need to override on a per call basis
+		option.WithoutAuthentication(),
+		// grpc oauth TokenSource credentials require transport security, so
+		// this must be set explicitly even though TLS is used
+		option.WithGRPCDialOption(grpc.WithTransportCredentials(credentials.NewTLS(nil))),
+		option.WithGRPCConnectionPool(5),
+	}
+	return iam.NewIamCredentialsClient(ctx, iamOpts...)
+}
+
+type k8sSATokenGenerator struct {
+	corev1 clientcorev1.CoreV1Interface
+}
+
+func (g *k8sSATokenGenerator) Generate(ctx context.Context, idPool, name, namespace string) (*authenticationv1.TokenRequest, error) {
+	// Request a serviceaccount token for the pod
+	ttl := int64((15 * time.Minute).Seconds())
+	return g.corev1.
+		ServiceAccounts(namespace).
+		CreateToken(ctx, name,
+			&authenticationv1.TokenRequest{
+				Spec: authenticationv1.TokenRequestSpec{
+					ExpirationSeconds: &ttl,
+					Audiences:         []string{idPool},
+				},
+			},
+			metav1.CreateOptions{},
+		)
+}
+
+func newSATokenGenerator() (saTokenGenerator, error) {
+	cfg, err := ctrlcfg.GetConfig()
+	if err != nil {
+		return nil, err
+	}
+	clientset, err := kubernetes.NewForConfig(cfg)
+	if err != nil {
+		return nil, err
+	}
+	return &k8sSATokenGenerator{
+		corev1: clientset.CoreV1(),
+	}, nil
+}
+
+// Trades the kubernetes token for an identitybindingtoken token.
+type gcpIDBindTokenGenerator struct {
+	targetURL string
+}
+
+func newIDBindTokenGenerator() idBindTokenGenerator {
+	return &gcpIDBindTokenGenerator{
+		targetURL: "https://securetoken.googleapis.com/v1/identitybindingtoken",
+	}
+}
+
+func (g *gcpIDBindTokenGenerator) Generate(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
+	body, err := json.Marshal(map[string]string{
+		"grant_type":           "urn:ietf:params:oauth:grant-type:token-exchange",
+		"subject_token_type":   "urn:ietf:params:oauth:token-type:jwt",
+		"requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
+		"subject_token":        k8sToken,
+		"audience":             fmt.Sprintf("identitynamespace:%s:%s", idPool, idProvider),
+		"scope":                "https://www.googleapis.com/auth/cloud-platform",
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := http.NewRequestWithContext(ctx, "POST", g.targetURL, bytes.NewBuffer(body))
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("could not get idbindtoken token, status: %v", resp.StatusCode)
+	}
+
+	defer resp.Body.Close()
+	respBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	idBindToken := &oauth2.Token{}
+	if err := json.Unmarshal(respBody, idBindToken); err != nil {
+		return nil, err
+	}
+	return idBindToken, nil
+}

+ 392 - 0
pkg/provider/gcp/secretmanager/secretsmanager_workload_identity_test.go

@@ -0,0 +1,392 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package secretmanager
+
+import (
+	"context"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/googleapis/gax-go"
+	"github.com/stretchr/testify/assert"
+	"golang.org/x/oauth2"
+	credentialspb "google.golang.org/genproto/googleapis/iam/credentials/v1"
+	authv1 "k8s.io/api/authentication/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type workloadIdentityTest struct {
+	name           string
+	expTS          bool
+	expToken       *oauth2.Token
+	expErr         string
+	genAccessToken func(context.Context, *credentialspb.GenerateAccessTokenRequest, ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
+	genIDBindToken func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error)
+	genSAToken     func(c context.Context, s1, s2, s3 string) (*authv1.TokenRequest, error)
+	store          esv1alpha1.GenericStore
+	kubeObjects    []client.Object
+}
+
+func TestWorkloadIdentity(t *testing.T) {
+	clusterSANamespace := "foobar"
+	tbl := []*workloadIdentityTest{
+		composeTestcase(
+			defaultTestCase("missing store spec should result in error"),
+			withErr("invalid: missing store spec"),
+			withStore(&esv1alpha1.SecretStore{}),
+		),
+		composeTestcase(
+			defaultTestCase("should skip when no workload identity is configured: TokenSource and error must be nil"),
+			withStore(&esv1alpha1.SecretStore{
+				Spec: esv1alpha1.SecretStoreSpec{
+					Provider: &esv1alpha1.SecretStoreProvider{
+						GCPSM: &esv1alpha1.GCPSMProvider{},
+					},
+				},
+			}),
+		),
+		composeTestcase(
+			defaultTestCase("return access token from GenerateAccessTokenRequest with SecretStore"),
+			withStore(defaultStore()),
+			expTokenSource(),
+			expectToken(defaultGenAccessToken),
+		),
+		composeTestcase(
+			defaultTestCase("return idBindToken when no annotation is set with SecretStore"),
+			expTokenSource(),
+			expectToken(defaultIDBindToken),
+			withStore(defaultStore()),
+			withK8sResources([]client.Object{
+				&v1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:        "example",
+						Namespace:   "default",
+						Annotations: map[string]string{},
+					},
+				},
+			}),
+		),
+		composeTestcase(
+			defaultTestCase("invalid ClusterSecretStore: missing service account namespace"),
+			expErr("invalid ClusterSecretStore: missing GCP Service Account Namespace"),
+			withStore(
+				composeStore(defaultClusterStore()),
+			),
+			withK8sResources([]client.Object{
+				&v1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:        "example",
+						Namespace:   "default",
+						Annotations: map[string]string{},
+					},
+				},
+			}),
+		),
+		composeTestcase(
+			defaultTestCase("return access token from GenerateAccessTokenRequest with ClusterSecretStore"),
+			expTokenSource(),
+			expectToken(defaultGenAccessToken),
+			withStore(
+				composeStore(defaultClusterStore(), withSANamespace(clusterSANamespace)),
+			),
+			withK8sResources([]client.Object{
+				&v1.ServiceAccount{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "example",
+						Namespace: clusterSANamespace,
+						Annotations: map[string]string{
+							gcpSAAnnotation: "example",
+						},
+					},
+				},
+			}),
+		),
+	}
+
+	for _, row := range tbl {
+		t.Run(row.name, func(t *testing.T) {
+			fakeIam := &fakeIAMClient{generateAccessTokenFunc: row.genAccessToken}
+			fakeIDBGen := &fakeIDBindTokenGen{generateFunc: row.genIDBindToken}
+			fakeSATG := &fakeSATokenGen{GenerateFunc: row.genSAToken}
+			w := &workloadIdentity{
+				iamClient:            fakeIam,
+				idBindTokenGenerator: fakeIDBGen,
+				saTokenGenerator:     fakeSATG,
+			}
+			cb := clientfake.NewClientBuilder()
+			cb.WithObjects(row.kubeObjects...)
+			client := cb.Build()
+			ts, err := w.TokenSource(context.Background(), row.store, client, "default")
+			// assert err
+			if row.expErr == "" {
+				assert.NoError(t, err)
+			} else {
+				assert.Error(t, err, row.expErr)
+			}
+			// assert ts
+			if row.expTS {
+				assert.NotNil(t, ts)
+				if row.expToken != nil {
+					tk, err := ts.Token()
+					assert.NoError(t, err)
+					assert.EqualValues(t, tk, row.expToken)
+				}
+			} else {
+				assert.Nil(t, ts)
+			}
+		})
+	}
+}
+
+func TestSATokenGen(t *testing.T) {
+	corev1 := &fakeK8sV1{}
+	g := &k8sSATokenGenerator{
+		corev1: corev1,
+	}
+	token, err := g.Generate(context.Background(), "my-fake-audience", "bar", "default")
+	assert.Nil(t, err)
+	assert.Equal(t, token.Status.Token, defaultSAToken)
+	assert.Equal(t, token.Spec.Audiences[0], "my-fake-audience")
+}
+
+func TestIDBTokenGen(t *testing.T) {
+	srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		payload := make(map[string]string)
+		rb, err := ioutil.ReadAll(r.Body)
+		assert.Nil(t, err)
+		err = json.Unmarshal(rb, &payload)
+		assert.Nil(t, err)
+		assert.Equal(t, payload["audience"], "identitynamespace:some-idpool:some-id-provider")
+
+		bt, err := json.Marshal(&oauth2.Token{
+			AccessToken: "12345",
+		})
+		assert.Nil(t, err)
+		rw.WriteHeader(http.StatusOK)
+		rw.Write(bt)
+	}))
+	defer srv.Close()
+	gen := &gcpIDBindTokenGenerator{
+		targetURL: srv.URL,
+	}
+	token, err := gen.Generate(context.Background(), http.DefaultClient, "some-token", "some-idpool", "some-id-provider")
+	assert.Nil(t, err)
+	assert.Equal(t, token.AccessToken, "12345")
+}
+
+type testCaseMutator func(tc *workloadIdentityTest)
+
+func composeTestcase(tc *workloadIdentityTest, mutators ...testCaseMutator) *workloadIdentityTest {
+	for _, m := range mutators {
+		m(tc)
+	}
+	return tc
+}
+
+func withErr(err string) testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.expErr = err
+	}
+}
+
+func withStore(store esv1alpha1.GenericStore) testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.store = store
+	}
+}
+
+func expTokenSource() testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.expTS = true
+	}
+}
+
+func expectToken(token string) testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.expToken = &oauth2.Token{
+			AccessToken: token,
+		}
+	}
+}
+
+func expErr(err string) testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.expErr = err
+	}
+}
+
+func withK8sResources(objs []client.Object) testCaseMutator {
+	return func(tc *workloadIdentityTest) {
+		tc.kubeObjects = objs
+	}
+}
+
+var (
+	defaultGenAccessToken = "default-gen-access-token"
+	defaultIDBindToken    = "default-id-bind-token"
+	defaultSAToken        = "default-k8s-sa-token"
+)
+
+func defaultTestCase(name string) *workloadIdentityTest {
+	return &workloadIdentityTest{
+		name: name,
+		genAccessToken: func(c context.Context, gatr *credentialspb.GenerateAccessTokenRequest, co ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error) {
+			return &credentialspb.GenerateAccessTokenResponse{
+				AccessToken: defaultGenAccessToken,
+			}, nil
+		},
+		genIDBindToken: func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
+			return &oauth2.Token{
+				AccessToken: defaultIDBindToken,
+			}, nil
+		},
+		genSAToken: func(c context.Context, s1, s2, s3 string) (*authv1.TokenRequest, error) {
+			return &authv1.TokenRequest{
+				Status: authv1.TokenRequestStatus{
+					Token: defaultSAToken,
+				},
+			}, nil
+		},
+		kubeObjects: []client.Object{
+			&v1.ServiceAccount{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "example",
+					Namespace: "default",
+					Annotations: map[string]string{
+						gcpSAAnnotation: "example",
+					},
+				},
+			},
+		},
+	}
+}
+
+func defaultStore() *esv1alpha1.SecretStore {
+	return &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "foobar",
+			Namespace: "default",
+		},
+		Spec: defaultStoreSpec(),
+	}
+}
+
+func defaultClusterStore() *esv1alpha1.ClusterSecretStore {
+	return &esv1alpha1.ClusterSecretStore{
+		TypeMeta: metav1.TypeMeta{
+			Kind: esv1alpha1.ClusterSecretStoreKind,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "foobar",
+		},
+		Spec: defaultStoreSpec(),
+	}
+}
+
+func defaultStoreSpec() esv1alpha1.SecretStoreSpec {
+	return esv1alpha1.SecretStoreSpec{
+		Provider: &esv1alpha1.SecretStoreProvider{
+			GCPSM: &esv1alpha1.GCPSMProvider{
+				Auth: esv1alpha1.GCPSMAuth{
+					WorkloadIdentity: &esv1alpha1.GCPWorkloadIdentity{
+						ServiceAccountRef: esmeta.ServiceAccountSelector{
+							Name: "example",
+						},
+						ClusterLocation: "example",
+						ClusterName:     "foobar",
+					},
+				},
+				ProjectID: "1234",
+			},
+		},
+	}
+}
+
+type storeMutator func(spc esv1alpha1.GenericStore)
+
+func composeStore(store esv1alpha1.GenericStore, mutators ...storeMutator) esv1alpha1.GenericStore {
+	for _, m := range mutators {
+		m(store)
+	}
+	return store
+}
+
+func withSANamespace(namespace string) storeMutator {
+	return func(store esv1alpha1.GenericStore) {
+		spc := store.GetSpec()
+		spc.Provider.GCPSM.Auth.WorkloadIdentity.ServiceAccountRef.Namespace = &namespace
+	}
+}
+
+// fake IDBindToken Generator.
+type fakeIDBindTokenGen struct {
+	generateFunc func(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error)
+}
+
+func (g *fakeIDBindTokenGen) Generate(ctx context.Context, client *http.Client, k8sToken, idPool, idProvider string) (*oauth2.Token, error) {
+	return g.generateFunc(ctx, client, k8sToken, idPool, idProvider)
+}
+
+// fake IAM Client.
+type fakeIAMClient struct {
+	generateAccessTokenFunc func(context.Context, *credentialspb.GenerateAccessTokenRequest, ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
+}
+
+func (f *fakeIAMClient) GenerateAccessToken(ctx context.Context, req *credentialspb.GenerateAccessTokenRequest, opts ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error) {
+	return f.generateAccessTokenFunc(ctx, req, opts...)
+}
+
+// fake SA Token Generator.
+type fakeSATokenGen struct {
+	GenerateFunc func(context.Context, string, string, string) (*authv1.TokenRequest, error)
+}
+
+func (f *fakeSATokenGen) Generate(ctx context.Context, idPool, namespace, name string) (*authv1.TokenRequest, error) {
+	return f.GenerateFunc(ctx, idPool, namespace, name)
+}
+
+// fake k8s client for creating tokens.
+type fakeK8sV1 struct {
+	k8sv1.CoreV1Interface
+}
+
+func (m *fakeK8sV1) ServiceAccounts(namespace string) k8sv1.ServiceAccountInterface {
+	return &fakeK8sV1SA{v1mock: m}
+}
+
+// Mock the K8s service account client.
+type fakeK8sV1SA struct {
+	k8sv1.ServiceAccountInterface
+	v1mock *fakeK8sV1
+}
+
+func (ma *fakeK8sV1SA) CreateToken(
+	ctx context.Context,
+	serviceAccountName string,
+	tokenRequest *authv1.TokenRequest,
+	opts metav1.CreateOptions,
+) (*authv1.TokenRequest, error) {
+	tokenRequest.Status.Token = defaultSAToken
+	return tokenRequest, nil
+}

+ 23 - 0
pkg/provider/ibm/provider.go

@@ -18,6 +18,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"strings"
+	"time"
 
 	"github.com/IBM/go-sdk-core/v5/core"
 	sm "github.com/IBM/secrets-manager-go-sdk/secretsmanagerv1"
@@ -334,6 +335,28 @@ func (ibm *providerIBM) NewClient(ctx context.Context, store esv1alpha1.GenericS
 		},
 	})
 
+	// Setup retry options, but only if present
+	if storeSpec.RetrySettings != nil {
+		var retryAmount int
+		var retryDuration time.Duration
+
+		if storeSpec.RetrySettings.MaxRetries != nil {
+			retryAmount = int(*storeSpec.RetrySettings.MaxRetries)
+		} else {
+			retryAmount = 3
+		}
+
+		if storeSpec.RetrySettings.RetryInterval != nil {
+			retryDuration, err = time.ParseDuration(*storeSpec.RetrySettings.RetryInterval)
+		} else {
+			retryDuration = 5 * time.Second
+		}
+
+		if err == nil {
+			secretsManager.Service.EnableRetries(retryAmount, retryDuration)
+		}
+	}
+
 	if err != nil {
 		return nil, fmt.Errorf(errIBMClient, err)
 	}

+ 52 - 0
pkg/provider/ibm/provider_test.go

@@ -22,9 +22,13 @@ import (
 
 	"github.com/IBM/go-sdk-core/v5/core"
 	sm "github.com/IBM/secrets-manager-go-sdk/secretsmanagerv1"
+	"github.com/crossplane/crossplane-runtime/pkg/test"
+	corev1 "k8s.io/api/core/v1"
 	utilpointer "k8s.io/utils/pointer"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
 	fakesm "github.com/external-secrets/external-secrets/pkg/provider/ibm/fake"
 )
 
@@ -372,6 +376,54 @@ func TestGetSecretMap(t *testing.T) {
 	}
 }
 
+func TestValidRetryInput(t *testing.T) {
+	sm := providerIBM{}
+
+	invalid := "Invalid"
+	serviceURL := "http://fake-service-url.cool"
+
+	spec := &esv1alpha1.SecretStore{
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				IBM: &esv1alpha1.IBMProvider{
+					Auth: esv1alpha1.IBMAuth{
+						SecretRef: esv1alpha1.IBMAuthSecretRef{
+							SecretAPIKey: v1.SecretKeySelector{
+								Name: "fake-secret",
+								Key:  "fake-key",
+							},
+						},
+					},
+					ServiceURL: &serviceURL,
+				},
+			},
+			RetrySettings: &esv1alpha1.SecretStoreRetrySettings{
+				RetryInterval: &invalid,
+			},
+		},
+	}
+
+	expected := fmt.Sprintf("cannot setup new ibm client: time: invalid duration %q", invalid)
+	ctx := context.TODO()
+	kube := &test.MockClient{
+		MockGet: test.NewMockGetFn(nil, func(obj kclient.Object) error {
+			if o, ok := obj.(*corev1.Secret); ok {
+				o.Data = map[string][]byte{
+					"fake-key": []byte("ImAFakeApiKey"),
+				}
+				return nil
+			}
+			return nil
+		}),
+	}
+
+	_, err := sm.NewClient(ctx, spec, kube, "default")
+
+	if !ErrorContains(err, expected) {
+		t.Errorf("CheckValidRetryInput unexpected error: %s, expected: '%s'", err.Error(), expected)
+	}
+}
+
 func ErrorContains(out error, want string) bool {
 	if out == nil {
 		return want == ""

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

@@ -17,6 +17,7 @@ package register
 // packages imported here are registered to the controller schema.
 // nolint:revive
 import (
+	_ "github.com/external-secrets/external-secrets/pkg/provider/akeyless"
 	_ "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"
@@ -25,5 +26,6 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/webhook"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
 )

+ 21 - 7
pkg/provider/vault/vault.go

@@ -73,6 +73,7 @@ const (
 	errVaultRevokeToken = "error while revoking token: %w"
 
 	errUnknownCAProvider = "unknown caProvider type given"
+	errCANamespace       = "cannot read secret for CAProvider due to missing namespace on kind ClusterSecretStore"
 )
 
 type Client interface {
@@ -224,6 +225,8 @@ func (v *client) readSecret(ctx context.Context, path, version string) (map[stri
 			byteMap[k] = []byte(t)
 		case []byte:
 			byteMap[k] = t
+		case nil:
+			byteMap[k] = []byte(nil)
 		default:
 			return nil, errors.New(errSecretFormat)
 		}
@@ -249,6 +252,10 @@ func (v *client) newConfig() (*vault.Config, error) {
 		}
 	}
 
+	if v.store.CAProvider != nil && v.storeKind == esv1alpha1.ClusterSecretStoreKind && v.store.CAProvider.Namespace == nil {
+		return nil, errors.New(errCANamespace)
+	}
+
 	if v.store.CAProvider != nil {
 		var cert []byte
 		var err error
@@ -281,10 +288,14 @@ func (v *client) newConfig() (*vault.Config, error) {
 
 func getCertFromSecret(v *client) ([]byte, error) {
 	secretRef := esmeta.SecretKeySelector{
-		Name:      v.store.CAProvider.Name,
-		Namespace: &v.store.CAProvider.Namespace,
-		Key:       v.store.CAProvider.Key,
+		Name: v.store.CAProvider.Name,
+		Key:  v.store.CAProvider.Key,
+	}
+
+	if v.store.CAProvider.Namespace != nil {
+		secretRef.Namespace = v.store.CAProvider.Namespace
 	}
+
 	ctx := context.Background()
 	res, err := v.secretKeyRef(ctx, &secretRef)
 	if err != nil {
@@ -296,8 +307,11 @@ func getCertFromSecret(v *client) ([]byte, error) {
 
 func getCertFromConfigMap(v *client) ([]byte, error) {
 	objKey := types.NamespacedName{
-		Namespace: v.store.CAProvider.Namespace,
-		Name:      v.store.CAProvider.Name,
+		Name: v.store.CAProvider.Name,
+	}
+
+	if v.store.CAProvider.Namespace != nil {
+		objKey.Namespace = *v.store.CAProvider.Namespace
 	}
 
 	configMapRef := &corev1.ConfigMap{}
@@ -619,7 +633,7 @@ func (v *client) requestTokenWithLdapAuth(ctx context.Context, client Client, ld
 	parameters := map[string]string{
 		"password": password,
 	}
-	url := strings.Join([]string{"/v1", "auth", "ldap", "login", username}, "/")
+	url := strings.Join([]string{"/v1", "auth", ldapAuth.Path, "login", username}, "/")
 	request := client.NewRequest("POST", url)
 
 	err = request.SetJSONBody(parameters)
@@ -659,7 +673,7 @@ func (v *client) requestTokenWithJwtAuth(ctx context.Context, client Client, jwt
 		"role": role,
 		"jwt":  jwt,
 	}
-	url := strings.Join([]string{"/v1", "auth", "jwt", "login"}, "/")
+	url := strings.Join([]string{"/v1", "auth", jwtAuth.Path, "login"}, "/")
 	request := client.NewRequest("POST", url)
 
 	err = request.SetJSONBody(parameters)

+ 138 - 6
pkg/provider/vault/vault_test.go

@@ -41,7 +41,7 @@ const (
 	secretDataString = "some-creds"
 )
 
-func makeValidSecretStore() *esv1alpha1.SecretStore {
+func makeValidSecretStoreWithVersion(v esv1alpha1.VaultKVStoreVersion) *esv1alpha1.SecretStore {
 	return &esv1alpha1.SecretStore{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "vault-store",
@@ -52,7 +52,7 @@ func makeValidSecretStore() *esv1alpha1.SecretStore {
 				Vault: &esv1alpha1.VaultProvider{
 					Server:  "vault.example.com",
 					Path:    "secret",
-					Version: esv1alpha1.VaultKVStoreV2,
+					Version: v,
 					Auth: esv1alpha1.VaultAuth{
 						Kubernetes: &esv1alpha1.VaultKubernetesAuth{
 							Path: "kubernetes",
@@ -68,6 +68,10 @@ func makeValidSecretStore() *esv1alpha1.SecretStore {
 	}
 }
 
+func makeValidSecretStore() *esv1alpha1.SecretStore {
+	return makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV2)
+}
+
 func makeValidSecretStoreWithCerts() *esv1alpha1.SecretStore {
 	return &esv1alpha1.SecretStore{
 		ObjectMeta: metav1.ObjectMeta{
@@ -115,6 +119,41 @@ func makeValidSecretStoreWithK8sCerts(isSecret bool) *esv1alpha1.SecretStore {
 	return store
 }
 
+func makeInvalidClusterSecretStoreWithK8sCerts() *esv1alpha1.ClusterSecretStore {
+	return &esv1alpha1.ClusterSecretStore{
+		TypeMeta: metav1.TypeMeta{
+			Kind: "ClusterSecretStore",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "vault-store",
+			Namespace: "default",
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				Vault: &esv1alpha1.VaultProvider{
+					Server:  "vault.example.com",
+					Path:    "secret",
+					Version: "v2",
+					Auth: esv1alpha1.VaultAuth{
+						Kubernetes: &esv1alpha1.VaultKubernetesAuth{
+							Path: "kubernetes",
+							Role: "kubernetes-auth-role",
+							ServiceAccountRef: &esmeta.ServiceAccountSelector{
+								Name: "example-sa",
+							},
+						},
+					},
+					CAProvider: &esv1alpha1.CAProvider{
+						Name: "vault-cert",
+						Key:  "cert",
+						Type: "Secret",
+					},
+				},
+			},
+		},
+	}
+}
+
 type secretStoreTweakFn func(s *esv1alpha1.SecretStore)
 
 func makeSecretStore(tweaks ...secretStoreTweakFn) *esv1alpha1.SecretStore {
@@ -136,11 +175,15 @@ func newVaultResponse(data *vault.Secret) *vault.Response {
 	}
 }
 
-func newVaultTokenIDResponse(token string) *vault.Response {
+func newVaultResponseWithData(data map[string]interface{}) *vault.Response {
 	return newVaultResponse(&vault.Secret{
-		Data: map[string]interface{}{
-			"id": token,
-		},
+		Data: data,
+	})
+}
+
+func newVaultTokenIDResponse(token string) *vault.Response {
+	return newVaultResponseWithData(map[string]interface{}{
+		"id": token,
 	})
 }
 
@@ -344,6 +387,18 @@ MIICsTCCAZkCFEJJ4daz5sxkFlzq9n1djLEuG7bmMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMMCHZh
 				err: nil,
 			},
 		},
+		"GetCertNamespaceMissingError": {
+			reason: "Should return an error if namespace is missing and is a ClusterSecretStore",
+			args: args{
+				store: makeInvalidClusterSecretStoreWithK8sCerts(),
+				kube: &test.MockClient{
+					MockGet: test.NewMockGetFn(nil, kubeMockWithSecretTokenAndServiceAcc),
+				},
+			},
+			want: want{
+				err: errors.New(errCANamespace),
+			},
+		},
 		"GetCertSecretKeyMissingError": {
 			reason: "Should return an error if the secret key is missing",
 			args: args{
@@ -494,6 +549,15 @@ func vaultTest(t *testing.T, name string, tc testCase) {
 
 func TestGetSecretMap(t *testing.T) {
 	errBoom := errors.New("boom")
+	secret := map[string]interface{}{
+		"access_key":    "access_key",
+		"access_secret": "access_secret",
+	}
+	secretWithNilVal := map[string]interface{}{
+		"access_key":    "access_key",
+		"access_secret": "access_secret",
+		"token":         nil,
+	}
 
 	type args struct {
 		store   *esv1alpha1.VaultProvider
@@ -512,6 +576,74 @@ func TestGetSecretMap(t *testing.T) {
 		args   args
 		want   want
 	}{
+		"ReadSecretKV1": {
+			reason: "Should map the secret even if it has a nil value",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV1).Spec.Provider.Vault,
+				vClient: &fake.VaultClient{
+					MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}),
+					MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn(
+						newVaultResponseWithData(secret), nil,
+					),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"ReadSecretKV2": {
+			reason: "Should map the secret even if it has a nil value",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.VaultClient{
+					MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}),
+					MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn(
+						newVaultResponseWithData(
+							map[string]interface{}{
+								"data": secret,
+							},
+						), nil,
+					),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"ReadSecretWithNilValueKV1": {
+			reason: "Should map the secret even if it has a nil value",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV1).Spec.Provider.Vault,
+				vClient: &fake.VaultClient{
+					MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}),
+					MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn(
+						newVaultResponseWithData(secretWithNilVal), nil,
+					),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
+		"ReadSecretWithNilValueKV2": {
+			reason: "Should map the secret even if it has a nil value",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1alpha1.VaultKVStoreV2).Spec.Provider.Vault,
+				vClient: &fake.VaultClient{
+					MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}),
+					MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn(
+						newVaultResponseWithData(
+							map[string]interface{}{
+								"data": secretWithNilVal,
+							},
+						), nil,
+					),
+				},
+			},
+			want: want{
+				err: nil,
+			},
+		},
 		"ReadSecretError": {
 			reason: "Should return error if vault client fails to read secret.",
 			args: args{

+ 401 - 0
pkg/provider/webhook/webhook.go

@@ -0,0 +1,401 @@
+/*
+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 webhook
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+	tpl "text/template"
+
+	"github.com/Masterminds/sprig"
+	"github.com/PaesslerAG/jsonpath"
+	"gopkg.in/yaml.v3"
+	corev1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider"
+	"github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/template"
+)
+
+// Provider satisfies the provider interface.
+type Provider struct{}
+
+type WebHook struct {
+	kube      client.Client
+	store     esv1alpha1.GenericStore
+	namespace string
+	storeKind string
+	http      *http.Client
+}
+
+func init() {
+	schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{
+		Webhook: &esv1alpha1.WebhookProvider{},
+	})
+}
+
+func (p *Provider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
+	whClient := &WebHook{
+		kube:      kube,
+		store:     store,
+		namespace: namespace,
+		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+	}
+	provider, err := getProvider(store)
+	if err != nil {
+		return nil, err
+	}
+	whClient.http, err = whClient.getHTTPClient(provider)
+	if err != nil {
+		return nil, err
+	}
+	return whClient, nil
+}
+
+func getProvider(store esv1alpha1.GenericStore) (*esv1alpha1.WebhookProvider, error) {
+	spc := store.GetSpec()
+	if spc == nil || spc.Provider == nil || spc.Provider.Webhook == nil {
+		return nil, fmt.Errorf("missing store provider webhook")
+	}
+	return spc.Provider.Webhook, nil
+}
+
+func (w *WebHook) getStoreSecret(ctx context.Context, ref esmeta.SecretKeySelector) (*corev1.Secret, error) {
+	ke := client.ObjectKey{
+		Name:      ref.Name,
+		Namespace: w.namespace,
+	}
+	if w.storeKind == esv1alpha1.ClusterSecretStoreKind {
+		if ref.Namespace == nil {
+			return nil, fmt.Errorf("no namespace on ClusterSecretStore webhook secret %s", ref.Name)
+		}
+		ke.Namespace = *ref.Namespace
+	}
+	secret := &corev1.Secret{}
+	if err := w.kube.Get(ctx, ke, secret); err != nil {
+		return nil, fmt.Errorf("failed to get clustersecretstore webhook secret %s: %w", ref.Name, err)
+	}
+	return secret, nil
+}
+
+func (w *WebHook) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	provider, err := getProvider(w.store)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get store: %w", err)
+	}
+	result, err := w.getWebhookData(ctx, provider, ref)
+	if err != nil {
+		return nil, err
+	}
+	// Only parse as json if we have a jsonpath set
+	if provider.Result.JSONPath != "" {
+		jsondata := interface{}(nil)
+		if err := yaml.Unmarshal(result, &jsondata); err != nil {
+			return nil, fmt.Errorf("failed to parse response json: %w", err)
+		}
+		jsondata, err = jsonpath.Get(provider.Result.JSONPath, jsondata)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get response path %s: %w", provider.Result.JSONPath, err)
+		}
+		jsonvalue, ok := jsondata.(string)
+		if !ok {
+			return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
+		}
+		return []byte(jsonvalue), nil
+	}
+
+	return result, nil
+}
+
+func (w *WebHook) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	provider, err := getProvider(w.store)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get store: %w", err)
+	}
+	result, err := w.getWebhookData(ctx, provider, ref)
+	if err != nil {
+		return nil, err
+	}
+
+	// We always want json here, so just parse it out
+	jsondata := interface{}(nil)
+	if err := yaml.Unmarshal(result, &jsondata); err != nil {
+		return nil, fmt.Errorf("failed to parse response json: %w", err)
+	}
+	// Get subdata via jsonpath, if given
+	if provider.Result.JSONPath != "" {
+		jsondata, err = jsonpath.Get(provider.Result.JSONPath, jsondata)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get response path %s: %w", provider.Result.JSONPath, err)
+		}
+	}
+	// If the value is a string, try to parse it as json
+	jsonstring, ok := jsondata.(string)
+	if ok {
+		// This could also happen if the response was a single json-encoded string
+		// but that is an extremely unlikely scenario
+		if err := yaml.Unmarshal([]byte(jsonstring), &jsondata); err != nil {
+			return nil, fmt.Errorf("failed to parse response json from jsonpath: %w", err)
+		}
+	}
+	// Use the data as a key-value map
+	jsonvalue, ok := jsondata.(map[string]interface{})
+	if !ok {
+		return nil, fmt.Errorf("failed to get response (wrong type: %T)", jsondata)
+	}
+
+	// Change the map of generic objects to a map of byte arrays
+	values := make(map[string][]byte)
+	for rKey, rValue := range jsonvalue {
+		jVal, ok := rValue.(string)
+		if !ok {
+			return nil, fmt.Errorf("failed to get response (wrong type in key '%s': %T)", rKey, rValue)
+		}
+		values[rKey] = []byte(jVal)
+	}
+	return values, nil
+}
+
+func (w *WebHook) getTemplateData(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef, secrets []esv1alpha1.WebhookSecret) (map[string]map[string]string, error) {
+	data := map[string]map[string]string{
+		"remoteRef": {
+			"key":      url.QueryEscape(ref.Key),
+			"version":  url.QueryEscape(ref.Version),
+			"property": url.QueryEscape(ref.Property),
+		},
+	}
+	for _, secref := range secrets {
+		if _, ok := data[secref.Name]; !ok {
+			data[secref.Name] = make(map[string]string)
+		}
+		secret, err := w.getStoreSecret(ctx, secref.SecretRef)
+		if err != nil {
+			return nil, err
+		}
+		for sKey, sVal := range secret.Data {
+			data[secref.Name][sKey] = string(sVal)
+		}
+	}
+	return data, nil
+}
+
+func (w *WebHook) getWebhookData(ctx context.Context, provider *esv1alpha1.WebhookProvider, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	if w.http == nil {
+		return nil, fmt.Errorf("http client not initialized")
+	}
+	data, err := w.getTemplateData(ctx, ref, provider.Secrets)
+	if err != nil {
+		return nil, err
+	}
+	method := provider.Method
+	if method == "" {
+		method = http.MethodGet
+	}
+	url, err := executeTemplateString(provider.URL, data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse url: %w", err)
+	}
+	body, err := executeTemplate(provider.Body, data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse body: %w", err)
+	}
+
+	req, err := http.NewRequestWithContext(ctx, method, url, &body)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create request: %w", err)
+	}
+	for hKey, hValueTpl := range provider.Headers {
+		hValue, err := executeTemplateString(hValueTpl, data)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse header %s: %w", hKey, err)
+		}
+		req.Header.Add(hKey, hValue)
+	}
+
+	resp, err := w.http.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to call endpoint: %w", err)
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return nil, fmt.Errorf("endpoint gave error %s", resp.Status)
+	}
+	return io.ReadAll(resp.Body)
+}
+
+func (w *WebHook) getHTTPClient(provider *esv1alpha1.WebhookProvider) (*http.Client, error) {
+	client := &http.Client{}
+	if provider.Timeout != nil {
+		client.Timeout = provider.Timeout.Duration
+	}
+	if len(provider.CABundle) == 0 && provider.CAProvider == nil {
+		// No need to process ca stuff if it is not there
+		return client, nil
+	}
+	caCertPool, err := w.getCACertPool(provider)
+	if err != nil {
+		return nil, err
+	}
+
+	tlsConf := &tls.Config{
+		RootCAs:    caCertPool,
+		MinVersion: tls.VersionTLS12,
+	}
+	client.Transport = &http.Transport{TLSClientConfig: tlsConf}
+	return client, nil
+}
+
+func (w *WebHook) getCACertPool(provider *esv1alpha1.WebhookProvider) (*x509.CertPool, error) {
+	caCertPool := x509.NewCertPool()
+	if len(provider.CABundle) > 0 {
+		ok := caCertPool.AppendCertsFromPEM(provider.CABundle)
+		if !ok {
+			return nil, fmt.Errorf("failed to append cabundle")
+		}
+	}
+
+	if provider.CAProvider != nil && w.storeKind == esv1alpha1.ClusterSecretStoreKind && provider.CAProvider.Namespace == nil {
+		return nil, fmt.Errorf("missing namespace on CAProvider secret")
+	}
+
+	if provider.CAProvider != nil {
+		var cert []byte
+		var err error
+
+		switch provider.CAProvider.Type {
+		case esv1alpha1.WebhookCAProviderTypeSecret:
+			cert, err = w.getCertFromSecret(provider)
+		case esv1alpha1.WebhookCAProviderTypeConfigMap:
+			cert, err = w.getCertFromConfigMap(provider)
+		default:
+			err = fmt.Errorf("unknown caprovider type: %s", provider.CAProvider.Type)
+		}
+
+		if err != nil {
+			return nil, err
+		}
+
+		ok := caCertPool.AppendCertsFromPEM(cert)
+		if !ok {
+			return nil, fmt.Errorf("failed to append cabundle")
+		}
+	}
+	return caCertPool, nil
+}
+
+func (w *WebHook) getCertFromSecret(provider *esv1alpha1.WebhookProvider) ([]byte, error) {
+	secretRef := esmeta.SecretKeySelector{
+		Name: provider.CAProvider.Name,
+		Key:  provider.CAProvider.Key,
+	}
+
+	if provider.CAProvider.Namespace != nil {
+		secretRef.Namespace = provider.CAProvider.Namespace
+	}
+
+	ctx := context.Background()
+	res, err := w.secretKeyRef(ctx, &secretRef)
+	if err != nil {
+		return nil, err
+	}
+
+	return []byte(res), nil
+}
+
+func (w *WebHook) secretKeyRef(ctx context.Context, secretRef *esmeta.SecretKeySelector) (string, error) {
+	secret := &corev1.Secret{}
+	ref := client.ObjectKey{
+		Namespace: w.namespace,
+		Name:      secretRef.Name,
+	}
+	if (w.storeKind == esv1alpha1.ClusterSecretStoreKind) &&
+		(secretRef.Namespace != nil) {
+		ref.Namespace = *secretRef.Namespace
+	}
+	err := w.kube.Get(ctx, ref, secret)
+	if err != nil {
+		return "", err
+	}
+
+	keyBytes, ok := secret.Data[secretRef.Key]
+	if !ok {
+		return "", err
+	}
+
+	value := string(keyBytes)
+	valueStr := strings.TrimSpace(value)
+	return valueStr, nil
+}
+
+func (w *WebHook) getCertFromConfigMap(provider *esv1alpha1.WebhookProvider) ([]byte, error) {
+	objKey := client.ObjectKey{
+		Name: provider.CAProvider.Name,
+	}
+
+	if provider.CAProvider.Namespace != nil {
+		objKey.Namespace = *provider.CAProvider.Namespace
+	}
+
+	configMapRef := &corev1.ConfigMap{}
+	ctx := context.Background()
+	err := w.kube.Get(ctx, objKey, configMapRef)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get caprovider secret %s: %w", objKey.Name, err)
+	}
+
+	val, ok := configMapRef.Data[provider.CAProvider.Key]
+	if !ok {
+		return nil, fmt.Errorf("failed to get caprovider configmap %s -> %s", objKey.Name, provider.CAProvider.Key)
+	}
+
+	return []byte(val), nil
+}
+
+func (w *WebHook) Close(ctx context.Context) error {
+	return nil
+}
+
+func executeTemplateString(tmpl string, data map[string]map[string]string) (string, error) {
+	result, err := executeTemplate(tmpl, data)
+	if err != nil {
+		return "", err
+	}
+	return result.String(), nil
+}
+
+func executeTemplate(tmpl string, data map[string]map[string]string) (bytes.Buffer, error) {
+	var result bytes.Buffer
+	if tmpl == "" {
+		return result, nil
+	}
+	urlt, err := tpl.New("webhooktemplate").Funcs(sprig.TxtFuncMap()).Funcs(template.FuncMap()).Parse(tmpl)
+	if err != nil {
+		return result, err
+	}
+	if err := urlt.Execute(&result, data); err != nil {
+		return result, err
+	}
+	return result, nil
+}

+ 340 - 0
pkg/provider/webhook/webhook_test.go

@@ -0,0 +1,340 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package webhook
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+	"time"
+
+	"gopkg.in/yaml.v3"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/pkg/provider"
+)
+
+type testCase struct {
+	Case string `json:"case,omitempty"`
+	Args args   `json:"args"`
+	Want want   `json:"want"`
+}
+
+type args struct {
+	URL        string `json:"url,omitempty"`
+	Body       string `json:"body,omitempty"`
+	Timeout    string `json:"timeout,omitempty"`
+	Key        string `json:"key,omitempty"`
+	Version    string `json:"version,omitempty"`
+	JSONPath   string `json:"jsonpath,omitempty"`
+	Response   string `json:"response,omitempty"`
+	StatusCode int    `json:"statuscode,omitempty"`
+}
+
+type want struct {
+	Path      string            `json:"path,omitempty"`
+	Err       string            `json:"err,omitempty"`
+	Result    string            `json:"result,omitempty"`
+	ResultMap map[string]string `json:"resultmap,omitempty"`
+}
+
+var testCases = `
+case: error url
+args:
+  url: /api/getsecret?id={{ .unclosed.template
+want:
+  err: failed to parse url
+---
+case: error body
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  body: Body error {{ .unclosed.template
+want:
+  err: failed to parse body
+---
+case: error connection
+args:
+  url: 1/api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+want:
+  err: failed to call endpoint
+---
+case: error not found
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  statuscode: 404
+  response: not found
+want:
+  path: /api/getsecret?id=testkey&version=1
+  err: endpoint gave error 404
+---
+case: error bad json
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  jsonpath: $.result.thesecret
+  response: '{"result":{"thesecret":"secret-value"}'
+want:
+  path: /api/getsecret?id=testkey&version=1
+  err: failed to parse response json
+---
+case: error bad jsonpath
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  jsonpath: $.result.thesecret
+  response: '{"result":{"nosecret":"secret-value"}}'
+want:
+  path: /api/getsecret?id=testkey&version=1
+  err: failed to get response path
+---
+case: error bad json data
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  jsonpath: $.result.thesecret
+  response: '{"result":{"thesecret":{"one":"secret-value"}}}'
+want:
+  path: /api/getsecret?id=testkey&version=1
+  err: failed to get response (wrong type
+---
+case: error timeout
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  response: secret-value
+  timeout: 0.01ms
+want:
+  path: /api/getsecret?id=testkey&version=1
+  err: context deadline exceeded
+---
+case: good plaintext
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  response: secret-value
+want:
+  path: /api/getsecret?id=testkey&version=1
+  result: secret-value
+---
+case: good json
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  jsonpath: $.result.thesecret
+  response: '{"result":{"thesecret":"secret-value"}}'
+want:
+  path: /api/getsecret?id=testkey&version=1
+  result: secret-value
+---
+case: good json map
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  jsonpath: $.result
+  response: '{"result":{"thesecret":"secret-value","alsosecret":"another-value"}}'
+want:
+  path: /api/getsecret?id=testkey&version=1
+  resultmap:
+    thesecret: secret-value
+    alsosecret: another-value
+---
+case: good json map string
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  response: '{"thesecret":"secret-value","alsosecret":"another-value"}'
+want:
+  path: /api/getsecret?id=testkey&version=1
+  resultmap:
+    thesecret: secret-value
+    alsosecret: another-value
+---
+case: error json map string
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  response: 'some simple string'
+want:
+  path: /api/getsecret?id=testkey&version=1
+  err: failed to get response (wrong type
+  resultmap:
+    thesecret: secret-value
+    alsosecret: another-value
+---
+case: error json map
+args:
+  url: /api/getsecret?id={{ .remoteRef.key }}&version={{ .remoteRef.version }}
+  key: testkey
+  version: 1
+  jsonpath: $.result.thesecret
+  response: '{"result":{"thesecret":"secret-value","alsosecret":"another-value"}}'
+want:
+  path: /api/getsecret?id=testkey&version=1
+  err: failed to get response (wrong type
+  resultmap:
+    thesecret: secret-value
+    alsosecret: another-value
+`
+
+func TestWebhookGetSecret(t *testing.T) {
+	ydec := yaml.NewDecoder(bytes.NewReader([]byte(testCases)))
+	for {
+		var tc testCase
+		if err := ydec.Decode(&tc); err != nil {
+			if !errors.Is(err, io.EOF) {
+				t.Errorf("testcase decode error %w", err)
+			}
+			break
+		}
+		runTestCase(tc, t)
+	}
+}
+
+func testCaseServer(tc testCase, t *testing.T) *httptest.Server {
+	// Start a new server for every test case because the server wants to check the expected api path
+	return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
+		if tc.Want.Path != "" && req.URL.String() != tc.Want.Path {
+			t.Errorf("%s: unexpected api path: %s, expected %s", tc.Case, req.URL.String(), tc.Want.Path)
+		}
+		if tc.Args.StatusCode != 0 {
+			rw.WriteHeader(tc.Args.StatusCode)
+		}
+		rw.Write([]byte(tc.Args.Response))
+	}))
+}
+
+func parseTimeout(timeout string) (*metav1.Duration, error) {
+	if timeout == "" {
+		return nil, nil
+	}
+	dur, err := time.ParseDuration(timeout)
+	if err != nil {
+		return nil, err
+	}
+	return &metav1.Duration{Duration: dur}, nil
+}
+
+func runTestCase(tc testCase, t *testing.T) {
+	ts := testCaseServer(tc, t)
+	defer ts.Close()
+
+	testStore := makeClusterSecretStore(ts.URL, tc.Args)
+	var err error
+	timeout, err := parseTimeout(tc.Args.Timeout)
+	if err != nil {
+		t.Errorf("%s: error parsing timeout '%s': %s", tc.Case, tc.Args.Timeout, err.Error())
+		return
+	}
+	testStore.Spec.Provider.Webhook.Timeout = timeout
+	testProv := &Provider{}
+	client, err := testProv.NewClient(context.Background(), testStore, nil, "testnamespace")
+	if err != nil {
+		t.Errorf("%s: error creating client: %s", tc.Case, err.Error())
+		return
+	}
+
+	if tc.Want.ResultMap != nil {
+		testGetSecretMap(tc, t, client)
+	} else {
+		testGetSecret(tc, t, client)
+	}
+}
+
+func testGetSecretMap(tc testCase, t *testing.T, client provider.SecretsClient) {
+	testRef := esv1alpha1.ExternalSecretDataRemoteRef{
+		Key:     tc.Args.Key,
+		Version: tc.Args.Version,
+	}
+	secretmap, err := client.GetSecretMap(context.Background(), testRef)
+	errStr := ""
+	if err != nil {
+		errStr = err.Error()
+	}
+	if (tc.Want.Err == "") != (errStr == "") || !strings.Contains(errStr, tc.Want.Err) {
+		t.Errorf("%s: unexpected error: '%s' (expected '%s')", tc.Case, errStr, tc.Want.Err)
+	}
+	if err == nil {
+		for wantkey, wantval := range tc.Want.ResultMap {
+			gotval, ok := secretmap[wantkey]
+			if !ok {
+				t.Errorf("%s: unexpected response: wanted key '%s' not found", tc.Case, wantkey)
+			} else if string(gotval) != wantval {
+				t.Errorf("%s: unexpected response: key '%s' = '%s' (expected '%s')", tc.Case, wantkey, wantval, gotval)
+			}
+		}
+	}
+}
+
+func testGetSecret(tc testCase, t *testing.T, client provider.SecretsClient) {
+	testRef := esv1alpha1.ExternalSecretDataRemoteRef{
+		Key:     tc.Args.Key,
+		Version: tc.Args.Version,
+	}
+	secret, err := client.GetSecret(context.Background(), testRef)
+	errStr := ""
+	if err != nil {
+		errStr = err.Error()
+	}
+	if !strings.Contains(errStr, tc.Want.Err) {
+		t.Errorf("%s: unexpected error: '%s' (expected '%s')", tc.Case, errStr, tc.Want.Err)
+	}
+	if err == nil && string(secret) != tc.Want.Result {
+		t.Errorf("%s: unexpected response: '%s' (expected '%s')", tc.Case, secret, tc.Want.Result)
+	}
+}
+
+func makeClusterSecretStore(url string, args args) *esv1alpha1.ClusterSecretStore {
+	store := &esv1alpha1.ClusterSecretStore{
+		TypeMeta: metav1.TypeMeta{
+			Kind: "ClusterSecretStore",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "wehbook-store",
+			Namespace: "default",
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				Webhook: &esv1alpha1.WebhookProvider{
+					URL:  url + args.URL,
+					Body: args.Body,
+					Headers: map[string]string{
+						"Content-Type": "application.json",
+						"X-SecretKey":  "{{ .remoteRef.key }}",
+					},
+					Result: esv1alpha1.WebhookResult{
+						JSONPath: args.JSONPath,
+					},
+				},
+			},
+		},
+	}
+	return store
+}

+ 1 - 1
pkg/provider/yandex/lockbox/client/client.go

@@ -23,7 +23,7 @@ import (
 
 // Creates Lockbox clients and Yandex.Cloud IAM tokens.
 type YandexCloudCreator interface {
-	CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (LockboxClient, error)
+	CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (LockboxClient, error)
 	CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*IamToken, error)
 	Now() time.Time
 }

+ 1 - 1
pkg/provider/yandex/lockbox/client/fake/fake.go

@@ -31,7 +31,7 @@ type YandexCloudCreator struct {
 	Backend *LockboxBackend
 }
 
-func (c *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+func (c *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (client.LockboxClient, error) {
 	return &LockboxClient{c.Backend}, nil
 }
 

+ 15 - 2
pkg/provider/yandex/lockbox/client/grpc/grpc.go

@@ -16,6 +16,8 @@ package grpc
 import (
 	"context"
 	"crypto/tls"
+	"crypto/x509"
+	"errors"
 	"time"
 
 	"github.com/yandex-cloud/go-genproto/yandex/cloud/endpoint"
@@ -33,7 +35,7 @@ import (
 type YandexCloudCreator struct {
 }
 
-func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (client.LockboxClient, error) {
 	sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
 	if err != nil {
 		return nil, err
@@ -51,8 +53,19 @@ func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoi
 		return nil, err
 	}
 
+	tlsConfig := tls.Config{MinVersion: tls.VersionTLS12}
+
+	if caCertificate != nil {
+		caCertPool := x509.NewCertPool()
+		ok := caCertPool.AppendCertsFromPEM(caCertificate)
+		if !ok {
+			return nil, errors.New("unable to read certificate from PEM file")
+		}
+		tlsConfig.RootCAs = caCertPool
+	}
+
 	conn, err := grpc.Dial(payloadAPIEndpoint.Address,
-		grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})),
+		grpc.WithTransportCredentials(credentials.NewTLS(&tlsConfig)),
 		grpc.WithKeepaliveParams(keepalive.ClientParameters{
 			Time:                time.Second * 30,
 			Timeout:             time.Second * 10,

+ 30 - 3
pkg/provider/yandex/lockbox/lockbox.go

@@ -107,7 +107,34 @@ func (p *lockboxProvider) NewClient(ctx context.Context, store esv1alpha1.Generi
 		return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
 	}
 
-	lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
+	var caCertificateData []byte
+
+	if storeSpecYandexLockbox.CAProvider != nil {
+		certObjectKey := types.NamespacedName{
+			Name:      storeSpecYandexLockbox.CAProvider.Certificate.Name,
+			Namespace: namespace,
+		}
+
+		if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
+			if storeSpecYandexLockbox.CAProvider.Certificate.Namespace == nil {
+				return nil, fmt.Errorf("invalid ClusterSecretStore: missing CA certificate Namespace")
+			}
+			certObjectKey.Namespace = *storeSpecYandexLockbox.CAProvider.Certificate.Namespace
+		}
+
+		caCertificateSecret := &corev1.Secret{}
+		err := kube.Get(ctx, certObjectKey, caCertificateSecret)
+		if err != nil {
+			return nil, fmt.Errorf("could not fetch CA certificate secret: %w", err)
+		}
+
+		caCertificateData = caCertificateSecret.Data[storeSpecYandexLockbox.CAProvider.Certificate.Key]
+		if (caCertificateData == nil) || (len(caCertificateData) == 0) {
+			return nil, fmt.Errorf("missing CA Certificate")
+		}
+	}
+
+	lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey, caCertificateData)
 	if err != nil {
 		return nil, fmt.Errorf("failed to create Yandex Lockbox client: %w", err)
 	}
@@ -120,14 +147,14 @@ func (p *lockboxProvider) NewClient(ctx context.Context, store esv1alpha1.Generi
 	return &lockboxSecretsClient{lockboxClient, iamToken.Token}, nil
 }
 
-func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
+func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (client.LockboxClient, error) {
 	p.lockboxClientMapMutex.Lock()
 	defer p.lockboxClientMapMutex.Unlock()
 
 	if _, ok := p.lockboxClientMap[apiEndpoint]; !ok {
 		log.Info("creating LockboxClient", "apiEndpoint", apiEndpoint)
 
-		lockboxClient, err := p.yandexCloudCreator.CreateLockboxClient(ctx, apiEndpoint, authorizedKey)
+		lockboxClient, err := p.yandexCloudCreator.CreateLockboxClient(ctx, apiEndpoint, authorizedKey, caCertificate)
 		if err != nil {
 			return nil, err
 		}

+ 38 - 0
pkg/provider/yandex/lockbox/lockbox_test.go

@@ -15,8 +15,11 @@ package lockbox
 
 import (
 	"context"
+	"crypto/x509"
+	"crypto/x509/pkix"
 	b64 "encoding/base64"
 	"encoding/json"
+	"math/big"
 	"testing"
 	"time"
 
@@ -83,6 +86,21 @@ func TestNewClient(t *testing.T) {
 
 	err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, newFakeAuthorizedKey())
 	tassert.Nil(t, err)
+
+	const caCertificateSecretName = "caCertificateSecretName"
+	const caCertificateSecretKey = "caCertificateSecretKey"
+	store.Spec.Provider.YandexLockbox.CAProvider = &esv1alpha1.YandexLockboxCAProvider{
+		Certificate: esmeta.SecretKeySelector{
+			Key:  caCertificateSecretKey,
+			Name: caCertificateSecretName,
+		},
+	}
+	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
+	tassert.EqualError(t, err, "could not fetch CA certificate secret: secrets \"caCertificateSecretName\" not found")
+	tassert.Nil(t, secretClient)
+
+	err = createK8sSecret(ctx, k8sClient, namespace, caCertificateSecretName, caCertificateSecretKey, newFakeCACertificate())
+	tassert.Nil(t, err)
 	secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
 	tassert.EqualError(t, err, "failed to create Yandex Lockbox client: private key parsing failed: Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key")
 	tassert.Nil(t, secretClient)
@@ -653,6 +671,26 @@ func newFakeAuthorizedKey() *iamkey.Key {
 	}
 }
 
+func newFakeCACertificate() []byte {
+	cert := x509.Certificate{
+		SerialNumber: big.NewInt(2019),
+		Subject: pkix.Name{
+			Organization:  []string{"Company, INC."},
+			Country:       []string{"US"},
+			Locality:      []string{"San Francisco"},
+			StreetAddress: []string{"Golden Gate Bridge"},
+			PostalCode:    []string{"94016"},
+		},
+		NotBefore:             time.Now(),
+		NotAfter:              time.Now().AddDate(10, 0, 0),
+		IsCA:                  true,
+		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
+		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
+		BasicConstraintsValid: true,
+	}
+	return cert.Raw
+}
+
 func textEntry(key, value string) *lockbox.Payload_Entry {
 	return &lockbox.Payload_Entry{
 		Key: key,

+ 5 - 0
pkg/template/template.go

@@ -51,6 +51,11 @@ var tplFuncs = tpl.FuncMap{
 	"lower":    strings.ToLower,
 }
 
+// So other templating calls can use the same extra functions.
+func FuncMap() tpl.FuncMap {
+	return tplFuncs
+}
+
 const (
 	errParse                = "unable to parse template at key %s: %s"
 	errExecute              = "unable to execute template at key %s: %s"

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff