/* 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 keyvault import ( "context" "encoding/json" "fmt" "path" "strings" "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault" kvauth "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/tidwall/gjson" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" smmeta "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" ) // Provider satisfies the provider interface. type Provider struct{} // interface to keyvault.BaseClient. type SecretClient interface { GetKey(ctx context.Context, vaultBaseURL string, keyName string, keyVersion string) (result keyvault.KeyBundle, 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) GetCertificate(ctx context.Context, vaultBaseURL string, certificateName string, certificateVersion string) (result keyvault.CertificateBundle, err error) } // Azure satisfies the provider.SecretsClient interface. type Azure struct { kube client.Client store esv1alpha1.GenericStore baseClient SecretClient vaultURL string namespace string } func init() { schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{ AzureKV: &esv1alpha1.AzureKVProvider{}, }) } // 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(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) { anAzure := &Azure{ kube: kube, store: store, namespace: namespace, } azClient, vaultURL, err := anAzure.newAzureClient(ctx) if err != nil { return nil, err } anAzure.baseClient = azClient anAzure.vaultURL = vaultURL return anAzure, nil } // Implements store.Client.GetSecret Interface. // Retrieves a secret/Key/Certificate with the secret name defined in ref.Name // The Object Type is defined as a prefix in the ref.Name , if no prefix is defined , we assume a secret is required. func (a *Azure) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) { version := "" objectType := "secret" basicClient := a.baseClient if ref.Version != "" { version = ref.Version } secretName := ref.Key nameSplitted := strings.Split(secretName, "/") if len(nameSplitted) > 1 { objectType = nameSplitted[0] secretName = nameSplitted[1] // TODO: later tokens can be used to read the secret tags } switch objectType { case "secret": // returns a SecretBundle with the secret value // https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#SecretBundle secretResp, err := basicClient.GetSecret(context.Background(), a.vaultURL, secretName, version) if err != nil { return nil, err } if ref.Property == "" { return []byte(*secretResp.Value), nil } res := gjson.Get(*secretResp.Value, ref.Property) if !res.Exists() { return nil, fmt.Errorf("property %s does not exist in key %s", ref.Property, ref.Key) } return []byte(res.String()), err case "cert": // returns a CertBundle. We return CER contents of x509 certificate // see: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#CertificateBundle secretResp, err := basicClient.GetCertificate(context.Background(), a.vaultURL, secretName, version) if err != nil { return nil, err } return *secretResp.Cer, nil case "key": // returns a KeyBundla that contains a jwk // azure kv returns only public keys // see: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/services/keyvault/v7.0/keyvault#KeyBundle keyResp, err := basicClient.GetKey(context.Background(), a.vaultURL, secretName, version) if err != nil { return nil, err } return json.Marshal(keyResp.Key) } return nil, fmt.Errorf("unknown Azure Keyvault object Type for %s", secretName) } // Implements store.Client.GetSecretMap Interface. // retrieve ALL secrets in a specific keyvault. // ExternalSecretDataRemoteRef Key is mandatory, but with current model we do not use its content. func (a *Azure) GetSecretMap(ctx context.Context, _ esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { basicClient := a.baseClient secretsMap := make(map[string][]byte) secretListIter, err := basicClient.GetSecretsComplete(context.Background(), a.vaultURL, nil) if err != nil { return nil, err } for secretListIter.NotDone() { secretList := secretListIter.Response().Value for _, secret := range *secretList { if !*secret.Attributes.Enabled { continue } secretName := path.Base(*secret.ID) secretResp, err := basicClient.GetSecret(context.Background(), a.vaultURL, secretName, "") secretValue := *secretResp.Value if err != nil { return nil, err } secretsMap[secretName] = []byte(secretValue) } err = secretListIter.Next() if err != nil { return nil, err } } return secretsMap, nil } func (a *Azure) newAzureClient(ctx context.Context) (*keyvault.BaseClient, string, error) { spec := *a.store.GetSpec().Provider.AzureKV tenantID := *spec.TenantID vaultURL := *spec.VaultURL if spec.AuthSecretRef == nil { return nil, "", fmt.Errorf("missing clientID/clientSecret in store config") } clusterScoped := false if a.store.GetObjectMeta().String() == 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 } csec, err := a.secretKeyRef(ctx, a.store.GetNamespace(), *spec.AuthSecretRef.ClientSecret, clusterScoped) if err != nil { return nil, "", 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" authorizer, err := clientCredentialsConfig.Authorizer() if err != nil { return nil, "", err } basicClient := keyvault.New() basicClient.Authorizer = authorizer return &basicClient, vaultURL, nil } func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, clusterScoped bool) (string, error) { var secret corev1.Secret ref := types.NamespacedName{ Namespace: namespace, Name: secretRef.Name, } if clusterScoped && secretRef.Namespace != nil { ref.Namespace = *secretRef.Namespace } err := a.kube.Get(ctx, ref, &secret) if err != nil { return "", fmt.Errorf("could not find secret %s/%s: %w", ref.Namespace, ref.Name, err) } keyBytes, ok := secret.Data[secretRef.Key] if !ok { return "", fmt.Errorf("no data for %q in secret '%s/%s'", secretRef.Key, secretRef.Name, namespace) } value := strings.TrimSpace(string(keyBytes)) return value, nil } func (a *Azure) Close() error { return nil }