From 5384954f46513a811425ce22c9186b2eb915f32e Mon Sep 17 00:00:00 2001 From: Moritz Johner Date: Fri, 13 Jan 2023 10:19:25 +0100 Subject: [PATCH] aws secretsmanager/parameterstore referent auth (#1884) * feat: implement referentAuth for aws Signed-off-by: Moritz Johner * feat: e2e tests Signed-off-by: Moritz Johner * Update pkg/provider/aws/provider.go Co-authored-by: Gustavo Fernandes de Carvalho Signed-off-by: Moritz Johner * Update pkg/provider/aws/provider.go Co-authored-by: Gustavo Fernandes de Carvalho Signed-off-by: Moritz Johner * feat: allow each credential to be referent Signed-off-by: Moritz Johner Signed-off-by: Moritz Johner Signed-off-by: Moritz Johner Co-authored-by: Gustavo Fernandes de Carvalho --- .github/workflows/e2e.yml | 4 +- docs/introduction/stability-support.md | 4 +- e2e/entrypoint.sh | 2 - e2e/suites/provider/cases/aws/common.go | 105 +++++++++++++----- .../aws/parameterstore/parameterstore.go | 59 +++++++--- .../cases/aws/parameterstore/provider.go | 34 ++++-- .../cases/aws/secretsmanager/provider.go | 35 ++++-- .../aws/secretsmanager/secretsmanager.go | 58 +++++++--- pkg/provider/aws/auth/auth.go | 49 ++++---- pkg/provider/aws/auth/auth_test.go | 18 ++- .../aws/parameterstore/parameterstore.go | 17 ++- pkg/provider/aws/provider.go | 31 +++++- pkg/provider/aws/provider_test.go | 12 +- .../aws/secretsmanager/secretsmanager.go | 21 ++-- pkg/provider/aws/util/provider.go | 14 +++ terraform/aws/modules/cluster/main.tf | 2 +- 16 files changed, 320 insertions(+), 145 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f8a74ac8b..61981dfd2 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -16,8 +16,8 @@ env: GO_VERSION: '1.19' GINKGO_VERSION: 'v2.1.6' DOCKER_BUILDX_VERSION: 'v0.4.2' - KIND_VERSION: 'v0.14.0' - KIND_IMAGE: 'kindest/node:v1.24.2' + KIND_VERSION: 'v0.17.0' + KIND_IMAGE: 'kindest/node:v1.26.0' # Common users. We can't run a step 'if secrets.GHCR_USERNAME != ""' but we can run # a step 'if env.GHCR_USERNAME' != ""', so we copy these to succinctly test whether diff --git a/docs/introduction/stability-support.md b/docs/introduction/stability-support.md index 16dd8ef1d..ae77b38be 100644 --- a/docs/introduction/stability-support.md +++ b/docs/introduction/stability-support.md @@ -46,8 +46,8 @@ The following table show the support for features across different providers. | Provider | find by name | find by tags | metadataPolicy Fetch | referent authentication | store validation | push secret | |---------------------------|:------------:|:------------:| :------------------: | :---------------------: | :--------------: | :---------: | -| AWS Secrets Manager | x | x | | | x | | -| AWS Parameter Store | x | x | | | x | | +| AWS Secrets Manager | x | x | | x | x | | +| AWS Parameter Store | x | x | | x | x | | | Hashicorp Vault | x | x | | | x | | | GCP Secret Manager | x | x | | x | x | | | Azure Keyvault | x | x | x | x | x | | diff --git a/e2e/entrypoint.sh b/e2e/entrypoint.sh index fc4804bfe..caf7b74a0 100755 --- a/e2e/entrypoint.sh +++ b/e2e/entrypoint.sh @@ -33,9 +33,7 @@ ginkgo_args=( "--randomize-all" "--flake-attempts=2" "-p" - "-progress" "-trace" - "--slow-spec-threshold=5m" "-r" "-v" "-timeout=45m" diff --git a/e2e/suites/provider/cases/aws/common.go b/e2e/suites/provider/cases/aws/common.go index 5e89c631c..ee111193b 100644 --- a/e2e/suites/provider/cases/aws/common.go +++ b/e2e/suites/provider/cases/aws/common.go @@ -27,9 +27,10 @@ import ( ) const ( - WithReferencedIRSA = "with referenced IRSA" - WithMountedIRSA = "with mounted IRSA" - StaticCredentialsSecretName = "provider-secret" + WithReferencedIRSA = "with referenced IRSA" + WithMountedIRSA = "with mounted IRSA" + StaticCredentialsSecretName = "provider-secret" + StaticReferentCredentialsSecretName = "referent-provider-secret" ) func ReferencedIRSAStoreName(f *framework.Framework) string { @@ -50,6 +51,38 @@ func UseMountedIRSAStore(tc *framework.TestCase) { tc.ExternalSecret.Spec.SecretStoreRef.Name = MountedIRSAStoreName(tc.Framework) } +const ( + StaticStoreName = "aws-static-creds" + staticKeyID = "kid" + staticSecretAccessKey = "sak" + staticySessionToken = "st" +) + +func newStaticStoreProvider(serviceType esv1beta1.AWSServiceType, region, secretName string) *esv1beta1.SecretStoreProvider { + return &esv1beta1.SecretStoreProvider{ + AWS: &esv1beta1.AWSProvider{ + Service: serviceType, + Region: region, + Auth: esv1beta1.AWSAuth{ + SecretRef: &esv1beta1.AWSAuthSecretRef{ + AccessKeyID: esmetav1.SecretKeySelector{ + Name: StaticReferentCredentialsSecretName, + Key: staticKeyID, + }, + SecretAccessKey: esmetav1.SecretKeySelector{ + Name: StaticReferentCredentialsSecretName, + Key: staticSecretAccessKey, + }, + SessionToken: &esmetav1.SecretKeySelector{ + Name: StaticReferentCredentialsSecretName, + Key: staticySessionToken, + }, + }, + }, + }, + } +} + // StaticStore is namespaced and references // static credentials from a secret. func SetupStaticStore(f *framework.Framework, kid, sak, st, region string, serviceType esv1beta1.AWSServiceType) { @@ -59,9 +92,9 @@ func SetupStaticStore(f *framework.Framework, kid, sak, st, region string, servi Namespace: f.Namespace.Name, }, StringData: map[string]string{ - "kid": kid, - "sak": sak, - "st": st, + staticKeyID: kid, + staticSecretAccessKey: sak, + staticySessionToken: st, }, } err := f.CRClient.Create(context.Background(), awsCreds) @@ -69,34 +102,48 @@ func SetupStaticStore(f *framework.Framework, kid, sak, st, region string, servi secretStore := &esv1beta1.SecretStore{ ObjectMeta: metav1.ObjectMeta{ - Name: f.Namespace.Name, + Name: StaticStoreName, Namespace: f.Namespace.Name, }, Spec: esv1beta1.SecretStoreSpec{ - Provider: &esv1beta1.SecretStoreProvider{ - AWS: &esv1beta1.AWSProvider{ - Service: serviceType, - Region: region, - Auth: esv1beta1.AWSAuth{ - SecretRef: &esv1beta1.AWSAuthSecretRef{ - AccessKeyID: esmetav1.SecretKeySelector{ - Name: StaticCredentialsSecretName, - Key: "kid", - }, - SecretAccessKey: esmetav1.SecretKeySelector{ - Name: StaticCredentialsSecretName, - Key: "sak", - }, - SessionToken: &esmetav1.SecretKeySelector{ - Name: StaticCredentialsSecretName, - Key: "st", - }, - }, - }, - }, - }, + Provider: newStaticStoreProvider(serviceType, region, StaticCredentialsSecretName), }, } err = f.CRClient.Create(context.Background(), secretStore) Expect(err).ToNot(HaveOccurred()) } + +// CreateReferentStaticStore creates a CSS with referent auth and +// creates a secret with static authentication credentials in the ExternalSecret namespace. +func CreateReferentStaticStore(f *framework.Framework, kid, sak, st, region string, serviceType esv1beta1.AWSServiceType) { + ns := f.Namespace.Name + + awsCreds := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: StaticReferentCredentialsSecretName, + Namespace: ns, + }, + StringData: map[string]string{ + staticKeyID: kid, + staticSecretAccessKey: sak, + staticySessionToken: st, + }, + } + err := f.CRClient.Create(context.Background(), awsCreds) + Expect(err).ToNot(HaveOccurred()) + + secretStore := &esv1beta1.ClusterSecretStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: ReferentSecretStoreName(f), + }, + Spec: esv1beta1.SecretStoreSpec{ + Provider: newStaticStoreProvider(serviceType, region, StaticReferentCredentialsSecretName), + }, + } + err = f.CRClient.Create(context.Background(), secretStore) + Expect(err).ToNot(HaveOccurred()) +} + +func ReferentSecretStoreName(f *framework.Framework) string { + return "referent-auth" + f.Namespace.Name +} diff --git a/e2e/suites/provider/cases/aws/parameterstore/parameterstore.go b/e2e/suites/provider/cases/aws/parameterstore/parameterstore.go index 5596f1476..4f8c2f0c9 100644 --- a/e2e/suites/provider/cases/aws/parameterstore/parameterstore.go +++ b/e2e/suites/provider/cases/aws/parameterstore/parameterstore.go @@ -20,7 +20,14 @@ import ( . "github.com/onsi/ginkgo/v2" "github.com/external-secrets/external-secrets-e2e/framework" + awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws" "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common" + esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" +) + +const ( + withStaticAuth = "with static auth" + withReferentStaticAuth = "with static referent auth" ) var _ = Describe("[aws] ", Label("aws", "parameterstore"), func() { @@ -30,25 +37,41 @@ var _ = Describe("[aws] ", Label("aws", "parameterstore"), func() { DescribeTable("sync secrets", framework.TableFunc(f, prov), - Entry(common.SimpleDataSync(f)), - Entry(common.NestedJSONWithGJSON(f)), - Entry(common.JSONDataFromSync(f)), - Entry(common.JSONDataFromRewrite(f)), - Entry(common.JSONDataWithProperty(f)), - Entry(common.JSONDataWithTemplate(f)), - Entry(common.DockerJSONConfig(f)), - Entry(common.DataPropertyDockerconfigJSON(f)), - Entry(common.SSHKeySync(f)), - Entry(common.SSHKeySyncDataProperty(f)), - Entry(common.SyncWithoutTargetName(f)), - Entry(common.JSONDataWithoutTargetName(f)), - Entry(common.SyncV1Alpha1(f)), - Entry(common.DeletionPolicyDelete(f)), + framework.Compose(withStaticAuth, f, common.SimpleDataSync, useStaticAuth), + framework.Compose(withStaticAuth, f, common.NestedJSONWithGJSON, useStaticAuth), + framework.Compose(withStaticAuth, f, common.JSONDataFromSync, useStaticAuth), + framework.Compose(withStaticAuth, f, common.JSONDataFromRewrite, useStaticAuth), + framework.Compose(withStaticAuth, f, common.JSONDataWithProperty, useStaticAuth), + framework.Compose(withStaticAuth, f, common.JSONDataWithTemplate, useStaticAuth), + framework.Compose(withStaticAuth, f, common.DockerJSONConfig, useStaticAuth), + framework.Compose(withStaticAuth, f, common.DataPropertyDockerconfigJSON, useStaticAuth), + framework.Compose(withStaticAuth, f, common.SSHKeySync, useStaticAuth), + framework.Compose(withStaticAuth, f, common.SSHKeySyncDataProperty, useStaticAuth), + framework.Compose(withStaticAuth, f, common.SyncWithoutTargetName, useStaticAuth), + framework.Compose(withStaticAuth, f, common.JSONDataWithoutTargetName, useStaticAuth), + + framework.Compose(withStaticAuth, f, common.SyncV1Alpha1, useStaticAuth), + framework.Compose(withStaticAuth, f, common.DeletionPolicyDelete, useStaticAuth), + + // referent auth + framework.Compose(withReferentStaticAuth, f, common.SimpleDataSync, useReferentStaticAuth), // These are specific to parameterstore - Entry(FindByName(f)), - Entry(FindByNameWithPath(f)), - Entry(FindByTag(f)), - Entry(FindByTagWithPath(f)), + framework.Compose(withStaticAuth, f, FindByName, useStaticAuth), + framework.Compose(withStaticAuth, f, FindByNameWithPath, useStaticAuth), + framework.Compose(withStaticAuth, f, FindByTag, useStaticAuth), + framework.Compose(withStaticAuth, f, FindByTagWithPath, useStaticAuth), ) }) + +func useStaticAuth(tc *framework.TestCase) { + tc.ExternalSecret.Spec.SecretStoreRef.Name = awscommon.StaticStoreName + if tc.ExternalSecretV1Alpha1 != nil { + tc.ExternalSecretV1Alpha1.Spec.SecretStoreRef.Name = awscommon.StaticStoreName + } +} + +func useReferentStaticAuth(tc *framework.TestCase) { + tc.ExternalSecret.Spec.SecretStoreRef.Name = awscommon.ReferentSecretStoreName(tc.Framework) + tc.ExternalSecret.Spec.SecretStoreRef.Kind = esapi.ClusterSecretStoreKind +} diff --git a/e2e/suites/provider/cases/aws/parameterstore/provider.go b/e2e/suites/provider/cases/aws/parameterstore/provider.go index 018abe5fa..79e659098 100644 --- a/e2e/suites/provider/cases/aws/parameterstore/provider.go +++ b/e2e/suites/provider/cases/aws/parameterstore/provider.go @@ -34,7 +34,7 @@ import ( "github.com/external-secrets/external-secrets-e2e/framework" "github.com/external-secrets/external-secrets-e2e/framework/log" - common "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws" + awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esmetav1 "github.com/external-secrets/external-secrets/apis/meta/v1" ) @@ -68,19 +68,15 @@ func NewProvider(f *framework.Framework, kid, sak, st, region, saName, saNamespa } BeforeEach(func() { - common.SetupStaticStore(f, kid, sak, st, region, esv1beta1.AWSServiceParameterStore) + awscommon.SetupStaticStore(f, kid, sak, st, region, esv1beta1.AWSServiceParameterStore) + awscommon.CreateReferentStaticStore(f, kid, sak, st, region, esv1beta1.AWSServiceParameterStore) prov.SetupReferencedIRSAStore() prov.SetupMountedIRSAStore() }) AfterEach(func() { - // Cleanup ClusterSecretStore - err := prov.framework.CRClient.Delete(context.Background(), &esv1beta1.ClusterSecretStore{ - ObjectMeta: metav1.ObjectMeta{ - Name: common.ReferencedIRSAStoreName(f), - }, - }) - Expect(err).ToNot(HaveOccurred()) + prov.TeardownReferencedIRSAStore() + prov.TeardownMountedIRSAStore() }) return prov @@ -132,7 +128,7 @@ func (s *Provider) DeleteSecret(key string) { func (s *Provider) SetupMountedIRSAStore() { secretStore := &esv1beta1.SecretStore{ ObjectMeta: metav1.ObjectMeta{ - Name: common.MountedIRSAStoreName(s.framework), + Name: awscommon.MountedIRSAStoreName(s.framework), Namespace: s.framework.Namespace.Name, }, Spec: esv1beta1.SecretStoreSpec{ @@ -149,13 +145,21 @@ func (s *Provider) SetupMountedIRSAStore() { Expect(err).ToNot(HaveOccurred()) } +func (s *Provider) TeardownMountedIRSAStore() { + s.framework.CRClient.Delete(context.Background(), &esv1beta1.ClusterSecretStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: awscommon.MountedIRSAStoreName(s.framework), + }, + }) +} + // ReferncedIRSAStore is a ClusterStore // that references a (IRSA-) ServiceAccount in the default namespace. func (s *Provider) SetupReferencedIRSAStore() { log.Logf("creating IRSA ClusterSecretStore %s", s.framework.Namespace.Name) secretStore := &esv1beta1.ClusterSecretStore{ ObjectMeta: metav1.ObjectMeta{ - Name: common.ReferencedIRSAStoreName(s.framework), + Name: awscommon.ReferencedIRSAStoreName(s.framework), }, } _, err := controllerutil.CreateOrUpdate(context.Background(), s.framework.CRClient, secretStore, func() error { @@ -177,3 +181,11 @@ func (s *Provider) SetupReferencedIRSAStore() { }) Expect(err).ToNot(HaveOccurred()) } + +func (s *Provider) TeardownReferencedIRSAStore() { + s.framework.CRClient.Delete(context.Background(), &esv1beta1.ClusterSecretStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: awscommon.ReferencedIRSAStoreName(s.framework), + }, + }) +} diff --git a/e2e/suites/provider/cases/aws/secretsmanager/provider.go b/e2e/suites/provider/cases/aws/secretsmanager/provider.go index 2bb181a09..d1ac6afb0 100644 --- a/e2e/suites/provider/cases/aws/secretsmanager/provider.go +++ b/e2e/suites/provider/cases/aws/secretsmanager/provider.go @@ -35,7 +35,7 @@ import ( "github.com/external-secrets/external-secrets-e2e/framework" "github.com/external-secrets/external-secrets-e2e/framework/log" - common "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws" + awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" esmetav1 "github.com/external-secrets/external-secrets/apis/meta/v1" ) @@ -69,19 +69,16 @@ func NewProvider(f *framework.Framework, kid, sak, st, region, saName, saNamespa } BeforeEach(func() { - common.SetupStaticStore(f, kid, sak, st, region, esv1beta1.AWSServiceSecretsManager) + awscommon.SetupStaticStore(f, kid, sak, st, region, esv1beta1.AWSServiceSecretsManager) + awscommon.CreateReferentStaticStore(f, kid, sak, st, region, esv1beta1.AWSServiceSecretsManager) prov.SetupReferencedIRSAStore() prov.SetupMountedIRSAStore() }) AfterEach(func() { - // Cleanup ClusterSecretStore - err := prov.framework.CRClient.Delete(context.Background(), &esv1beta1.ClusterSecretStore{ - ObjectMeta: metav1.ObjectMeta{ - Name: common.ReferencedIRSAStoreName(f), - }, - }) - Expect(err).ToNot(HaveOccurred()) + prov.TeardownReferencedIRSAStore() + prov.TeardownMountedIRSAStore() + }) return prov @@ -150,7 +147,7 @@ func (s *Provider) DeleteSecret(key string) { func (s *Provider) SetupMountedIRSAStore() { secretStore := &esv1beta1.SecretStore{ ObjectMeta: metav1.ObjectMeta{ - Name: common.MountedIRSAStoreName(s.framework), + Name: awscommon.MountedIRSAStoreName(s.framework), Namespace: s.framework.Namespace.Name, }, Spec: esv1beta1.SecretStoreSpec{ @@ -167,13 +164,21 @@ func (s *Provider) SetupMountedIRSAStore() { Expect(err).ToNot(HaveOccurred()) } +func (s *Provider) TeardownMountedIRSAStore() { + s.framework.CRClient.Delete(context.Background(), &esv1beta1.ClusterSecretStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: awscommon.MountedIRSAStoreName(s.framework), + }, + }) +} + // ReferncedIRSAStore is a ClusterStore // that references a (IRSA-) ServiceAccount in the default namespace. func (s *Provider) SetupReferencedIRSAStore() { log.Logf("creating IRSA ClusterSecretStore %s", s.framework.Namespace.Name) secretStore := &esv1beta1.ClusterSecretStore{ ObjectMeta: metav1.ObjectMeta{ - Name: common.ReferencedIRSAStoreName(s.framework), + Name: awscommon.ReferencedIRSAStoreName(s.framework), }, } _, err := controllerutil.CreateOrUpdate(context.Background(), s.framework.CRClient, secretStore, func() error { @@ -195,3 +200,11 @@ func (s *Provider) SetupReferencedIRSAStore() { }) Expect(err).ToNot(HaveOccurred()) } + +func (s *Provider) TeardownReferencedIRSAStore() { + s.framework.CRClient.Delete(context.Background(), &esv1beta1.ClusterSecretStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: awscommon.ReferencedIRSAStoreName(s.framework), + }, + }) +} diff --git a/e2e/suites/provider/cases/aws/secretsmanager/secretsmanager.go b/e2e/suites/provider/cases/aws/secretsmanager/secretsmanager.go index e2fd37b34..fc705c3df 100644 --- a/e2e/suites/provider/cases/aws/secretsmanager/secretsmanager.go +++ b/e2e/suites/provider/cases/aws/secretsmanager/secretsmanager.go @@ -20,7 +20,14 @@ import ( . "github.com/onsi/ginkgo/v2" "github.com/external-secrets/external-secrets-e2e/framework" + awscommon "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/aws" "github.com/external-secrets/external-secrets-e2e/suites/provider/cases/common" + esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" +) + +const ( + withStaticAuth = "with static auth" + withReferentStaticAuth = "with static referent auth" ) var _ = Describe("[aws] ", Label("aws", "secretsmanager"), func() { @@ -30,23 +37,38 @@ var _ = Describe("[aws] ", Label("aws", "secretsmanager"), func() { DescribeTable("sync secrets", framework.TableFunc(f, prov), - Entry(common.SimpleDataSync(f)), - Entry(common.NestedJSONWithGJSON(f)), - Entry(common.JSONDataFromSync(f)), - Entry(common.JSONDataFromRewrite(f)), - Entry(common.JSONDataWithProperty(f)), - Entry(common.JSONDataWithTemplate(f)), - Entry(common.DockerJSONConfig(f)), - Entry(common.DataPropertyDockerconfigJSON(f)), - Entry(common.SSHKeySync(f)), - Entry(common.SSHKeySyncDataProperty(f)), - Entry(common.SyncWithoutTargetName(f)), - Entry(common.JSONDataWithoutTargetName(f)), - Entry(common.FindByName(f)), - Entry(common.FindByNameWithPath(f)), - Entry(common.FindByTag(f)), - Entry(common.FindByTagWithPath(f)), - Entry(common.SyncV1Alpha1(f)), - Entry(common.DeletionPolicyDelete(f)), + framework.Compose(withStaticAuth, f, common.SimpleDataSync, useStaticAuth), + framework.Compose(withStaticAuth, f, common.NestedJSONWithGJSON, useStaticAuth), + framework.Compose(withStaticAuth, f, common.JSONDataFromSync, useStaticAuth), + framework.Compose(withStaticAuth, f, common.JSONDataFromRewrite, useStaticAuth), + framework.Compose(withStaticAuth, f, common.JSONDataWithProperty, useStaticAuth), + framework.Compose(withStaticAuth, f, common.JSONDataWithTemplate, useStaticAuth), + framework.Compose(withStaticAuth, f, common.DockerJSONConfig, useStaticAuth), + framework.Compose(withStaticAuth, f, common.DataPropertyDockerconfigJSON, useStaticAuth), + framework.Compose(withStaticAuth, f, common.SSHKeySync, useStaticAuth), + framework.Compose(withStaticAuth, f, common.SSHKeySyncDataProperty, useStaticAuth), + framework.Compose(withStaticAuth, f, common.SyncWithoutTargetName, useStaticAuth), + framework.Compose(withStaticAuth, f, common.JSONDataWithoutTargetName, useStaticAuth), + framework.Compose(withStaticAuth, f, common.FindByName, useStaticAuth), + framework.Compose(withStaticAuth, f, common.FindByNameWithPath, useStaticAuth), + framework.Compose(withStaticAuth, f, common.FindByTag, useStaticAuth), + framework.Compose(withStaticAuth, f, common.FindByTagWithPath, useStaticAuth), + framework.Compose(withStaticAuth, f, common.SyncV1Alpha1, useStaticAuth), + framework.Compose(withStaticAuth, f, common.DeletionPolicyDelete, useStaticAuth), + + // referent auth + framework.Compose(withStaticAuth, f, common.SimpleDataSync, useReferentStaticAuth), ) }) + +func useStaticAuth(tc *framework.TestCase) { + tc.ExternalSecret.Spec.SecretStoreRef.Name = awscommon.StaticStoreName + if tc.ExternalSecretV1Alpha1 != nil { + tc.ExternalSecretV1Alpha1.Spec.SecretStoreRef.Name = awscommon.StaticStoreName + } +} + +func useReferentStaticAuth(tc *framework.TestCase) { + tc.ExternalSecret.Spec.SecretStoreRef.Name = awscommon.ReferentSecretStoreName(tc.Framework) + tc.ExternalSecret.Spec.SecretStoreRef.Kind = esapi.ClusterSecretStoreKind +} diff --git a/pkg/provider/aws/auth/auth.go b/pkg/provider/aws/auth/auth.go index 52a06c13e..773528f0c 100644 --- a/pkg/provider/aws/auth/auth.go +++ b/pkg/provider/aws/auth/auth.go @@ -87,7 +87,7 @@ func New(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, // use credentials via service account token jwtAuth := prov.Auth.JWTAuth if jwtAuth != nil { - creds, err = sessionFromServiceAccount(ctx, prov.Auth, prov.Region, isClusterKind, kube, namespace, jwtProvider) + creds, err = credsFromServiceAccount(ctx, prov.Auth, prov.Region, isClusterKind, kube, namespace, jwtProvider) if err != nil { return nil, err } @@ -97,7 +97,7 @@ func New(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, secretRef := prov.Auth.SecretRef if secretRef != nil { log.V(1).Info("using credentials from secretRef") - creds, err = sessionFromSecretRef(ctx, prov.Auth, isClusterKind, kube, namespace) + creds, err = credsFromSecretRef(ctx, prov.Auth, isClusterKind, kube, namespace) if err != nil { return nil, err } @@ -141,7 +141,7 @@ func NewGeneratorSession(ctx context.Context, auth esv1beta1.AWSAuth, role, regi // use credentials via service account token jwtAuth := auth.JWTAuth if jwtAuth != nil { - creds, err = sessionFromServiceAccount(ctx, auth, region, false, kube, namespace, jwtProvider) + creds, err = credsFromServiceAccount(ctx, auth, region, false, kube, namespace, jwtProvider) if err != nil { return nil, err } @@ -151,7 +151,7 @@ func NewGeneratorSession(ctx context.Context, auth esv1beta1.AWSAuth, role, regi secretRef := auth.SecretRef if secretRef != nil { log.V(1).Info("using credentials from secretRef") - creds, err = sessionFromSecretRef(ctx, auth, false, kube, namespace) + creds, err = credsFromSecretRef(ctx, auth, false, kube, namespace) if err != nil { return nil, err } @@ -178,16 +178,16 @@ func NewGeneratorSession(ctx context.Context, auth esv1beta1.AWSAuth, role, regi return sess, nil } -func sessionFromSecretRef(ctx context.Context, auth esv1beta1.AWSAuth, isClusterKind bool, kube client.Client, namespace string) (*credentials.Credentials, error) { +// credsFromSecretRef pulls access-key / secret-access-key from a secretRef to +// construct a aws.Credentials object +// The namespace of the external secret is used if the ClusterSecretStore does not specify a namespace (referentAuth) +// If the ClusterSecretStore defines a namespace it will take precedence. +func credsFromSecretRef(ctx context.Context, auth esv1beta1.AWSAuth, isClusterKind bool, kube client.Client, namespace string) (*credentials.Credentials, error) { ke := client.ObjectKey{ Name: auth.SecretRef.AccessKeyID.Name, - Namespace: namespace, // default to ExternalSecret namespace + Namespace: namespace, } - // only ClusterStore is allowed to set namespace (and then it's required) - if isClusterKind { - if auth.SecretRef.AccessKeyID.Namespace == nil { - return nil, fmt.Errorf(errInvalidClusterStoreMissingAKIDNamespace) - } + if isClusterKind && auth.SecretRef.AccessKeyID.Namespace != nil { ke.Namespace = *auth.SecretRef.AccessKeyID.Namespace } akSecret := v1.Secret{} @@ -197,13 +197,9 @@ func sessionFromSecretRef(ctx context.Context, auth esv1beta1.AWSAuth, isCluster } ke = client.ObjectKey{ Name: auth.SecretRef.SecretAccessKey.Name, - Namespace: namespace, // default to ExternalSecret namespace + Namespace: namespace, } - // only ClusterStore is allowed to set namespace (and then it's required) - if isClusterKind { - if auth.SecretRef.SecretAccessKey.Namespace == nil { - return nil, fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace) - } + if isClusterKind && auth.SecretRef.SecretAccessKey.Namespace != nil { ke.Namespace = *auth.SecretRef.SecretAccessKey.Namespace } sakSecret := v1.Secret{} @@ -224,13 +220,9 @@ func sessionFromSecretRef(ctx context.Context, auth esv1beta1.AWSAuth, isCluster if auth.SecretRef.SessionToken != nil { ke = client.ObjectKey{ Name: auth.SecretRef.SessionToken.Name, - Namespace: namespace, // default to ExternalSecret namespace + Namespace: namespace, } - // only ClusterStore is allowed to set namespace (and then it's required) - if isClusterKind { - if auth.SecretRef.SessionToken.Namespace == nil { - return nil, fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace) - } + if isClusterKind && auth.SecretRef.SessionToken.Namespace != nil { ke.Namespace = *auth.SecretRef.SessionToken.Namespace } stSecret := v1.Secret{} @@ -244,9 +236,14 @@ func sessionFromSecretRef(ctx context.Context, auth esv1beta1.AWSAuth, isCluster return credentials.NewStaticCredentials(aks, sak, sessionToken), err } -func sessionFromServiceAccount(ctx context.Context, auth esv1beta1.AWSAuth, region string, isClusterKind bool, kube client.Client, namespace string, jwtProvider jwtProviderFactory) (*credentials.Credentials, error) { +// credsFromServiceAccount uses a Kubernetes Service Account to acquire temporary +// credentials using aws.AssumeRoleWithWebIdentity. It will assume the role defined +// in the ServiceAccount annotation. +// If the ClusterSecretStore does not define a namespace it will use the namespace from the ExternalSecret (referentAuth). +// If the ClusterSecretStore defines the namespace it will take precedence. +func credsFromServiceAccount(ctx context.Context, auth esv1beta1.AWSAuth, region string, isClusterKind bool, kube client.Client, namespace string, jwtProvider jwtProviderFactory) (*credentials.Credentials, error) { name := auth.JWTAuth.ServiceAccountRef.Name - if isClusterKind { + if isClusterKind && auth.JWTAuth.ServiceAccountRef.Namespace != nil { namespace = *auth.JWTAuth.ServiceAccountRef.Namespace } sa := v1.ServiceAccount{} @@ -327,7 +324,7 @@ func DefaultSTSProvider(sess *session.Session) stsiface.STSAPI { return sts.New(sess) } -// getAWSSession check if an AWS session should be reused +// getAWSSession checks if an AWS session should be reused // it returns the aws session or an error. func getAWSSession(config *aws.Config, enableCache bool, name, kind, namespace, resourceVersion string) (*session.Session, error) { tmpSession := SessionCache{ diff --git a/pkg/provider/aws/auth/auth_test.go b/pkg/provider/aws/auth/auth_test.go index e8e2cb7a3..84a711f13 100644 --- a/pkg/provider/aws/auth/auth_test.go +++ b/pkg/provider/aws/auth/auth_test.go @@ -311,7 +311,7 @@ func TestNewSession(t *testing.T) { expectedSecretKey: "2222", }, { - name: "namespace is mandatory when using ClusterStore with SecretKeySelector", + name: "ClusterStore should use credentials from a ExternalSecret namespace (referentAuth)", namespace: esNamespaceKey, store: &esv1beta1.ClusterSecretStore{ TypeMeta: metav1.TypeMeta{ @@ -337,7 +337,21 @@ func TestNewSession(t *testing.T) { }, }, }, - expectErr: "invalid ClusterSecretStore: missing AWS AccessKeyID Namespace", + secrets: []v1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "onesecret", + Namespace: esNamespaceKey, + }, + Data: map[string][]byte{ + "one": []byte("7777"), + "two": []byte("4444"), + }, + }, + }, + expectProvider: true, + expectedKeyID: "7777", + expectedSecretKey: "4444", }, { name: "jwt auth via cluster secret store", diff --git a/pkg/provider/aws/parameterstore/parameterstore.go b/pkg/provider/aws/parameterstore/parameterstore.go index 945960f48..20f44026e 100644 --- a/pkg/provider/aws/parameterstore/parameterstore.go +++ b/pkg/provider/aws/parameterstore/parameterstore.go @@ -42,8 +42,9 @@ var ( // ParameterStore is a provider for AWS ParameterStore. type ParameterStore struct { - sess *session.Session - client PMInterface + sess *session.Session + client PMInterface + referentAuth bool } // PMInterface is a subset of the parameterstore api. @@ -61,10 +62,11 @@ const ( ) // New constructs a ParameterStore Provider that is specific to a store. -func New(sess *session.Session, cfg *aws.Config) (*ParameterStore, error) { +func New(sess *session.Session, cfg *aws.Config, referentAuth bool) (*ParameterStore, error) { return &ParameterStore{ - sess: sess, - client: ssm.New(sess, cfg), + sess: sess, + referentAuth: referentAuth, + client: ssm.New(sess, cfg), }, nil } @@ -377,6 +379,11 @@ func (pm *ParameterStore) Close(ctx context.Context) error { } func (pm *ParameterStore) Validate() (esv1beta1.ValidationResult, error) { + // skip validation stack because it depends on the namespace + // of the ExternalSecret + if pm.referentAuth { + return esv1beta1.ValidationResultUnknown, nil + } _, err := pm.sess.Config.Credentials.Get() if err != nil { return esv1beta1.ValidationResultError, err diff --git a/pkg/provider/aws/provider.go b/pkg/provider/aws/provider.go index d6f0d4c85..00cd6f109 100644 --- a/pkg/provider/aws/provider.go +++ b/pkg/provider/aws/provider.go @@ -23,6 +23,7 @@ import ( awsclient "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws/session" "sigs.k8s.io/controller-runtime/pkg/client" esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" @@ -68,17 +69,22 @@ func (p *Provider) ValidateStore(store esv1beta1.GenericStore) error { // case: static credentials if prov.Auth.SecretRef != nil { - if err := utils.ValidateSecretSelector(store, prov.Auth.SecretRef.AccessKeyID); err != nil { + if err := utils.ValidateReferentSecretSelector(store, prov.Auth.SecretRef.AccessKeyID); err != nil { return fmt.Errorf("invalid Auth.SecretRef.AccessKeyID: %w", err) } - if err := utils.ValidateSecretSelector(store, prov.Auth.SecretRef.SecretAccessKey); err != nil { + if err := utils.ValidateReferentSecretSelector(store, prov.Auth.SecretRef.SecretAccessKey); err != nil { return fmt.Errorf("invalid Auth.SecretRef.SecretAccessKey: %w", err) } + if prov.Auth.SecretRef.SessionToken != nil { + if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.SecretRef.SessionToken); err != nil { + return fmt.Errorf("invalid Auth.SecretRef.SessionToken: %w", err) + } + } } // case: jwt credentials if prov.Auth.JWTAuth != nil && prov.Auth.JWTAuth.ServiceAccountRef != nil { - if err := utils.ValidateServiceAccountSelector(store, *prov.Auth.JWTAuth.ServiceAccountRef); err != nil { + if err := utils.ValidateReferentServiceAccountSelector(store, *prov.Auth.JWTAuth.ServiceAccountRef); err != nil { return fmt.Errorf("invalid Auth.JWT.ServiceAccountRef: %w", err) } } @@ -114,6 +120,21 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl storeSpec := store.GetSpec() var cfg *aws.Config + // allow SecretStore controller validation to pass + // when using referent namespace. + if util.IsReferentSpec(prov.Auth) && namespace == "" && + store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind { + cfg = aws.NewConfig().WithRegion("eu-west-1").WithEndpointResolver(awsauth.ResolveEndpoint()) + sess := &session.Session{Config: cfg} + switch prov.Service { + case esv1beta1.AWSServiceSecretsManager: + return secretsmanager.New(sess, cfg, true) + case esv1beta1.AWSServiceParameterStore: + return parameterstore.New(sess, cfg, true) + } + return nil, fmt.Errorf(errUnknownProviderService, prov.Service) + } + sess, err := awsauth.New(ctx, store, kube, namespace, assumeRoler, awsauth.DefaultJWTProvider) if err != nil { return nil, fmt.Errorf(errUnableCreateSession, err) @@ -146,9 +167,9 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl switch prov.Service { case esv1beta1.AWSServiceSecretsManager: - return secretsmanager.New(sess, cfg) + return secretsmanager.New(sess, cfg, false) case esv1beta1.AWSServiceParameterStore: - return parameterstore.New(sess, cfg) + return parameterstore.New(sess, cfg, false) } return nil, fmt.Errorf(errUnknownProviderService, prov.Service) } diff --git a/pkg/provider/aws/provider_test.go b/pkg/provider/aws/provider_test.go index 4d77499cd..7e530f09a 100644 --- a/pkg/provider/aws/provider_test.go +++ b/pkg/provider/aws/provider_test.go @@ -238,8 +238,8 @@ func TestValidateStore(t *testing.T) { }, }, { - name: "invalid static creds auth / SecretAccessKey missing namespace", - wantErr: true, + name: "referentAuth static creds / SecretAccessKey without namespace", + wantErr: false, args: args{ store: &esv1beta1.ClusterSecretStore{ TypeMeta: v1.TypeMeta{ @@ -263,8 +263,8 @@ func TestValidateStore(t *testing.T) { }, }, { - name: "invalid static creds auth / AccessKeyID missing namespace", - wantErr: true, + name: "referentAuth static creds / AccessKeyID without namespace", + wantErr: false, args: args{ store: &esv1beta1.ClusterSecretStore{ TypeMeta: v1.TypeMeta{ @@ -288,8 +288,8 @@ func TestValidateStore(t *testing.T) { }, }, { - name: "invalid jwt auth: missing sa selector namespace", - wantErr: true, + name: "referentAuth jwt: sa selector without namespace", + wantErr: false, args: args{ store: &esv1beta1.ClusterSecretStore{ TypeMeta: v1.TypeMeta{ diff --git a/pkg/provider/aws/secretsmanager/secretsmanager.go b/pkg/provider/aws/secretsmanager/secretsmanager.go index 68305f95a..03a0d311d 100644 --- a/pkg/provider/aws/secretsmanager/secretsmanager.go +++ b/pkg/provider/aws/secretsmanager/secretsmanager.go @@ -41,9 +41,10 @@ var _ esv1beta1.SecretsClient = &SecretsManager{} // SecretsManager is a provider for AWS SecretsManager. type SecretsManager struct { - sess *session.Session - client SMInterface - cache map[string]*awssm.GetSecretValueOutput + sess *session.Session + client SMInterface + referentAuth bool + cache map[string]*awssm.GetSecretValueOutput } // SMInterface is a subset of the smiface api. @@ -67,11 +68,12 @@ const ( var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager") // New creates a new SecretsManager client. -func New(sess *session.Session, cfg *aws.Config) (*SecretsManager, error) { +func New(sess *session.Session, cfg *aws.Config, referentAuth bool) (*SecretsManager, error) { return &SecretsManager{ - sess: sess, - client: awssm.New(sess, cfg), - cache: make(map[string]*awssm.GetSecretValueOutput), + sess: sess, + client: awssm.New(sess, cfg), + referentAuth: referentAuth, + cache: make(map[string]*awssm.GetSecretValueOutput), }, nil } @@ -407,6 +409,11 @@ func (sm *SecretsManager) Close(ctx context.Context) error { } func (sm *SecretsManager) Validate() (esv1beta1.ValidationResult, error) { + // skip validation stack because it depends on the namespace + // of the ExternalSecret + if sm.referentAuth { + return esv1beta1.ValidationResultUnknown, nil + } _, err := sm.sess.Config.Credentials.Get() if err != nil { return esv1beta1.ValidationResultError, err diff --git a/pkg/provider/aws/util/provider.go b/pkg/provider/aws/util/provider.go index 34f356b0f..0b04597b2 100644 --- a/pkg/provider/aws/util/provider.go +++ b/pkg/provider/aws/util/provider.go @@ -45,3 +45,17 @@ func GetAWSProvider(store esv1beta1.GenericStore) (*esv1beta1.AWSProvider, error } return prov, nil } + +func IsReferentSpec(prov esv1beta1.AWSAuth) bool { + if prov.JWTAuth != nil && prov.JWTAuth.ServiceAccountRef != nil && prov.JWTAuth.ServiceAccountRef.Namespace == nil { + return true + } + if prov.SecretRef != nil && + (prov.SecretRef.AccessKeyID.Namespace == nil || + prov.SecretRef.SecretAccessKey.Namespace == nil || + (prov.SecretRef.SessionToken != nil && prov.SecretRef.SessionToken.Namespace == nil)) { + return true + } + + return false +} diff --git a/terraform/aws/modules/cluster/main.tf b/terraform/aws/modules/cluster/main.tf index 7c1cfe4c6..949d858c2 100644 --- a/terraform/aws/modules/cluster/main.tf +++ b/terraform/aws/modules/cluster/main.tf @@ -4,7 +4,7 @@ provider "aws" { locals { name = var.cluster_name - cluster_version = "1.22" + cluster_version = "1.24" region = var.cluster_region serviceaccount_name = var.irsa_sa_name