Browse Source

:sparkles: Adds Keyvault PushSecret (#1883)

* Adds Keyvault PushSecret

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
Gustavo Fernandes de Carvalho 3 years ago
parent
commit
833658699d

+ 1 - 1
docs/introduction/stability-support.md

@@ -50,7 +50,7 @@ The following table show the support for features across different providers.
 | AWS Parameter Store       |      x       |      x       |                      |            x            |        x         |             |
 | AWS Parameter Store       |      x       |      x       |                      |            x            |        x         |             |
 | Hashicorp Vault           |      x       |      x       |                      |                         |        x         |             |
 | Hashicorp Vault           |      x       |      x       |                      |                         |        x         |             |
 | GCP Secret Manager        |      x       |      x       |                      |            x            |        x         |             |
 | GCP Secret Manager        |      x       |      x       |                      |            x            |        x         |             |
-| Azure Keyvault            |      x       |      x       |          x           |            x            |        x         |             |
+| Azure Keyvault            |      x       |      x       |          x           |            x            |        x         |     x        |
 | Kubernetes                |      x       |      x       |                      |            x            |        x         |             |
 | Kubernetes                |      x       |      x       |                      |            x            |        x         |             |
 | IBM Cloud Secrets Manager |              |              |                      |                         |        x         |             |
 | IBM Cloud Secrets Manager |              |              |                      |                         |        x         |             |
 | Yandex Lockbox            |              |              |                      |                         |        x         |             |
 | Yandex Lockbox            |              |              |                      |                         |        x         |             |

+ 30 - 0
docs/provider/azure-key-vault.md

@@ -137,3 +137,33 @@ To get a PKCS#12 certificate from Azure Key Vault and inject it as a `Kind=Secre
 ```yaml
 ```yaml
 {% include 'azkv-pkcs12-cert-external-secret.yaml' %}
 {% include 'azkv-pkcs12-cert-external-secret.yaml' %}
 ```
 ```
+
+### Creating a PushSecret
+You can push secrets to Keyvault into the different `secret`, `key` and `certificate` APIs.
+
+#### Pushing to a Secret
+Pushing to a Secret requires no previous setup. with the secret available in kubernetes, you can simply refer it to a PushSecret object to have it created on Azure Keyvault:
+```yaml
+{% include 'azkv-pushsecret-secret.yaml' %}
+```
+!!! note
+      In order to create a PushSecret targeting keys, `CreateSecret` and `DeleteSecret` actions must be granted to the Service Principal/Identity configured on the SecretStore.
+
+#### Pushing to a Key
+The first step is to generate a valid Private Key. Supported Formats include `PRIVATE KEY`, `RSA PRIVATE KEY` AND `EC PRIVATE KEY` (EC/PKCS1/PKCS8 types). After uploading your key to a Kubernetes Secret, the next step is to create a PushSecret manifest with the following configuration:
+
+```yaml
+{% include 'azkv-pushsecret-key.yaml' %}
+```
+
+!!! note
+      In order to create a PushSecret targeting keys, `ImportKey` and `DeleteKey` actions must be granted to the Service Principal/Identity configured on the SecretStore.
+#### Pushing to a Certificate
+The first step is to generate a valid P12 certificate. Currently, only PKCS1/PKCS8 types are supported. Currently only passwordless P12 certificates are supported.
+
+After uploading your P12 certificate to a Kubernetes Secret, the next step is to create a PushSecret manifest with the following configuration
+```yaml
+{% include 'azkv-pushsecret-certificate.yaml' %}
+```
+!!! note
+       In order to create a PushSecret targeting keys, `ImportCertificate` and `DeleteCertificate` actions must be granted to the Service Principal/Identity configured on the SecretStore.

File diff suppressed because it is too large
+ 36 - 0
docs/snippets/azkv-pushsecret-certificate.yaml


File diff suppressed because it is too large
+ 26 - 0
docs/snippets/azkv-pushsecret-key.yaml


+ 26 - 0
docs/snippets/azkv-pushsecret-secret.yaml

@@ -0,0 +1,26 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: source-secret
+stringData:
+  source-key: "my-secret"
+---
+apiVersion: external-secrets.io/v1alpha1
+kind: PushSecret
+metadata:
+  name: pushsecret-example
+  namespace: default
+spec:
+  refreshInterval: 10s # Refresh interval for which push secret will reconcile
+  deletionPolicy: Delete
+  secretStoreRefs: # A list of secret stores to push secrets to
+    - name: azure-store
+      kind: SecretStore
+  selector:
+    secret:
+      name: source-secret # Source Kubernetes secret to be pushed
+  data:
+    - match:
+        secretKey: source-key # Source Kubernetes secret key containing the secret
+        remoteRef:
+          remoteKey: my-azkv-secret-name 

+ 2 - 2
docs/snippets/azkv-secret-store.yaml

@@ -7,9 +7,9 @@ spec:
     # provider type: azure keyvault
     # provider type: azure keyvault
     azurekv:
     azurekv:
       # azure tenant ID, see: https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-how-to-find-tenant
       # azure tenant ID, see: https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-how-to-find-tenant
-      tenantId: "d3bc2180-xxxx-xxxx-xxxx-154105743342"
+      tenantId: "2ed1d494-6c5a-4c5d-aa24-479446fb844d"
       # URL of your vault instance, see: https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates
       # 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"
+      vaultUrl: "https://kvtestpushsecret.vault.azure.net"
       authSecretRef:
       authSecretRef:
         # points to the secret that contains
         # points to the secret that contains
         # the azure service principal credentials
         # the azure service principal credentials

+ 78 - 0
pkg/provider/azure/keyvault/fake/fake.go

@@ -24,6 +24,12 @@ type AzureMockClient struct {
 	getSecret          func(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error)
 	getSecret          func(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error)
 	getSecretsComplete func(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.SecretListResultIterator, err error)
 	getSecretsComplete func(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.SecretListResultIterator, err error)
 	getCertificate     func(ctx context.Context, vaultBaseURL string, certificateName string, certificateVersion string) (result keyvault.CertificateBundle, err error)
 	getCertificate     func(ctx context.Context, vaultBaseURL string, certificateName string, certificateVersion string) (result keyvault.CertificateBundle, err error)
+	setSecret          func(ctx context.Context, vaultBaseURL string, secretName string, parameters keyvault.SecretSetParameters) (result keyvault.SecretBundle, err error)
+	importCertificate  func(ctx context.Context, vaultBaseURL string, certificateName string, parameters keyvault.CertificateImportParameters) (result keyvault.CertificateBundle, err error)
+	importKey          func(ctx context.Context, vaultBaseURL string, keyName string, parameters keyvault.KeyImportParameters) (result keyvault.KeyBundle, err error)
+	deleteCertificate  func(ctx context.Context, vaultBaseURL string, certificateName string) (result keyvault.DeletedCertificateBundle, err error)
+	deleteKey          func(ctx context.Context, vaultBaseURL string, keyName string) (result keyvault.DeletedKeyBundle, err error)
+	deleteSecret       func(ctx context.Context, vaultBaseURL string, secretName string) (result keyvault.DeletedSecretBundle, err error)
 }
 }
 
 
 func (mc *AzureMockClient) GetSecret(ctx context.Context, vaultBaseURL, secretName, secretVersion string) (result keyvault.SecretBundle, err error) {
 func (mc *AzureMockClient) GetSecret(ctx context.Context, vaultBaseURL, secretName, secretVersion string) (result keyvault.SecretBundle, err error) {
@@ -42,6 +48,30 @@ func (mc *AzureMockClient) GetSecretsComplete(ctx context.Context, vaultBaseURL
 	return mc.getSecretsComplete(ctx, vaultBaseURL, maxresults)
 	return mc.getSecretsComplete(ctx, vaultBaseURL, maxresults)
 }
 }
 
 
+func (mc *AzureMockClient) SetSecret(ctx context.Context, vaultBaseURL, secretName string, parameters keyvault.SecretSetParameters) (keyvault.SecretBundle, error) {
+	return mc.setSecret(ctx, vaultBaseURL, secretName, parameters)
+}
+
+func (mc *AzureMockClient) ImportCertificate(ctx context.Context, vaultBaseURL, certificateName string, parameters keyvault.CertificateImportParameters) (result keyvault.CertificateBundle, err error) {
+	return mc.importCertificate(ctx, vaultBaseURL, certificateName, parameters)
+}
+
+func (mc *AzureMockClient) ImportKey(ctx context.Context, vaultBaseURL, keyName string, parameters keyvault.KeyImportParameters) (result keyvault.KeyBundle, err error) {
+	return mc.importKey(ctx, vaultBaseURL, keyName, parameters)
+}
+
+func (mc *AzureMockClient) DeleteKey(ctx context.Context, vaultBaseURL, keyName string) (keyvault.DeletedKeyBundle, error) {
+	return mc.deleteKey(ctx, vaultBaseURL, keyName)
+}
+
+func (mc *AzureMockClient) DeleteSecret(ctx context.Context, vaultBaseURL, secretName string) (keyvault.DeletedSecretBundle, error) {
+	return mc.deleteSecret(ctx, vaultBaseURL, secretName)
+}
+
+func (mc *AzureMockClient) DeleteCertificate(ctx context.Context, vaultBaseURL, certificateName string) (keyvault.DeletedCertificateBundle, error) {
+	return mc.deleteCertificate(ctx, vaultBaseURL, certificateName)
+}
+
 func (mc *AzureMockClient) WithValue(serviceURL, secretName, secretVersion string, apiOutput keyvault.SecretBundle, err error) {
 func (mc *AzureMockClient) WithValue(serviceURL, secretName, secretVersion string, apiOutput keyvault.SecretBundle, err error) {
 	if mc != nil {
 	if mc != nil {
 		mc.getSecret = func(ctx context.Context, serviceURL, secretName, secretVersion string) (result keyvault.SecretBundle, retErr error) {
 		mc.getSecret = func(ctx context.Context, serviceURL, secretName, secretVersion string) (result keyvault.SecretBundle, retErr error) {
@@ -66,6 +96,54 @@ func (mc *AzureMockClient) WithCertificate(serviceURL, secretName, secretVersion
 	}
 	}
 }
 }
 
 
+func (mc *AzureMockClient) WithImportCertificate(apiOutput keyvault.CertificateBundle, err error) {
+	if mc != nil {
+		mc.importCertificate = func(ctx context.Context, vaultBaseURL string, certificateName string, parameters keyvault.CertificateImportParameters) (keyvault.CertificateBundle, error) {
+			return apiOutput, err
+		}
+	}
+}
+
+func (mc *AzureMockClient) WithImportKey(output keyvault.KeyBundle, err error) {
+	if mc != nil {
+		mc.importKey = func(ctx context.Context, vaultBaseURL string, keyName string, parameters keyvault.KeyImportParameters) (keyvault.KeyBundle, error) {
+			return output, err
+		}
+	}
+}
+
+func (mc *AzureMockClient) WithSetSecret(output keyvault.SecretBundle, err error) {
+	if mc != nil {
+		mc.setSecret = func(ctx context.Context, vaultBaseURL string, secretName string, parameters keyvault.SecretSetParameters) (keyvault.SecretBundle, error) {
+			return output, err
+		}
+	}
+}
+
+func (mc *AzureMockClient) WithDeleteSecret(output keyvault.DeletedSecretBundle, err error) {
+	if mc != nil {
+		mc.deleteSecret = func(ctx context.Context, vaultBaseURL string, secretName string) (keyvault.DeletedSecretBundle, error) {
+			return output, err
+		}
+	}
+}
+
+func (mc *AzureMockClient) WithDeleteCertificate(output keyvault.DeletedCertificateBundle, err error) {
+	if mc != nil {
+		mc.deleteCertificate = func(ctx context.Context, vaultBaseURL string, certificateName string) (keyvault.DeletedCertificateBundle, error) {
+			return output, err
+		}
+	}
+}
+
+func (mc *AzureMockClient) WithDeleteKey(output keyvault.DeletedKeyBundle, err error) {
+	if mc != nil {
+		mc.deleteKey = func(ctx context.Context, vaultBaseURL string, keyName string) (keyvault.DeletedKeyBundle, error) {
+			return output, err
+		}
+	}
+}
+
 func (mc *AzureMockClient) WithList(serviceURL string, apiOutput keyvault.SecretListResultIterator, err error) {
 func (mc *AzureMockClient) WithList(serviceURL string, apiOutput keyvault.SecretListResultIterator, err error) {
 	if mc != nil {
 	if mc != nil {
 		mc.getSecretsComplete = func(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.SecretListResultIterator, err error) {
 		mc.getSecretsComplete = func(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.SecretListResultIterator, err error) {

+ 265 - 3
pkg/provider/azure/keyvault/keyvault.go

@@ -16,7 +16,10 @@ package keyvault
 
 
 import (
 import (
 	"context"
 	"context"
+	"crypto/x509"
+	b64 "encoding/base64"
 	"encoding/json"
 	"encoding/json"
+	"encoding/pem"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"os"
 	"os"
@@ -30,13 +33,17 @@ import (
 	"github.com/Azure/go-autorest/autorest/azure"
 	"github.com/Azure/go-autorest/autorest/azure"
 	kvauth "github.com/Azure/go-autorest/autorest/azure/auth"
 	kvauth "github.com/Azure/go-autorest/autorest/azure/auth"
 	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
 	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
+	"github.com/lestrrat-go/jwx/jwk"
 	"github.com/tidwall/gjson"
 	"github.com/tidwall/gjson"
+	"golang.org/x/crypto/pkcs12"
+	"golang.org/x/crypto/sha3"
 	authv1 "k8s.io/api/authentication/v1"
 	authv1 "k8s.io/api/authentication/v1"
 	corev1 "k8s.io/api/core/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/kubernetes"
 	kcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
 	kcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
+	"k8s.io/utils/pointer"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 	ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
 
 
@@ -52,6 +59,7 @@ const (
 	AzureDefaultAudience = "api://AzureADTokenExchange"
 	AzureDefaultAudience = "api://AzureADTokenExchange"
 	AnnotationClientID   = "azure.workload.identity/client-id"
 	AnnotationClientID   = "azure.workload.identity/client-id"
 	AnnotationTenantID   = "azure.workload.identity/tenant-id"
 	AnnotationTenantID   = "azure.workload.identity/tenant-id"
+	managerLabel         = "external-secrets"
 
 
 	errUnexpectedStoreSpec   = "unexpected store spec"
 	errUnexpectedStoreSpec   = "unexpected store spec"
 	errMissingAuthType       = "cannot initialize Azure Client: no valid authType was specified"
 	errMissingAuthType       = "cannot initialize Azure Client: no valid authType was specified"
@@ -90,6 +98,12 @@ type SecretClient interface {
 	GetSecret(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error)
 	GetSecret(ctx context.Context, vaultBaseURL string, secretName string, secretVersion string) (result keyvault.SecretBundle, err error)
 	GetSecretsComplete(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.SecretListResultIterator, err error)
 	GetSecretsComplete(ctx context.Context, vaultBaseURL string, maxresults *int32) (result keyvault.SecretListResultIterator, err error)
 	GetCertificate(ctx context.Context, vaultBaseURL string, certificateName string, certificateVersion string) (result keyvault.CertificateBundle, err error)
 	GetCertificate(ctx context.Context, vaultBaseURL string, certificateName string, certificateVersion string) (result keyvault.CertificateBundle, err error)
+	SetSecret(ctx context.Context, vaultBaseURL string, secretName string, parameters keyvault.SecretSetParameters) (result keyvault.SecretBundle, err error)
+	ImportKey(ctx context.Context, vaultBaseURL string, keyName string, parameters keyvault.KeyImportParameters) (result keyvault.KeyBundle, err error)
+	ImportCertificate(ctx context.Context, vaultBaseURL string, certificateName string, parameters keyvault.CertificateImportParameters) (result keyvault.CertificateBundle, err error)
+	DeleteCertificate(ctx context.Context, vaultBaseURL string, certificateName string) (result keyvault.DeletedCertificateBundle, err error)
+	DeleteKey(ctx context.Context, vaultBaseURL string, keyName string) (result keyvault.DeletedKeyBundle, err error)
+	DeleteSecret(ctx context.Context, vaultBaseURL string, secretName string) (result keyvault.DeletedSecretBundle, err error)
 }
 }
 
 
 type Azure struct {
 type Azure struct {
@@ -201,13 +215,261 @@ func (a *Azure) ValidateStore(store esv1beta1.GenericStore) error {
 	return nil
 	return nil
 }
 }
 
 
+func canDelete(tags map[string]*string, err error) (bool, error) {
+	aerr := &autorest.DetailedError{}
+	conv := errors.As(err, aerr)
+	if err != nil && !conv {
+		return false, fmt.Errorf("could not parse error: %w", err)
+	}
+	if conv && aerr.StatusCode != 404 { // Secret is already deleted, nothing to do.
+		return false, fmt.Errorf("unexpected api error: %w", err)
+	}
+	if aerr.StatusCode == 404 {
+		return false, nil
+	}
+	manager, ok := tags["managed-by"]
+	if !ok || manager == nil || *manager != managerLabel {
+		return false, fmt.Errorf("not managed by external-secrets")
+	}
+	return true, nil
+}
+
+func (a *Azure) deleteKeyVaultKey(ctx context.Context, keyName string) error {
+	value, err := a.baseClient.GetKey(ctx, *a.provider.VaultURL, keyName, "")
+	ok, err := canDelete(value.Tags, err)
+	if err != nil {
+		return fmt.Errorf("error getting key %v: %w", keyName, err)
+	}
+	if ok {
+		_, err = a.baseClient.DeleteKey(ctx, *a.provider.VaultURL, keyName)
+		if err != nil {
+			return fmt.Errorf("error deleting key %v: %w", keyName, err)
+		}
+	}
+	return nil
+}
+
+func (a *Azure) deleteKeyVaultSecret(ctx context.Context, secretName string) error {
+	value, err := a.baseClient.GetSecret(ctx, *a.provider.VaultURL, secretName, "")
+	ok, err := canDelete(value.Tags, err)
+	if err != nil {
+		return fmt.Errorf("error getting secret %v: %w", secretName, err)
+	}
+	if ok {
+		_, err = a.baseClient.DeleteSecret(ctx, *a.provider.VaultURL, secretName)
+		if err != nil {
+			return fmt.Errorf("error deleting secret %v: %w", secretName, err)
+		}
+	}
+	return nil
+}
+
+func (a *Azure) deleteKeyVaultCertificate(ctx context.Context, certName string) error {
+	value, err := a.baseClient.GetCertificate(ctx, *a.provider.VaultURL, certName, "")
+	ok, err := canDelete(value.Tags, err)
+	if err != nil {
+		return fmt.Errorf("error getting certificate %v: %w", certName, err)
+	}
+	if ok {
+		_, err = a.baseClient.DeleteCertificate(ctx, *a.provider.VaultURL, certName)
+		if err != nil {
+			return fmt.Errorf("error deleting certificate %v: %w", certName, err)
+		}
+	}
+	return nil
+}
+
 func (a *Azure) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
 func (a *Azure) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushRemoteRef) error {
-	return fmt.Errorf("not implemented")
+	objectType, secretName := getObjType(esv1beta1.ExternalSecretDataRemoteRef{Key: remoteRef.GetRemoteKey()})
+	switch objectType {
+	case defaultObjType:
+		return a.deleteKeyVaultSecret(ctx, secretName)
+	case objectTypeCert:
+		return a.deleteKeyVaultCertificate(ctx, secretName)
+	case objectTypeKey:
+		return a.deleteKeyVaultKey(ctx, secretName)
+	default:
+		return fmt.Errorf("secret type '%v' is not supported", objectType)
+	}
+}
+
+func getCertificateFromValue(value []byte) (*x509.Certificate, error) {
+	_, localCert, err := pkcs12.Decode(value, "")
+	if err != nil {
+		pemBlock, _ := pem.Decode(value)
+		if pemBlock == nil {
+			return x509.ParseCertificate(value)
+		}
+		return x509.ParseCertificate(pemBlock.Bytes)
+	}
+	return localCert, err
+}
+
+func getKeyFromValue(value []byte) (interface{}, error) {
+	val := value
+	pemBlock, _ := pem.Decode(value)
+	// if a private key regular expression doesn't match, we should consider this key to be symmetric
+	if pemBlock == nil {
+		return val, nil
+	}
+	val = pemBlock.Bytes
+	switch pemBlock.Type {
+	case "PRIVATE KEY":
+		return x509.ParsePKCS8PrivateKey(val)
+	case "RSA PRIVATE KEY":
+		return x509.ParsePKCS1PrivateKey(val)
+	case "EC PRIVATE KEY":
+		return x509.ParseECPrivateKey(val)
+	default:
+		return nil, fmt.Errorf("key type %v is not supported", pemBlock.Type)
+	}
 }
 }
 
 
-// Not Implemented PushSecret.
+func canCreate(tags map[string]*string, err error) (bool, error) {
+	aerr := &autorest.DetailedError{}
+	conv := errors.As(err, aerr)
+	if err != nil && !conv {
+		return false, fmt.Errorf("could not parse error: %w", err)
+	}
+	if conv && aerr.StatusCode != 404 {
+		return false, fmt.Errorf("unexpected api error: %w", err)
+	}
+	if err == nil {
+		manager, ok := tags["managed-by"]
+		if !ok || manager == nil || *manager != managerLabel {
+			return false, fmt.Errorf("not managed by external-secrets")
+		}
+	}
+	return true, nil
+}
+
+func (a *Azure) setKeyVaultSecret(ctx context.Context, secretName string, value []byte) error {
+	secret, err := a.baseClient.GetSecret(ctx, *a.provider.VaultURL, secretName, "")
+	ok, err := canCreate(secret.Tags, err)
+	if err != nil {
+		return fmt.Errorf("cannot get secret %v: %w", secretName, err)
+	}
+	if !ok {
+		return nil
+	}
+	val := string(value)
+	if secret.Value != nil && val == *secret.Value {
+		return nil
+	}
+	secretParams := keyvault.SecretSetParameters{
+		Value: &val,
+		Tags: map[string]*string{
+			"managed-by": pointer.String(managerLabel),
+		},
+		SecretAttributes: &keyvault.SecretAttributes{
+			Enabled: pointer.Bool(true),
+		},
+	}
+	_, err = a.baseClient.SetSecret(ctx, *a.provider.VaultURL, secretName, secretParams)
+	if err != nil {
+		return fmt.Errorf("could not set secret %v: %w", secretName, err)
+	}
+	return nil
+}
+
+func (a *Azure) setKeyVaultCertificate(ctx context.Context, secretName string, value []byte) error {
+	val := b64.StdEncoding.EncodeToString(value)
+	localCert, err := getCertificateFromValue(value)
+	if err != nil {
+		return fmt.Errorf("value from secret is not a valid certificate: %w", err)
+	}
+	cert, err := a.baseClient.GetCertificate(ctx, *a.provider.VaultURL, secretName, "")
+	ok, err := canCreate(cert.Tags, err)
+	if err != nil {
+		return fmt.Errorf("cannot get certificate %v: %w", secretName, err)
+	}
+	if !ok {
+		return nil
+	}
+	b512 := sha3.Sum512(localCert.Raw)
+	if cert.Cer != nil && b512 == sha3.Sum512(*cert.Cer) {
+		return nil
+	}
+	params := keyvault.CertificateImportParameters{
+		Base64EncodedCertificate: &val,
+		Tags: map[string]*string{
+			"managed-by": pointer.String(managerLabel),
+		},
+	}
+	_, err = a.baseClient.ImportCertificate(ctx, *a.provider.VaultURL, secretName, params)
+	if err != nil {
+		return fmt.Errorf("could not import certificate %v: %w", secretName, err)
+	}
+	return nil
+}
+func equalKeys(newKey, oldKey keyvault.JSONWebKey) bool {
+	// checks for everything except KeyID and KeyOps
+	rsaCheck := newKey.E != nil && oldKey.E != nil && *newKey.E == *oldKey.E &&
+		newKey.N != nil && oldKey.N != nil && *newKey.N == *oldKey.N
+
+	symmetricCheck := newKey.Crv == oldKey.Crv &&
+		newKey.T != nil && oldKey.T != nil && *newKey.T == *oldKey.T &&
+		newKey.X != nil && oldKey.X != nil && *newKey.X == *oldKey.X &&
+		newKey.Y != nil && oldKey.Y != nil && *newKey.Y == *oldKey.Y
+
+	return newKey.Kty == oldKey.Kty && (rsaCheck || symmetricCheck)
+}
+func (a *Azure) setKeyVaultKey(ctx context.Context, secretName string, value []byte) error {
+	key, err := getKeyFromValue(value)
+	if err != nil {
+		return fmt.Errorf("could not load private key %v: %w", secretName, err)
+	}
+	jwKey, err := jwk.New(key)
+	if err != nil {
+		return fmt.Errorf("failed to generate a JWK from secret %v content: %w", secretName, err)
+	}
+	buf, err := json.Marshal(jwKey)
+	if err != nil {
+		return fmt.Errorf("error parsing key: %w", err)
+	}
+	azkey := keyvault.JSONWebKey{}
+	err = json.Unmarshal(buf, &azkey)
+	if err != nil {
+		return fmt.Errorf("error unmarshalling key: %w", err)
+	}
+	keyFromVault, err := a.baseClient.GetKey(ctx, *a.provider.VaultURL, secretName, "")
+	ok, err := canCreate(keyFromVault.Tags, err)
+	if err != nil {
+		return fmt.Errorf("cannot get key %v: %w", secretName, err)
+	}
+	if !ok {
+		return nil
+	}
+	if keyFromVault.Key != nil && equalKeys(azkey, *keyFromVault.Key) {
+		return nil
+	}
+	params := keyvault.KeyImportParameters{
+		Key:           &azkey,
+		KeyAttributes: &keyvault.KeyAttributes{},
+		Tags: map[string]*string{
+			"managed-by": pointer.String(managerLabel),
+		},
+	}
+	_, err = a.baseClient.ImportKey(ctx, *a.provider.VaultURL, secretName, params)
+	if err != nil {
+		return fmt.Errorf("could not import key %v: %w", secretName, err)
+	}
+	return nil
+}
+
+// PushSecret stores secrets into a Key vault instance.
 func (a *Azure) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
 func (a *Azure) PushSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
-	return fmt.Errorf("not implemented")
+	objectType, secretName := getObjType(esv1beta1.ExternalSecretDataRemoteRef{Key: remoteRef.GetRemoteKey()})
+	switch objectType {
+	case defaultObjType:
+		return a.setKeyVaultSecret(ctx, secretName, value)
+	case objectTypeCert:
+		return a.setKeyVaultCertificate(ctx, secretName, value)
+	case objectTypeKey:
+		return a.setKeyVaultKey(ctx, secretName, value)
+	default:
+		return fmt.Errorf("secret type %v not supported", objectType)
+	}
 }
 }
 
 
 // Implements store.Client.GetAllSecrets Interface.
 // Implements store.Client.GetAllSecrets Interface.

File diff suppressed because it is too large
+ 632 - 12
pkg/provider/azure/keyvault/keyvault_test.go