1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

Support azure kv as provider

This commit is contained in:
mjiao 2021-01-28 15:28:43 +01:00 committed by ahmed mustafa
parent 6f69264240
commit fc95068034
5 changed files with 386 additions and 0 deletions

View file

@ -0,0 +1,19 @@
package v1alpha1
import smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
// Configures an store to sync secrets using Azure KV.
type AzureKVProvider struct {
// 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"`
}
// Configuration used to authenticate with Azure.
type AzureKVAuth struct {
// The Azure clientId of the service principle used for authentication.
ClientID *smmeta.SecretKeySelector `json:"clientID"`
// The Azure ClientSecret of the service principle used for authentication.
ClientSecret *smmeta.SecretKeySelector `json:"clientSecret"`
}

View file

@ -38,6 +38,10 @@ type SecretStoreProvider struct {
// +optional
AWS *AWSProvider `json:"aws,omitempty"`
// AzureKV configures this store to sync secrets using Azure Key Vault provider
// +optional
AzureKV *AzureKVProvider `json:"azurekv,omitempty"`
// Vault configures this store to sync secrets using Hashi provider
// +optional
Vault *VaultProvider `json:"vault,omitempty"`

View file

@ -0,0 +1,259 @@
package keyvault
import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"os"
"path"
"strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault"
kvauth "github.com/Azure/azure-sdk-for-go/services/keyvault/auth"
"golang.org/x/crypto/pkcs12"
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"
)
type Azure struct {
kube client.Client
store esv1alpha1.GenericStore
baseClient *keyvault.BaseClient
namespace string
iAzure IAzure
}
type IAzure interface {
getKeyVaultSecrets(ctx context.Context, vaultName string, version string, secretName string, withTags bool) (map[string][]byte, error)
}
func init() {
schema.Register(&Azure{}, &esv1alpha1.SecretStoreProvider{
AzureKV: &esv1alpha1.AzureKVProvider{},
})
}
func (a *Azure) New(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.Provider, error) {
anAzure := &Azure{
kube: kube,
store: store,
namespace: namespace,
}
anAzure.iAzure = anAzure
azClient, err := anAzure.newAzureClient(ctx)
if err != nil {
return nil, err
}
anAzure.baseClient = azClient
return anAzure, nil
}
// implement store.Client.GetSecret Interface.
// retrieve a secret with the secret name defined in ref.Property in a specific keyvault with the name ref.Name.
func (a *Azure) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
version := ""
var secretBundle []byte
if ref.Version != "" {
version = ref.Version
}
secretName := ref.Property
nameSplitted := strings.Split(secretName, "_")
getTags := false
if nameSplitted[len(nameSplitted)-1] == "TAG" {
secretName = nameSplitted[0]
getTags = true
}
secretMap, err := a.iAzure.getKeyVaultSecrets(ctx, ref.Key, version, secretName, getTags)
if err != nil {
return nil, err
}
secretBundle = secretMap[ref.Property]
return secretBundle, nil
}
// implement store.Client.GetSecretMap Interface.
// retrieve ALL secrets in a specific keyvault with the name ref.Name.
func (a *Azure) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
secretMap, err := a.iAzure.getKeyVaultSecrets(ctx, ref.Key, ref.Version, "", true)
return secretMap, err
}
// getCertBundle returns the certificate bundle.
func getCertBundleForPKCS(certificateRawVal string, certBundleOnly, certKeyOnly bool) (bundle string, err error) {
pfx, err := base64.StdEncoding.DecodeString(certificateRawVal)
if err != nil {
return bundle, err
}
blocks, _ := pkcs12.ToPEM(pfx, "")
for _, block := range blocks {
// skip the private key if looking for the cert only
if block.Type == "PRIVATE KEY" && certBundleOnly {
continue
}
// no headers
if block.Type == "PRIVATE KEY" {
pkey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
panic(err)
}
derStream := x509.MarshalPKCS1PrivateKey(pkey)
block = &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: derStream,
}
if certKeyOnly {
bundle = string(pem.EncodeToMemory(block))
break
}
}
block.Headers = nil
bundle += string(pem.EncodeToMemory(block))
}
return bundle, nil
}
// consolidated method to retrieve secret value or secrets list based on whether or not a secret name is passed.
// if the secret is of type PKCS then this is a cerificate that needs some decoding.
func (a *Azure) getKeyVaultSecrets(ctx context.Context, vaultName, version, secretName string, withTags bool) (map[string][]byte, error) {
basicClient := a.baseClient
secretsMap := make(map[string][]byte)
certBundleOnly := false
certKeyOnly := false
secretNameinBE := secretName
if secretName != "" {
nameSplitted := strings.Split(secretName, "_")
if nameSplitted[len(nameSplitted)-1] == "CRT" {
secretNameinBE = nameSplitted[0]
certBundleOnly = true
}
if nameSplitted[len(nameSplitted)-1] == "KEY" {
secretNameinBE = nameSplitted[0]
certKeyOnly = true
}
secretResp, err := basicClient.GetSecret(context.Background(), "https://"+vaultName+".vault.azure.net", secretNameinBE, version)
if err != nil {
return nil, err
}
secretValue := *secretResp.Value
// Azure currently supports only PKCS#12 or PEM, PEM will be taken as it is, PKCS needs processing
if secretResp.ContentType != nil && *secretResp.ContentType == "application/x-pkcs12" {
secretValue, err = getCertBundleForPKCS(*secretResp.Value, certBundleOnly, certKeyOnly)
if err != nil {
return nil, err
}
}
secretsMap[secretName] = []byte(secretValue)
if withTags {
appendTagsToSecretMap(secretName, secretsMap, secretResp.Tags)
}
} else {
secretList, err := basicClient.GetSecrets(context.Background(), "https://"+vaultName+".vault.azure.net", nil)
if err != nil {
return nil, err
}
for _, secret := range secretList.Values() {
if !*secret.Attributes.Enabled {
continue
}
secretResp, err := basicClient.GetSecret(context.Background(), "https://"+vaultName+".vault.azure.net", path.Base(*secret.ID), "")
secretValue := *secretResp.Value
// Azure currently supports only PKCS#12 or PEM, PEM will be taken as it is, PKCS needs processing
if secretResp.ContentType != nil && *secretResp.ContentType == "application/x-pkcs12" {
secretValue, err = getCertBundleForPKCS(*secretResp.Value, certBundleOnly, certKeyOnly)
}
if err != nil {
return nil, err
}
secretsMap[path.Base(*secret.ID)] = []byte(secretValue)
if withTags {
appendTagsToSecretMap(path.Base(*secret.ID), secretsMap, secretResp.Tags)
}
}
}
return secretsMap, nil
}
func appendTagsToSecretMap(secretName string, secretsMap map[string][]byte, tags map[string]*string) {
for tagKey, tagValue := range tags {
secretsMap[secretName+"_"+tagKey+"_TAG"] = []byte(*tagValue)
}
}
func (a *Azure) newAzureClient(ctx context.Context) (*keyvault.BaseClient, error) {
spec := *a.store.GetSpec().Provider.AzureKV
tenantID := *spec.TenantID
if spec.AuthSecretRef == nil {
return nil, fmt.Errorf("missing clientID/clientSecret in store config")
}
scoped := true
if a.store.GetObjectMeta().String() == "ClusterSecretStore" {
scoped = false
}
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.GetNamespacedName(), *spec.AuthSecretRef.ClientID, scoped)
if err != nil {
return nil, err
}
csec, err := a.secretKeyRef(ctx, a.store.GetNamespacedName(), *spec.AuthSecretRef.ClientSecret, scoped)
if err != nil {
return nil, err
}
os.Setenv("AZURE_TENANT_ID", tenantID)
os.Setenv("AZURE_CLIENT_ID", cid)
os.Setenv("AZURE_CLIENT_SECRET", csec)
authorizer, err := kvauth.NewAuthorizerFromEnvironment()
if err != nil {
return nil, err
}
os.Unsetenv("AZURE_TENANT_ID")
os.Unsetenv("AZURE_CLIENT_ID")
os.Unsetenv("AZURE_CLIENT_SECRET")
basicClient := keyvault.New()
basicClient.Authorizer = authorizer
return &basicClient, nil
}
func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef smmeta.SecretKeySelector, scoped bool) (string, error) {
var secret corev1.Secret
ref := types.NamespacedName{
Namespace: namespace,
Name: secretRef.Name,
}
if !scoped && secretRef.Namespace != nil {
ref.Namespace = *secretRef.Namespace
}
err := a.kube.Get(ctx, ref, &secret)
if err != nil {
return "", 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
}

View file

@ -0,0 +1,103 @@
package keyvault
import (
context "context"
"testing"
tassert "github.com/stretchr/testify/assert"
mock "github.com/stretchr/testify/mock"
"gotest.tools/assert"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
)
type azureMock struct {
mock.Mock
}
func (_m *azureMock) getKeyVaultSecrets(ctx context.Context, vaultName, version, secretName string, withTags bool) (map[string][]byte, error) {
ret := _m.Called(ctx, vaultName, version, secretName, withTags)
var r0 map[string][]byte
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool) map[string][]byte); ok {
r0 = rf(ctx, vaultName, version, secretName, withTags)
} else if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]byte)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, bool) error); ok {
r1 = rf(ctx, vaultName, version, secretName, withTags)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
func TestGetSecret(t *testing.T) {
testAzure := new(Azure)
anAzureMock := new(azureMock)
ctx := context.Background()
testAzure.iAzure = anAzureMock
property := "testProperty"
version := "v1"
rf := esv1alpha1.ExternalSecretDataRemoteRef{
Key: "testName",
Property: property,
Version: version,
}
returnValue := make(map[string][]byte)
returnValue["key"] = []byte{'A'}
anAzureMock.On("getKeyVaultSecrets", ctx, "testName", "v1", "testProperty", false).Return(returnValue, nil)
_, err := testAzure.GetSecret(ctx, rf)
assert.NilError(t, err, "the return err should be nil")
anAzureMock.AssertExpectations(t)
}
func TestGetSecretMap(t *testing.T) {
testAzure := new(Azure)
anAzureMock := new(azureMock)
ctx := context.Background()
testAzure.iAzure = anAzureMock
property := "testProperty"
version := "v1"
rf := esv1alpha1.ExternalSecretDataRemoteRef{
Key: "testName",
Property: property,
Version: version,
}
returnValue := make(map[string][]byte)
returnValue["key"] = []byte{'a'}
anAzureMock.On("getKeyVaultSecrets", ctx, "testName", "v1", "", true).Return(returnValue, nil)
_, err := testAzure.GetSecretMap(ctx, rf)
assert.NilError(t, err, "the return err should be nil")
anAzureMock.AssertExpectations(t)
}
func TestGetCertBundleForPKCS(t *testing.T) {
rawCertExample := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURC" +
"VENDQWUyZ0F3SUJBZ0lFUnIxWTdEQU5CZ2txaGtpRzl3MEJBUVVGQURBeU1Rc3d" +
"DUVlEVlFRR0V3SkUKUlRFUU1BNEdBMVVFQ2hNSFFXMWhaR1YxY3pFUk1BOEdBMV" +
"VFQXhNSVUwRlFJRkp2YjNRd0hoY05NVE13TWpFMApNVE15TmpRNVdoY05NelV4T" +
"WpNeE1UTXlOalE1V2pBeU1Rc3dDUVlEVlFRR0V3SkVSVEVRTUE0R0ExVUVDaE1I" +
"CnFWUlE3NjNGODFwWnorNXgyejJ6NmZyd0JHNUF3YUZKL1RmTE9HQzZQWnl5bW1" +
"pSlllL2tjUDdVeUhMQnBUUVkKLzloNTF5dDB5NlRBS1JmRk1wMlhuVUZBaWdyL0" +
"0xYVc1NjdORStQYzN5S0RWWlVHdU82UXZ0cExCZkpPS3pZSAowc3F3OElmYjRlN" +
"0R6TkJuTmRoVDhzbGdUYkh5K3RzZUtPb0xHNi9rUktmRmRvSmRoeHAzeGNnbm56" +
"ZkY0anUvCi9UZTRYaWsxNC9FMAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t"
c, ok := getCertBundleForPKCS(rawCertExample, true, true)
bundle := ""
tassert.Nil(t, ok)
tassert.Equal(t, c, bundle)
}
func TestAppendTagsToSecretMap(t *testing.T) {
var secretsMap map[string][]byte
var secretsMapOrigin map[string][]byte
secret := "testsecret"
var tags map[string]*string
appendTagsToSecretMap(secret, secretsMap, tags)
tassert.Equal(t, secretsMap, secretsMapOrigin)
}

View file

@ -19,5 +19,6 @@ package register
import (
_ "github.com/external-secrets/external-secrets/pkg/provider/aws"
_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
_ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"
_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
)