Browse Source

Add ability provide CA for Yandex' Lockbox provider (#487)

* Add ability provide CA for Yandex' Lockbox provider

* Add tests for getting CA from secrets at Lockbox provider

* fixup! Add tests for getting CA from secrets at Lockbox provider

Co-authored-by: Vladimir Fedin <vladimirfedin@yandex-team.ru>
Vladimir Fedin 4 years ago
parent
commit
c351efcc15

+ 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"`
 }

+ 21 - 0
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -1261,9 +1261,30 @@ func (in *YandexLockboxAuth) DeepCopy() *YandexLockboxAuth {
 }
 
 // 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.

+ 25 - 0
deploy/crds/external-secrets.io_clustersecretstores.yaml

@@ -921,6 +921,31 @@ 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

+ 25 - 0
deploy/crds/external-secrets.io_secretstores.yaml

@@ -921,6 +921,31 @@ 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

+ 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,