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:
parent
6f69264240
commit
fc95068034
5 changed files with 386 additions and 0 deletions
19
apis/externalsecrets/v1alpha1/secretstore_azurekv_types.go
Normal file
19
apis/externalsecrets/v1alpha1/secretstore_azurekv_types.go
Normal 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"`
|
||||
}
|
|
@ -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"`
|
||||
|
|
259
pkg/provider/azure/keyvault/keyvault.go
Normal file
259
pkg/provider/azure/keyvault/keyvault.go
Normal 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
|
||||
}
|
103
pkg/provider/azure/keyvault/keyvault_test.go
Normal file
103
pkg/provider/azure/keyvault/keyvault_test.go
Normal 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)
|
||||
}
|
|
@ -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"
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue