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

Merge pull request #251 from external-secrets/feature/aws-jwt

feat(aws): add jwt authentication
This commit is contained in:
paul-the-alien[bot] 2021-07-17 19:14:27 +00:00 committed by GitHub
commit 8827e3ab92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1451 additions and 843 deletions

View file

@ -68,7 +68,7 @@ jobs:
with:
version: "v0.11.1"
wait: 10m
node_image: kindest/node:v1.20.2
node_image: kindest/node:v1.20.7
name: external-secrets
- name: Run e2e Tests
@ -125,7 +125,7 @@ jobs:
with:
version: "v0.11.1"
wait: 10m
node_image: kindest/node:v1.20.2
node_image: kindest/node:v1.20.7
name: external-secrets
- name: Run e2e Tests

View file

@ -18,12 +18,17 @@ import (
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
)
// AWSAuth contains a secretRef for credentials.
// AWSAuth tells the controller how to do authentication with aws.
// Only one of secretRef or jwt can be specified.
// if none is specified the controller will load credentials using the aws sdk defaults.
type AWSAuth struct {
SecretRef AWSAuthSecretRef `json:"secretRef"`
// +optional
SecretRef *AWSAuthSecretRef `json:"secretRef,omitempty"`
// +optional
JWTAuth *AWSJWTAuth `json:"jwt,omitempty"`
}
// AWSAuthSecretRef holds secret references for aws credentials
// AWSAuthSecretRef holds secret references for AWS credentials
// both AccessKeyID and SecretAccessKey must be defined in order to properly authenticate.
type AWSAuthSecretRef struct {
// The AccessKeyID is used for authentication
@ -33,6 +38,11 @@ type AWSAuthSecretRef struct {
SecretAccessKey esmeta.SecretKeySelector `json:"secretAccessKeySecretRef,omitempty"`
}
// Authenticate against AWS using service account tokens.
type AWSJWTAuth struct {
ServiceAccountRef *esmeta.ServiceAccountSelector `json:"serviceAccountRef,omitempty"`
}
// AWSServiceType is a enum that defines the service/API that is used to fetch the secrets.
// +kubebuilder:validation:Enum=SecretsManager;ParameterStore
type AWSServiceType string
@ -54,9 +64,8 @@ type AWSProvider struct {
// Auth defines the information necessary to authenticate against AWS
// if not set aws sdk will infer credentials from your environment
// see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials
// +nullable
// +optional
Auth *AWSAuth `json:"auth"`
Auth AWSAuth `json:"auth"`
// Role is a Role ARN which the SecretManager provider will assume
// +optional

View file

@ -27,7 +27,16 @@ import (
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AWSAuth) DeepCopyInto(out *AWSAuth) {
*out = *in
in.SecretRef.DeepCopyInto(&out.SecretRef)
if in.SecretRef != nil {
in, out := &in.SecretRef, &out.SecretRef
*out = new(AWSAuthSecretRef)
(*in).DeepCopyInto(*out)
}
if in.JWTAuth != nil {
in, out := &in.JWTAuth, &out.JWTAuth
*out = new(AWSJWTAuth)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSAuth.
@ -58,15 +67,31 @@ func (in *AWSAuthSecretRef) DeepCopy() *AWSAuthSecretRef {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AWSProvider) DeepCopyInto(out *AWSProvider) {
func (in *AWSJWTAuth) DeepCopyInto(out *AWSJWTAuth) {
*out = *in
if in.Auth != nil {
in, out := &in.Auth, &out.Auth
*out = new(AWSAuth)
if in.ServiceAccountRef != nil {
in, out := &in.ServiceAccountRef, &out.ServiceAccountRef
*out = new(metav1.ServiceAccountSelector)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSJWTAuth.
func (in *AWSJWTAuth) DeepCopy() *AWSJWTAuth {
if in == nil {
return nil
}
out := new(AWSJWTAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AWSProvider) DeepCopyInto(out *AWSProvider) {
*out = *in
in.Auth.DeepCopyInto(&out.Auth)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSProvider.
func (in *AWSProvider) DeepCopy() *AWSProvider {
if in == nil {

View file

@ -51,6 +51,12 @@ rules:
- "create"
- "update"
- "delete"
- apiGroups:
- ""
resources:
- "serviceaccounts/token"
verbs:
- "create"
- apiGroups:
- ""
resources:

View file

@ -62,11 +62,31 @@ spec:
description: 'Auth defines the information necessary to authenticate
against AWS if not set aws sdk will infer credentials from
your environment see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials'
nullable: true
properties:
jwt:
description: Authenticate against AWS using service account
tokens.
properties:
serviceAccountRef:
description: A reference to a ServiceAccount resource.
properties:
name:
description: The name of the ServiceAccount 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
required:
- name
type: object
type: object
secretRef:
description: AWSAuthSecretRef holds secret references
for aws credentials both AccessKeyID and SecretAccessKey
for AWS credentials both AccessKeyID and SecretAccessKey
must be defined in order to properly authenticate.
properties:
accessKeyIDSecretRef:
@ -114,8 +134,6 @@ spec:
- name
type: object
type: object
required:
- secretRef
type: object
region:
description: AWS Region to be used for the provider

View file

@ -62,11 +62,31 @@ spec:
description: 'Auth defines the information necessary to authenticate
against AWS if not set aws sdk will infer credentials from
your environment see: https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials'
nullable: true
properties:
jwt:
description: Authenticate against AWS using service account
tokens.
properties:
serviceAccountRef:
description: A reference to a ServiceAccount resource.
properties:
name:
description: The name of the ServiceAccount 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
required:
- name
type: object
type: object
secretRef:
description: AWSAuthSecretRef holds secret references
for aws credentials both AccessKeyID and SecretAccessKey
for AWS credentials both AccessKeyID and SecretAccessKey
must be defined in order to properly authenticate.
properties:
accessKeyIDSecretRef:
@ -114,8 +134,6 @@ spec:
- name
type: object
type: object
required:
- secretRef
type: object
region:
description: AWS Region to be used for the provider

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

File diff suppressed because one or more lines are too long

View file

@ -1,9 +1,85 @@
## AWS Authentication
Access to AWS providers can be granted in various ways:
### Controller's Pod Identity
* [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html): IAM roles for service accounts.
* Per pod IAM authentication: [kiam](https://github.com/uswitch/kiam) or [kube2iam](https://github.com/jtblin/kube2iam).
* Directly provide AWS credentials to the External Secrets Operator pod by using environment variables.
![Pod Identity Authentication](./pictures/diagrams-provider-aws-auth-pod-identity.png)
Additionally, before fetching a secret from a store, ESO is able to assume role (as a proxy so to speak). It is advisable to use multiple roles in a multi-tenant environment.
This is basicially a zero-configuration authentication method that inherits the credentials from the runtime environment using the [aws sdk default credential chain](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default).
You can attach a role to the pod using [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html), [kiam](https://github.com/uswitch/kiam) or [kube2iam](https://github.com/jtblin/kube2iam). When no other authentication method is configured in the `Kind=Secretstore` this role is used to make all API calls against AWS Secrets Manager or SSM Parameter Store.
Based on the Pod's identity you can do a `sts:assumeRole` before fetching the secrets to limit access to certain keys in your provider. This is optional.
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
name: team-b-store
spec:
provider:
aws:
service: SecretsManager
# optional: do a sts:assumeRole before fetching secrets
role: team-b
```
### Access Key ID & Secret Access Key
![SecretRef](./pictures/diagrams-provider-aws-auth-secret-ref.png)
You can store Access Key ID & Secret Access Key in a `Kind=Secret` and reference it from a SecretStore.
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
name: team-b-store
spec:
provider:
aws:
service: SecretsManager
# optional: assume role before fetching secrets
role: team-b
auth:
secretRef:
accessKeyIDSecretRef:
name: awssm-secret
key: access-key
secretAccessKeySecretRef:
name: awssm-secret
key: secret-access-key
```
### EKS Service Account credentials
![Service Account](./pictures/diagrams-provider-aws-auth-service-account.png)
This feature lets you use short-lived service account tokens to authenticate with AWS.
You must have [Service Account Volume Projection](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#service-account-token-volume-projection) enabled - it is by default on EKS. See [EKS guide](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-technical-overview.html) on how to set up IAM roles for service accounts.
The big advantage of this approach is that ESO runs without any credentials.
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/team-a
name: my-serviceaccount
namespace: default
```
Reference the service account from above in the Secret Store:
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
name: secretstore-sample
spec:
provider:
aws:
service: SecretsManager
auth:
jwt:
serviceAccountRef:
name: my-serviceaccount
```

View file

@ -17,7 +17,9 @@ Resource Types:
<a href="#external-secrets.io/v1alpha1.AWSProvider">AWSProvider</a>)
</p>
<p>
<p>AWSAuth contains a secretRef for credentials.</p>
<p>AWSAuth tells the controller how to do authentication with aws.
Only one of secretRef or jwt can be specified.
if none is specified the controller will load credentials using the aws sdk defaults</p>
</p>
<table>
<thead>
@ -37,6 +39,20 @@ AWSAuthSecretRef
</em>
</td>
<td>
<em>(Optional)</em>
</td>
</tr>
<tr>
<td>
<code>jwt</code></br>
<em>
<a href="#external-secrets.io/v1alpha1.AWSJWTAuth">
AWSJWTAuth
</a>
</em>
</td>
<td>
<em>(Optional)</em>
</td>
</tr>
</tbody>
@ -48,7 +64,7 @@ AWSAuthSecretRef
<a href="#external-secrets.io/v1alpha1.AWSAuth">AWSAuth</a>)
</p>
<p>
<p>AWSAuthSecretRef holds secret references for aws credentials
<p>AWSAuthSecretRef holds secret references for AWS credentials
both AccessKeyID and SecretAccessKey must be defined in order to properly authenticate.</p>
</p>
<table>
@ -83,6 +99,35 @@ github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1alpha1.AWSJWTAuth">AWSJWTAuth
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1alpha1.AWSAuth">AWSAuth</a>)
</p>
<p>
<p>Authenticate against AWS using service account tokens</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>serviceAccountRef</code></br>
<em>
github.com/external-secrets/external-secrets/apis/meta/v1.ServiceAccountSelector
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1alpha1.AWSProvider">AWSProvider
</h3>
<p>

View file

@ -4,7 +4,7 @@ SHELL := /bin/bash
IMG_TAG = test
IMG = local/external-secrets-e2e:$(IMG_TAG)
K8S_VERSION = "1.19.1"
K8S_VERSION = "1.20.7"
export FOCUS := $(FOCUS)
start-kind: ## Start kind cluster

View file

@ -5,3 +5,7 @@ image:
extraEnv:
- name: AWS_SECRETSMANAGER_ENDPOINT
value: "http://localstack.default"
- name: AWS_STS_ENDPOINT
value: "http://localstack.default"
- name: AWS_SSM_ENDPOINT
value: "http://localstack.default"

View file

@ -1,3 +1,5 @@
image:
tag: "0.12.14"
service:
type: ClusterIP
edgeService:

View file

@ -1,6 +1,14 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
kubeadmConfigPatches:
- |
kind: ClusterConfiguration
apiServer:
extraArgs:
api-audiences: "sts.amazonaws.com"
service-account-key-file: "/etc/kubernetes/pki/sa.pub"
service-account-signing-key-file: "/etc/kubernetes/pki/sa.key"
service-account-issuer: "https://s3-XXXXXXXXXX.amazonaws.com/XXXXXXXXXXXXXXXXXXXXX"
- |
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration

View file

@ -32,7 +32,7 @@ import (
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/e2e/framework"
prov "github.com/external-secrets/external-secrets/pkg/provider/aws"
"github.com/external-secrets/external-secrets/pkg/provider/aws/auth"
)
type SMProvider struct {
@ -45,7 +45,7 @@ func newSMProvider(f *framework.Framework, url string) *SMProvider {
sess, err := session.NewSessionWithOptions(session.Options{
Config: aws.Config{
Credentials: credentials.NewStaticCredentials("foobar", "foobar", "secret-manager"),
EndpointResolver: prov.ResolveEndpointWithServiceMap(map[string]string{
EndpointResolver: auth.ResolveEndpointWithServiceMap(map[string]string{
"secretsmanager": url,
}),
Region: aws.String("eu-east-1"),
@ -103,8 +103,8 @@ func (s *SMProvider) BeforeEach() {
AWS: &esv1alpha1.AWSProvider{
Service: esv1alpha1.AWSServiceSecretsManager,
Region: "us-east-1",
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
Auth: esv1alpha1.AWSAuth{
SecretRef: &esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "provider-secret",
Key: "kid",

View file

@ -14,26 +14,112 @@ limitations under the License.
package aws
import (
"context"
"fmt"
// nolint
. "github.com/onsi/ginkgo"
// nolint
. "github.com/onsi/ginkgo/extensions/table"
// nolint
. "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/e2e/framework"
"github.com/external-secrets/external-secrets/e2e/suite/common"
)
var _ = Describe("[aws] ", func() {
f := framework.New("eso-aws")
prov := newSMProvider(f, "http://localstack.default")
jwt := func(tc *framework.TestCase) {
saName := "my-sa"
err := f.CRClient.Create(context.Background(), &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: saName,
Namespace: f.Namespace.Name,
Annotations: map[string]string{
"eks.amazonaws.com/role-arn": "arn:aws:iam::account:role/my-example-role",
},
},
})
Expect(err).ToNot(HaveOccurred())
// create secret store
secretStore := &esv1alpha1.SecretStore{
TypeMeta: metav1.TypeMeta{
Kind: esv1alpha1.SecretStoreKind,
APIVersion: esv1alpha1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: f.Namespace.Name,
Namespace: f.Namespace.Name,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Service: esv1alpha1.AWSServiceSecretsManager,
Region: "us-east-1",
Auth: esv1alpha1.AWSAuth{
JWTAuth: &esv1alpha1.AWSJWTAuth{
ServiceAccountRef: &esmeta.ServiceAccountSelector{
Name: saName,
Namespace: &f.Namespace.Name,
},
},
},
},
},
},
}
err = f.CRClient.Patch(context.Background(), secretStore, client.Apply, client.FieldOwner("e2e-case"), client.ForceOwnership)
Expect(err).ToNot(HaveOccurred())
secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
secretKey2 := fmt.Sprintf("%s-%s", f.Namespace.Name, "other")
secretValue := "bar"
tc.Secrets = map[string]string{
secretKey1: secretValue,
secretKey2: secretValue,
}
tc.ExpectedSecret = &v1.Secret{
Type: v1.SecretTypeOpaque,
Data: map[string][]byte{
secretKey1: []byte(secretValue),
secretKey2: []byte(secretValue),
},
}
tc.ExternalSecret.Spec.Data = []esv1alpha1.ExternalSecretData{
{
SecretKey: secretKey1,
RemoteRef: esv1alpha1.ExternalSecretDataRemoteRef{
Key: secretKey1,
},
},
{
SecretKey: secretKey2,
RemoteRef: esv1alpha1.ExternalSecretDataRemoteRef{
Key: secretKey2,
},
},
}
}
DescribeTable("sync secrets",
framework.TableFunc(f,
newSMProvider(f, "http://localstack.default")),
prov),
Entry(common.SimpleDataSync(f)),
Entry(common.NestedJSONWithGJSON(f)),
Entry(common.JSONDataFromSync(f)),
Entry(common.JSONDataWithProperty(f)),
Entry(common.JSONDataWithTemplate(f)),
Entry("should sync secrets with jwt auth", jwt),
)
})

View file

@ -0,0 +1,231 @@
/*
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 auth
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/defaults"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/aws-sdk-go/service/sts/stsiface"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
)
// Config contains configuration to create a new AWS provider.
type Config struct {
AssumeRole string
Region string
APIRetries int
}
var log = ctrl.Log.WithName("provider").WithName("aws")
const (
roleARNAnnotation = "eks.amazonaws.com/role-arn"
errInvalidClusterStoreMissingAKIDNamespace = "invalid ClusterSecretStore: missing AWS AccessKeyID Namespace"
errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing AWS SecretAccessKey Namespace"
errFetchAKIDSecret = "could not fetch accessKeyID secret: %w"
errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w"
errMissingSAK = "missing SecretAccessKey"
errMissingAKID = "missing AccessKeyID"
)
// New creates a new aws session based on the provided store
// it uses the following authentication mechanisms in order:
// * service-account token authentication via AssumeRoleWithWebIdentity
// * static credentials from a Kind=Secret, optionally with doing a AssumeRole.
// * sdk default provider chain, see: https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default
func New(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string, assumeRoler STSProvider, jwtProvider jwtProviderFactory) (*session.Session, error) {
prov, err := util.GetAWSProvider(store)
if err != nil {
return nil, err
}
var creds *credentials.Credentials
// use credentials via service account token
jwtAuth := prov.Auth.JWTAuth
if jwtAuth != nil {
creds, err = sessionFromServiceAccount(ctx, prov, store, kube, namespace, jwtProvider)
if err != nil {
return nil, err
}
}
// use credentials from sercretRef
secretRef := prov.Auth.SecretRef
if secretRef != nil {
log.V(1).Info("using credentials from secretRef")
creds, err = sessionFromSecretRef(ctx, prov, store, kube, namespace)
if err != nil {
return nil, err
}
}
config := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
if creds != nil {
config.WithCredentials(creds)
}
if prov.Region != "" {
config.WithRegion(prov.Region)
}
handlers := defaults.Handlers()
handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
sess, err := session.NewSessionWithOptions(session.Options{
Config: *config,
Handlers: handlers,
SharedConfigState: session.SharedConfigDisable,
})
if err != nil {
return nil, err
}
if prov.Role != "" {
stsclient := assumeRoler(sess)
sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, prov.Role))
}
log.Info("using aws session", "region", *sess.Config.Region, "credentials", creds)
return sess, nil
}
func sessionFromSecretRef(ctx context.Context, prov *esv1alpha1.AWSProvider, store esv1alpha1.GenericStore, kube client.Client, namespace string) (*credentials.Credentials, error) {
ke := client.ObjectKey{
Name: prov.Auth.SecretRef.AccessKeyID.Name,
Namespace: namespace, // default to ExternalSecret namespace
}
// only ClusterStore is allowed to set namespace (and then it's required)
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
if prov.Auth.SecretRef.AccessKeyID.Namespace == nil {
return nil, fmt.Errorf(errInvalidClusterStoreMissingAKIDNamespace)
}
ke.Namespace = *prov.Auth.SecretRef.AccessKeyID.Namespace
}
akSecret := v1.Secret{}
err := kube.Get(ctx, ke, &akSecret)
if err != nil {
return nil, fmt.Errorf(errFetchAKIDSecret, err)
}
ke = client.ObjectKey{
Name: prov.Auth.SecretRef.SecretAccessKey.Name,
Namespace: namespace, // default to ExternalSecret namespace
}
// only ClusterStore is allowed to set namespace (and then it's required)
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
if prov.Auth.SecretRef.SecretAccessKey.Namespace == nil {
return nil, fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
}
ke.Namespace = *prov.Auth.SecretRef.SecretAccessKey.Namespace
}
sakSecret := v1.Secret{}
err = kube.Get(ctx, ke, &sakSecret)
if err != nil {
return nil, fmt.Errorf(errFetchSAKSecret, err)
}
sak := string(sakSecret.Data[prov.Auth.SecretRef.SecretAccessKey.Key])
aks := string(akSecret.Data[prov.Auth.SecretRef.AccessKeyID.Key])
if sak == "" {
return nil, fmt.Errorf(errMissingSAK)
}
if aks == "" {
return nil, fmt.Errorf(errMissingAKID)
}
return credentials.NewStaticCredentials(aks, sak, ""), err
}
func sessionFromServiceAccount(ctx context.Context, prov *esv1alpha1.AWSProvider, store esv1alpha1.GenericStore, kube client.Client, namespace string, jwtProvider jwtProviderFactory) (*credentials.Credentials, error) {
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
namespace = *prov.Auth.JWTAuth.ServiceAccountRef.Namespace
}
name := prov.Auth.JWTAuth.ServiceAccountRef.Name
sa := v1.ServiceAccount{}
err := kube.Get(ctx, types.NamespacedName{
Name: name,
Namespace: namespace,
}, &sa)
if err != nil {
return nil, err
}
// the service account is expected to have a well-known annotation
// this is used as input to assumeRoleWithWebIdentity
roleArn := sa.Annotations[roleARNAnnotation]
if roleArn == "" {
return nil, fmt.Errorf("an IAM role must be associated with service account %s (namespace: %s)", name, namespace)
}
jwtProv, err := jwtProvider(name, namespace, roleArn, prov.Region)
if err != nil {
return nil, err
}
log.V(1).Info("using credentials via service account", "role", roleArn, "region", prov.Region)
return credentials.NewCredentials(jwtProv), nil
}
type jwtProviderFactory func(name, namespace, roleArn, region string) (credentials.Provider, error)
// DefaultJWTProvider returns a credentials.Provider that calls the AssumeRoleWithWebidentity
// controller-runtime/client does not support TokenRequest or other subresource APIs
// so we need to construct our own client and use it to fetch tokens.
func DefaultJWTProvider(name, namespace, roleArn, region string) (credentials.Provider, error) {
cfg, err := ctrlcfg.GetConfig()
if err != nil {
return nil, err
}
clientset, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, err
}
handlers := defaults.Handlers()
handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
awscfg := aws.NewConfig().WithEndpointResolver(ResolveEndpoint())
if region != "" {
awscfg.WithRegion(region)
}
sess, err := session.NewSessionWithOptions(session.Options{
Config: *awscfg,
SharedConfigState: session.SharedConfigDisable,
Handlers: handlers,
})
if err != nil {
return nil, err
}
tokenFetcher := &authTokenFetcher{
Namespace: namespace,
ServiceAccount: name,
k8sClient: clientset.CoreV1(),
}
return stscreds.NewWebIdentityRoleProviderWithToken(
sts.New(sess), roleArn, "external-secrets-provider-aws", tokenFetcher), nil
}
type STSProvider func(*session.Session) stsiface.STSAPI
func DefaultSTSProvider(sess *session.Session) stsiface.STSAPI {
return sts.New(sess)
}

View file

@ -0,0 +1,534 @@
/*
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 auth
import (
"context"
"os"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
awssess "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/aws-sdk-go/service/sts/stsiface"
"github.com/stretchr/testify/assert"
authv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
fakesess "github.com/external-secrets/external-secrets/pkg/provider/aws/auth/fake"
)
func TestNewSession(t *testing.T) {
rows := []TestSessionRow{
{
name: "nil store",
expectErr: "found nil store",
store: nil,
},
{
name: "not store spec",
expectErr: "storeSpec is missing provider",
store: &esv1alpha1.SecretStore{},
},
{
name: "store spec has no provider",
expectErr: "storeSpec is missing provider",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{},
},
},
{
name: "spec has no awssm field",
expectErr: "Missing AWS field",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{},
},
},
},
{
name: "configure aws using environment variables",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{},
},
},
},
env: map[string]string{
"AWS_ACCESS_KEY_ID": "1111",
"AWS_SECRET_ACCESS_KEY": "2222",
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "configure aws using environment variables + assume role",
stsProvider: func(*awssess.Session) stsiface.STSAPI {
return &fakesess.AssumeRoler{
AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
assert.Equal(t, *input.RoleArn, "foo-bar-baz")
return &sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("1123132"),
AssumedRoleId: aws.String("xxxxx"),
},
Credentials: &sts.Credentials{
AccessKeyId: aws.String("3333"),
SecretAccessKey: aws.String("4444"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
SessionToken: aws.String("6666"),
},
}, nil
},
}
},
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Role: "foo-bar-baz",
},
},
},
},
env: map[string]string{
"AWS_ACCESS_KEY_ID": "1111",
"AWS_SECRET_ACCESS_KEY": "2222",
},
expectProvider: true,
expectedKeyID: "3333",
expectedSecretKey: "4444",
},
{
name: "error out when secret with credentials does not exist",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: esv1alpha1.AWSAuth{
SecretRef: &esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "othersecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "othersecret",
Key: "two",
},
},
},
},
},
},
},
expectErr: `secrets "othersecret" not found`,
},
{
name: "use credentials from secret to configure aws",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: esv1alpha1.AWSAuth{
SecretRef: &esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
// Namespace is not set
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
// Namespace is not set
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "foo",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "error out when secret key does not exist",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: esv1alpha1.AWSAuth{
SecretRef: &esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "brokensecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "brokensecret",
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "brokensecret",
Namespace: "foo",
},
Data: map[string][]byte{},
},
},
expectErr: "missing SecretAccessKey",
},
{
name: "should not be able to access secrets from different namespace",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: esv1alpha1.AWSAuth{
SecretRef: &esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("evil"), // this should not be possible!
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("evil"),
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "evil",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectErr: `secrets "onesecret" not found`,
},
{
name: "ClusterStore should use credentials from a specific namespace",
namespace: "es-namespace",
store: &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion,
Kind: esv1alpha1.ClusterSecretStoreKind,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: esv1alpha1.AWSAuth{
SecretRef: &esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("platform-team-ns"),
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("platform-team-ns"),
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "platform-team-ns",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "namespace is mandatory when using ClusterStore with SecretKeySelector",
namespace: "es-namespace",
store: &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion,
Kind: esv1alpha1.ClusterSecretStoreKind,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: esv1alpha1.AWSAuth{
SecretRef: &esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Key: "two",
},
},
},
},
},
},
},
expectErr: "invalid ClusterSecretStore: missing AWS AccessKeyID Namespace",
},
{
name: "jwt auth via cluster secret store",
namespace: "es-namespace",
sa: &v1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "my-service-account",
Namespace: "other-ns",
Annotations: map[string]string{
roleARNAnnotation: "my-sa-role",
},
},
},
jwtProvider: func(name, namespace, roleArn, region string) (credentials.Provider, error) {
assert.Equal(t, "my-service-account", name)
assert.Equal(t, "other-ns", namespace)
assert.Equal(t, "my-sa-role", roleArn)
return fakesess.CredentialsProvider{
RetrieveFunc: func() (credentials.Value, error) {
return credentials.Value{
AccessKeyID: "3333",
SecretAccessKey: "4444",
SessionToken: "1234",
ProviderName: "fake",
}, nil
},
IsExpiredFunc: func() bool { return false },
}, nil
},
store: &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion,
Kind: esv1alpha1.ClusterSecretStoreKind,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: esv1alpha1.AWSAuth{
JWTAuth: &esv1alpha1.AWSJWTAuth{
ServiceAccountRef: &esmeta.ServiceAccountSelector{
Name: "my-service-account",
Namespace: aws.String("other-ns"),
},
},
},
},
},
},
},
expectProvider: true,
expectedKeyID: "3333",
expectedSecretKey: "4444",
},
}
for i := range rows {
row := rows[i]
t.Run(row.name, func(t *testing.T) {
testRow(t, row)
})
}
}
type TestSessionRow struct {
name string
store esv1alpha1.GenericStore
secrets []v1.Secret
sa *v1.ServiceAccount
jwtProvider jwtProviderFactory
namespace string
stsProvider STSProvider
expectProvider bool
expectErr string
expectedKeyID string
expectedSecretKey string
env map[string]string
}
func testRow(t *testing.T, row TestSessionRow) {
kc := clientfake.NewClientBuilder().Build()
for i := range row.secrets {
err := kc.Create(context.Background(), &row.secrets[i])
assert.Nil(t, err)
}
for k, v := range row.env {
os.Setenv(k, v)
}
if row.sa != nil {
err := kc.Create(context.Background(), row.sa)
assert.Nil(t, err)
}
err := kc.Create(context.Background(), &authv1.TokenRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "my-service-account",
Namespace: "other-ns",
},
})
assert.Nil(t, err)
defer func() {
for k := range row.env {
os.Unsetenv(k)
}
}()
s, err := New(context.Background(), row.store, kc, row.namespace, row.stsProvider, row.jwtProvider)
if !ErrorContains(err, row.expectErr) {
t.Errorf("expected error %s but found %s", row.expectErr, err.Error())
}
// pass test on expected error
if err != nil {
return
}
if row.expectProvider && s == nil {
t.Errorf("expected provider object, found nil")
return
}
creds, _ := s.Config.Credentials.Get()
assert.Equal(t, row.expectedKeyID, creds.AccessKeyID)
assert.Equal(t, row.expectedSecretKey, creds.SecretAccessKey)
}
func TestSMEnvCredentials(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
os.Setenv("AWS_SECRET_ACCESS_KEY", "1111")
os.Setenv("AWS_ACCESS_KEY_ID", "2222")
defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
defer os.Unsetenv("AWS_ACCESS_KEY_ID")
s, err := New(context.Background(), &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
// defaults
AWS: &esv1alpha1.AWSProvider{},
},
},
}, k8sClient, "example-ns", DefaultSTSProvider, nil)
assert.Nil(t, err)
assert.NotNil(t, s)
creds, err := s.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "2222")
assert.Equal(t, creds.SecretAccessKey, "1111")
}
func TestSMAssumeRole(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
sts := &fakesess.AssumeRoler{
AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
// make sure the correct role is passed in
assert.Equal(t, *input.RoleArn, "my-awesome-role")
return &sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("1123132"),
AssumedRoleId: aws.String("xxxxx"),
},
Credentials: &sts.Credentials{
AccessKeyId: aws.String("3333"),
SecretAccessKey: aws.String("4444"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
SessionToken: aws.String("6666"),
},
}, nil
},
}
os.Setenv("AWS_SECRET_ACCESS_KEY", "1111")
os.Setenv("AWS_ACCESS_KEY_ID", "2222")
defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
defer os.Unsetenv("AWS_ACCESS_KEY_ID")
s, err := New(context.Background(), &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
// do assume role!
AWS: &esv1alpha1.AWSProvider{
Role: "my-awesome-role",
},
},
},
}, k8sClient, "example-ns", func(se *awssess.Session) stsiface.STSAPI {
// check if the correct temporary credentials were used
creds, err := se.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "2222")
assert.Equal(t, creds.SecretAccessKey, "1111")
return sts
}, nil)
assert.Nil(t, err)
assert.NotNil(t, s)
creds, err := s.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "3333")
assert.Equal(t, creds.SecretAccessKey, "4444")
}
func ErrorContains(out error, want string) bool {
if out == nil {
return want == ""
}
if want == "" {
return false
}
return strings.Contains(out.Error(), want)
}

View file

@ -13,12 +13,36 @@ limitations under the License.
*/
package fake
import "github.com/aws/aws-sdk-go/service/sts"
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/aws/aws-sdk-go/service/sts/stsiface"
)
type AssumeRoler struct {
stsiface.STSAPI
AssumeRoleFunc func(*sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error)
}
func (f *AssumeRoler) AssumeRole(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
return f.AssumeRoleFunc(input)
}
func (f *AssumeRoler) AssumeRoleWithContext(ctx aws.Context, input *sts.AssumeRoleInput, opts ...request.Option) (*sts.AssumeRoleOutput, error) {
return f.AssumeRoleFunc(input)
}
type CredentialsProvider struct {
RetrieveFunc func() (credentials.Value, error)
IsExpiredFunc func() bool
}
func (t CredentialsProvider) Retrieve() (credentials.Value, error) {
return t.RetrieveFunc()
}
func (t CredentialsProvider) IsExpired() bool {
return t.IsExpiredFunc()
}

View file

@ -0,0 +1,54 @@
/*
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 auth
import (
"os"
"github.com/aws/aws-sdk-go/aws/endpoints"
)
const (
SecretsManagerEndpointEnv = "AWS_SECRETSMANAGER_ENDPOINT"
STSEndpointEnv = "AWS_STS_ENDPOINT"
SSMEndpointEnv = "AWS_SSM_ENDPOINT"
)
// ResolveEndpoint returns a ResolverFunc with
// customizable endpoints.
func ResolveEndpoint() endpoints.ResolverFunc {
customEndpoints := make(map[string]string)
if v := os.Getenv(SecretsManagerEndpointEnv); v != "" {
customEndpoints["secretsmanager"] = v
}
if v := os.Getenv(SSMEndpointEnv); v != "" {
customEndpoints["ssm"] = v
}
if v := os.Getenv(STSEndpointEnv); v != "" {
customEndpoints["sts"] = v
}
return ResolveEndpointWithServiceMap(customEndpoints)
}
func ResolveEndpointWithServiceMap(customEndpoints map[string]string) endpoints.ResolverFunc {
defaultResolver := endpoints.DefaultResolver()
return func(service, region string, opts ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
if ep, ok := customEndpoints[service]; ok {
return endpoints.ResolvedEndpoint{
URL: ep,
}, nil
}
return defaultResolver.EndpointFor(service, region, opts...)
}
}

View file

@ -0,0 +1,58 @@
/*
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 auth
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestResolver(t *testing.T) {
tbl := []struct {
env string
service string
url string
}{
{
env: SecretsManagerEndpointEnv,
service: "secretsmanager",
url: "http://sm.foo",
},
{
env: SSMEndpointEnv,
service: "ssm",
url: "http://ssm.foo",
},
{
env: STSEndpointEnv,
service: "sts",
url: "http://sts.foo",
},
}
for _, item := range tbl {
os.Setenv(item.env, item.url)
defer os.Unsetenv(item.env)
}
f := ResolveEndpoint()
for _, item := range tbl {
ep, err := f.EndpointFor(item.service, "")
assert.Nil(t, err)
assert.Equal(t, item.url, ep.URL)
}
}

View file

@ -0,0 +1,50 @@
/*
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 auth
import (
"fmt"
"github.com/aws/aws-sdk-go/aws/credentials"
authv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
// mostly taken from:
// https://github.com/aws/secrets-store-csi-driver-provider-aws/blob/main/auth/auth.go#L140-L145
const (
tokenAudience = "sts.amazonaws.com"
)
type authTokenFetcher struct {
Namespace string
ServiceAccount string
k8sClient corev1.CoreV1Interface
}
// FetchToken satisfies the stscreds.TokenFetcher interface
// it is used to generate service account tokens which are consumed by the aws sdk.
func (p authTokenFetcher) FetchToken(ctx credentials.Context) ([]byte, error) {
log.V(1).Info("fetching token", "ns", p.Namespace, "sa", p.ServiceAccount)
tokRsp, err := p.k8sClient.ServiceAccounts(p.Namespace).CreateToken(ctx, p.ServiceAccount, &authv1.TokenRequest{
Spec: authv1.TokenRequestSpec{
Audiences: []string{tokenAudience},
},
}, metav1.CreateOptions{})
if err != nil {
return nil, fmt.Errorf("error creating service account token: %w", err)
}
return []byte(tokRsp.Status.Token), nil
}

View file

@ -0,0 +1,63 @@
/*
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 auth
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
authv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8sv1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
func TestTokenFetcher(t *testing.T) {
tf := &authTokenFetcher{
ServiceAccount: "foobar",
Namespace: "example",
k8sClient: &mockK8sV1{},
}
token, err := tf.FetchToken(context.Background())
assert.Nil(t, err)
assert.Equal(t, []byte("FAKETOKEN"), token)
}
// Mock K8s client for creating tokens.
type mockK8sV1 struct {
k8sv1.CoreV1Interface
}
func (m *mockK8sV1) ServiceAccounts(namespace string) k8sv1.ServiceAccountInterface {
return &mockK8sV1SA{v1mock: m}
}
// Mock the K8s service account client.
type mockK8sV1SA struct {
k8sv1.ServiceAccountInterface
v1mock *mockK8sV1
}
func (ma *mockK8sV1SA) CreateToken(
ctx context.Context,
serviceAccountName string,
tokenRequest *authv1.TokenRequest,
opts metav1.CreateOptions,
) (*authv1.TokenRequest, error) {
return &authv1.TokenRequest{
Status: authv1.TokenRequestStatus{
Token: "FAKETOKEN",
},
}, nil
}

View file

@ -22,21 +22,11 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
fake "github.com/external-secrets/external-secrets/pkg/provider/aws/parameterstore/fake"
sess "github.com/external-secrets/external-secrets/pkg/provider/aws/session"
)
func TestConstructor(t *testing.T) {
s, err := sess.New("1111", "2222", "foo", "", nil)
assert.Nil(t, err)
c, err := New(s)
assert.Nil(t, err)
assert.NotNil(t, c.client)
}
type parameterstoreTestCase struct {
fakeClient *fake.Client
apiInput *ssm.GetParameterInput

View file

@ -17,57 +17,37 @@ package aws
import (
"context"
"fmt"
"os"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
v1 "k8s.io/api/core/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
"github.com/external-secrets/external-secrets/pkg/provider"
awsauth "github.com/external-secrets/external-secrets/pkg/provider/aws/auth"
"github.com/external-secrets/external-secrets/pkg/provider/aws/parameterstore"
"github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager"
awssess "github.com/external-secrets/external-secrets/pkg/provider/aws/session"
"github.com/external-secrets/external-secrets/pkg/provider/aws/util"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
)
// Provider satisfies the provider interface.
type Provider struct{}
var log = ctrl.Log.WithName("provider").WithName("aws")
const (
SecretsManagerEndpointEnv = "AWS_SECRETSMANAGER_ENDPOINT"
STSEndpointEnv = "AWS_STS_ENDPOINT"
SSMEndpointEnv = "AWS_SSM_ENDPOINT"
errUnableCreateSession = "unable to create session: %w"
errUnknownProviderService = "unknown AWS Provider Service: %s"
errInvalidClusterStoreMissingAKIDNamespace = "invalid ClusterSecretStore: missing AWS AccessKeyID Namespace"
errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing AWS SecretAccessKey Namespace"
errFetchAKIDSecret = "could not fetch accessKeyID secret: %w"
errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w"
errMissingSAK = "missing SecretAccessKey"
errMissingAKID = "missing AccessKeyID"
errNilStore = "found nil store"
errMissingStoreSpec = "store is missing spec"
errMissingProvider = "storeSpec is missing provider"
errInvalidProvider = "invalid provider spec. Missing AWS field in store %s"
errUnableCreateSession = "unable to create session: %w"
errUnknownProviderService = "unknown AWS Provider Service: %s"
)
// 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, awssess.DefaultSTSProvider)
return newClient(ctx, store, kube, namespace, awsauth.DefaultSTSProvider)
}
func newClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string, assumeRoler awssess.STSProvider) (provider.SecretsClient, error) {
prov, err := getAWSProvider(store)
func newClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string, assumeRoler awsauth.STSProvider) (provider.SecretsClient, error) {
prov, err := util.GetAWSProvider(store)
if err != nil {
return nil, err
}
sess, err := newSession(ctx, store, kube, namespace, assumeRoler)
sess, err := awsauth.New(ctx, store, kube, namespace, assumeRoler, awsauth.DefaultJWTProvider)
if err != nil {
return nil, fmt.Errorf(errUnableCreateSession, err)
}
@ -80,114 +60,6 @@ func newClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.C
return nil, fmt.Errorf(errUnknownProviderService, prov.Service)
}
// newSession creates a new aws session based on a store
// it looks up credentials at the provided secrets.
func newSession(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string, assumeRoler awssess.STSProvider) (*session.Session, error) {
prov, err := getAWSProvider(store)
if err != nil {
return nil, err
}
var sak, aks string
// use provided credentials via secret reference
if prov.Auth != nil {
log.V(1).Info("fetching secrets for authentication")
ke := client.ObjectKey{
Name: prov.Auth.SecretRef.AccessKeyID.Name,
Namespace: namespace, // default to ExternalSecret namespace
}
// only ClusterStore is allowed to set namespace (and then it's required)
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
if prov.Auth.SecretRef.AccessKeyID.Namespace == nil {
return nil, fmt.Errorf(errInvalidClusterStoreMissingAKIDNamespace)
}
ke.Namespace = *prov.Auth.SecretRef.AccessKeyID.Namespace
}
akSecret := v1.Secret{}
err := kube.Get(ctx, ke, &akSecret)
if err != nil {
return nil, fmt.Errorf(errFetchAKIDSecret, err)
}
ke = client.ObjectKey{
Name: prov.Auth.SecretRef.SecretAccessKey.Name,
Namespace: namespace, // default to ExternalSecret namespace
}
// only ClusterStore is allowed to set namespace (and then it's required)
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
if prov.Auth.SecretRef.SecretAccessKey.Namespace == nil {
return nil, fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace)
}
ke.Namespace = *prov.Auth.SecretRef.SecretAccessKey.Namespace
}
sakSecret := v1.Secret{}
err = kube.Get(ctx, ke, &sakSecret)
if err != nil {
return nil, fmt.Errorf(errFetchSAKSecret, err)
}
sak = string(sakSecret.Data[prov.Auth.SecretRef.SecretAccessKey.Key])
aks = string(akSecret.Data[prov.Auth.SecretRef.AccessKeyID.Key])
if sak == "" {
return nil, fmt.Errorf(errMissingSAK)
}
if aks == "" {
return nil, fmt.Errorf(errMissingAKID)
}
}
session, err := awssess.New(sak, aks, prov.Region, prov.Role, assumeRoler)
if err != nil {
return nil, err
}
session.Config.EndpointResolver = ResolveEndpoint()
return session, nil
}
// getAWSProvider does the necessary nil checks on the generic store
// it returns the aws provider or an error.
func getAWSProvider(store esv1alpha1.GenericStore) (*esv1alpha1.AWSProvider, error) {
if store == nil {
return nil, fmt.Errorf(errNilStore)
}
spc := store.GetSpec()
if spc == nil {
return nil, fmt.Errorf(errMissingStoreSpec)
}
if spc.Provider == nil {
return nil, fmt.Errorf(errMissingProvider)
}
prov := spc.Provider.AWS
if prov == nil {
return nil, fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String())
}
return prov, nil
}
// ResolveEndpoint returns a ResolverFunc with
// customizable endpoints.
func ResolveEndpoint() endpoints.ResolverFunc {
customEndpoints := make(map[string]string)
if v := os.Getenv(SecretsManagerEndpointEnv); v != "" {
customEndpoints["secretsmanager"] = v
}
if v := os.Getenv(SSMEndpointEnv); v != "" {
customEndpoints["ssm"] = v
}
if v := os.Getenv(STSEndpointEnv); v != "" {
customEndpoints["sts"] = v
}
return ResolveEndpointWithServiceMap(customEndpoints)
}
func ResolveEndpointWithServiceMap(customEndpoints map[string]string) endpoints.ResolverFunc {
defaultResolver := endpoints.DefaultResolver()
return func(service, region string, opts ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) {
if ep, ok := customEndpoints[service]; ok {
return endpoints.ResolvedEndpoint{
URL: ep,
}, nil
}
return defaultResolver.EndpointFor(service, region, opts...)
}
}
func init() {
schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{},

View file

@ -16,26 +16,16 @@ package aws
import (
"context"
"os"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
awssess "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider/aws/parameterstore"
"github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager"
session "github.com/external-secrets/external-secrets/pkg/provider/aws/session"
fakesess "github.com/external-secrets/external-secrets/pkg/provider/aws/session/fake"
)
func TestProvider(t *testing.T) {
@ -118,8 +108,8 @@ func TestProvider(t *testing.T) {
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Service: esv1alpha1.AWSServiceParameterStore,
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
Auth: esv1alpha1.AWSAuth{
SecretRef: &esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "foo",
Namespace: aws.String("NOOP"),
@ -147,472 +137,3 @@ func TestProvider(t *testing.T) {
})
}
}
func TestNewSession(t *testing.T) {
rows := []TestSessionRow{
{
name: "nil store",
expectErr: "found nil store",
store: nil,
},
{
name: "not store spec",
expectErr: "storeSpec is missing provider",
store: &esv1alpha1.SecretStore{},
},
{
name: "store spec has no provider",
expectErr: "storeSpec is missing provider",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{},
},
},
{
name: "spec has no awssm field",
expectErr: "Missing AWS field",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{},
},
},
},
{
name: "configure aws using environment variables",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{},
},
},
},
env: map[string]string{
"AWS_ACCESS_KEY_ID": "1111",
"AWS_SECRET_ACCESS_KEY": "2222",
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "configure aws using environment variables + assume role",
stsProvider: func(*awssess.Session) stscreds.AssumeRoler {
return &fakesess.AssumeRoler{
AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
assert.Equal(t, *input.RoleArn, "foo-bar-baz")
return &sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("1123132"),
AssumedRoleId: aws.String("xxxxx"),
},
Credentials: &sts.Credentials{
AccessKeyId: aws.String("3333"),
SecretAccessKey: aws.String("4444"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
SessionToken: aws.String("6666"),
},
}, nil
},
}
},
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Role: "foo-bar-baz",
},
},
},
},
env: map[string]string{
"AWS_ACCESS_KEY_ID": "1111",
"AWS_SECRET_ACCESS_KEY": "2222",
},
expectProvider: true,
expectedKeyID: "3333",
expectedSecretKey: "4444",
},
{
name: "error out when secret with credentials does not exist",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "othersecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "othersecret",
Key: "two",
},
},
},
},
},
},
},
expectErr: `secrets "othersecret" not found`,
},
{
name: "use credentials from secret to configure aws",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
// Namespace is not set
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
// Namespace is not set
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "foo",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "error out when secret key does not exist",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "brokensecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "brokensecret",
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "brokensecret",
Namespace: "foo",
},
Data: map[string][]byte{},
},
},
expectErr: "missing SecretAccessKey",
},
{
name: "should not be able to access secrets from different namespace",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("evil"), // this should not be possible!
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("evil"),
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "evil",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectErr: `secrets "onesecret" not found`,
},
{
name: "ClusterStore should use credentials from a specific namespace",
namespace: "es-namespace",
store: &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion,
Kind: esv1alpha1.ClusterSecretStoreKind,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("platform-team-ns"),
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("platform-team-ns"),
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "platform-team-ns",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "namespace is mandatory when using ClusterStore with SecretKeySelector",
namespace: "es-namespace",
store: &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion,
Kind: esv1alpha1.ClusterSecretStoreKind,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Key: "two",
},
},
},
},
},
},
},
expectErr: "invalid ClusterSecretStore: missing AWS AccessKeyID Namespace",
},
}
for i := range rows {
row := rows[i]
t.Run(row.name, func(t *testing.T) {
testRow(t, row)
})
}
}
type TestSessionRow struct {
name string
store esv1alpha1.GenericStore
secrets []v1.Secret
namespace string
stsProvider session.STSProvider
expectProvider bool
expectErr string
expectedKeyID string
expectedSecretKey string
env map[string]string
}
func testRow(t *testing.T, row TestSessionRow) {
kc := clientfake.NewClientBuilder().Build()
for i := range row.secrets {
err := kc.Create(context.Background(), &row.secrets[i])
assert.Nil(t, err)
}
for k, v := range row.env {
os.Setenv(k, v)
}
defer func() {
for k := range row.env {
os.Unsetenv(k)
}
}()
s, err := newSession(context.Background(), row.store, kc, row.namespace, row.stsProvider)
if !ErrorContains(err, row.expectErr) {
t.Errorf("expected error %s but found %s", row.expectErr, err.Error())
}
// pass test on expected error
if err != nil {
return
}
if row.expectProvider && s == nil {
t.Errorf("expected provider object, found nil")
return
}
creds, _ := s.Config.Credentials.Get()
assert.Equal(t, creds.AccessKeyID, row.expectedKeyID)
assert.Equal(t, creds.SecretAccessKey, row.expectedSecretKey)
}
func TestSMEnvCredentials(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
os.Setenv("AWS_SECRET_ACCESS_KEY", "1111")
os.Setenv("AWS_ACCESS_KEY_ID", "2222")
defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
defer os.Unsetenv("AWS_ACCESS_KEY_ID")
s, err := newSession(context.Background(), &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
// defaults
AWS: &esv1alpha1.AWSProvider{},
},
},
}, k8sClient, "example-ns", session.DefaultSTSProvider)
assert.Nil(t, err)
assert.NotNil(t, s)
creds, err := s.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "2222")
assert.Equal(t, creds.SecretAccessKey, "1111")
}
func TestSMAssumeRole(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
sts := &fakesess.AssumeRoler{
AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
// make sure the correct role is passed in
assert.Equal(t, *input.RoleArn, "my-awesome-role")
return &sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("1123132"),
AssumedRoleId: aws.String("xxxxx"),
},
Credentials: &sts.Credentials{
AccessKeyId: aws.String("3333"),
SecretAccessKey: aws.String("4444"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
SessionToken: aws.String("6666"),
},
}, nil
},
}
os.Setenv("AWS_SECRET_ACCESS_KEY", "1111")
os.Setenv("AWS_ACCESS_KEY_ID", "2222")
defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
defer os.Unsetenv("AWS_ACCESS_KEY_ID")
s, err := newSession(context.Background(), &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
// do assume role!
AWS: &esv1alpha1.AWSProvider{
Role: "my-awesome-role",
},
},
},
}, k8sClient, "example-ns", func(se *awssess.Session) stscreds.AssumeRoler {
// check if the correct temporary credentials were used
creds, err := se.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "2222")
assert.Equal(t, creds.SecretAccessKey, "1111")
return sts
})
assert.Nil(t, err)
assert.NotNil(t, s)
creds, err := s.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "3333")
assert.Equal(t, creds.SecretAccessKey, "4444")
}
func TestResolver(t *testing.T) {
tbl := []struct {
env string
service string
url string
}{
{
env: SecretsManagerEndpointEnv,
service: "secretsmanager",
url: "http://sm.foo",
},
{
env: SSMEndpointEnv,
service: "ssm",
url: "http://ssm.foo",
},
{
env: STSEndpointEnv,
service: "sts",
url: "http://sts.foo",
},
}
for _, item := range tbl {
os.Setenv(item.env, item.url)
defer os.Unsetenv(item.env)
}
f := ResolveEndpoint()
for _, item := range tbl {
ep, err := f.EndpointFor(item.service, "")
assert.Nil(t, err)
assert.Equal(t, item.url, ep.URL)
}
}
func ErrorContains(out error, want string) bool {
if out == nil {
return want == ""
}
if want == "" {
return false
}
return strings.Contains(out.Error(), want)
}

View file

@ -22,21 +22,11 @@ import (
"github.com/aws/aws-sdk-go/aws"
awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
fakesm "github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager/fake"
sess "github.com/external-secrets/external-secrets/pkg/provider/aws/session"
)
func TestConstructor(t *testing.T) {
s, err := sess.New("1111", "2222", "foo", "", nil)
assert.Nil(t, err)
c, err := New(s)
assert.Nil(t, err)
assert.NotNil(t, c.client)
}
type secretsManagerTestCase struct {
fakeClient *fakesm.Client
apiInput *awssm.GetSecretValueInput

View file

@ -1,71 +0,0 @@
/*
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 session
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/request"
awssess "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
ctrl "sigs.k8s.io/controller-runtime"
)
// Config contains configuration to create a new AWS provider.
type Config struct {
AssumeRole string
Region string
APIRetries int
}
var log = ctrl.Log.WithName("provider").WithName("aws")
// New creates a new aws session based on the supported input methods.
// https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials
func New(sak, aks, region, role string, stsprovider STSProvider) (*awssess.Session, error) {
config := aws.NewConfig()
sessionOpts := awssess.Options{
Config: *config,
}
if sak != "" && aks != "" {
sessionOpts.Config.Credentials = credentials.NewStaticCredentials(aks, sak, "")
sessionOpts.SharedConfigState = awssess.SharedConfigDisable
}
sess, err := awssess.NewSessionWithOptions(sessionOpts)
if err != nil {
return nil, fmt.Errorf("unable to create aws session: %w", err)
}
if region != "" {
log.V(1).Info("using region", "region", region)
sess.Config.WithRegion(region)
}
if role != "" {
log.V(1).Info("assuming role", "role", role)
stsclient := stsprovider(sess)
sess.Config.WithCredentials(stscreds.NewCredentialsWithClient(stsclient, role))
}
sess.Handlers.Build.PushBack(request.WithAppendUserAgent("external-secrets"))
return sess, nil
}
type STSProvider func(*awssess.Session) stscreds.AssumeRoler
func DefaultSTSProvider(sess *awssess.Session) stscreds.AssumeRoler {
return sts.New(sess)
}

View file

@ -1,91 +0,0 @@
/*
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 session
import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/stretchr/testify/assert"
fakesess "github.com/external-secrets/external-secrets/pkg/provider/aws/session/fake"
)
func TestSession(t *testing.T) {
tbl := []struct {
test string
aks string
sak string
region string
role string
sts STSProvider
expectedKeyID string
expectedSecretKey string
}{
{
test: "test default role provider",
aks: "2222",
sak: "1111",
region: "xxxxx",
role: "",
sts: DefaultSTSProvider,
expectedSecretKey: "1111",
expectedKeyID: "2222",
},
{
test: "test custom sts provider",
aks: "1111",
sak: "2222",
region: "xxxxx",
role: "zzzzz",
sts: func(*session.Session) stscreds.AssumeRoler {
return &fakesess.AssumeRoler{
AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
assert.Equal(t, *input.RoleArn, "zzzzz")
return &sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("1123132"),
AssumedRoleId: aws.String("xxxxx"),
},
Credentials: &sts.Credentials{
SecretAccessKey: aws.String("3333"),
AccessKeyId: aws.String("4444"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
SessionToken: aws.String("6666"),
},
}, nil
},
}
},
expectedSecretKey: "3333",
expectedKeyID: "4444",
},
}
for i := range tbl {
row := tbl[i]
t.Run(row.test, func(t *testing.T) {
sess, err := New(row.sak, row.aks, row.region, row.role, row.sts)
assert.Nil(t, err)
creds, err := sess.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, row.expectedKeyID, creds.AccessKeyID)
assert.Equal(t, row.expectedSecretKey, creds.SecretAccessKey)
})
}
}

View file

@ -16,17 +16,14 @@ package util
import (
"errors"
"fmt"
"github.com/aws/aws-sdk-go/aws/awserr"
"regexp"
)
// SanitizeErr removes sanitizes the error string
var regexReqID = regexp.MustCompile(`request id: (\S+)`)
// SanitizeErr sanitizes the error string
// because the requestID must not be included in the error.
// otherwise the secrets keeps syncing.
func SanitizeErr(err error) error {
var bErr awserr.BatchedErrors
if errors.As(bErr, &bErr) {
return fmt.Errorf("%s: %s", bErr.Code(), bErr.Message())
}
return err
return errors.New(string(regexReqID.ReplaceAll([]byte(err.Error()), nil)))
}

View file

@ -0,0 +1,42 @@
/*
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 util
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSanitize(t *testing.T) {
tbl := []struct {
err error
expected string
}{
{
err: errors.New("some AccessDeniedException: User: arn:aws:sts::123123123123:assumed-role/foobar is not authorized to perform: secretsmanager:GetSecretValue on resource: example\n\tstatus code: 400, request id: df34-75f-0c5f-4b4c-a71a-f93d581d177c"),
expected: "some AccessDeniedException: User: arn:aws:sts::123123123123:assumed-role/foobar is not authorized to perform: secretsmanager:GetSecretValue on resource: example\n\tstatus code: 400, ",
},
{
err: errors.New("some generic error"),
expected: "some generic error",
},
}
for _, c := range tbl {
out := SanitizeErr(c.err)
assert.Equal(t, c.expected, out.Error())
}
}

View file

@ -0,0 +1,47 @@
/*
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 util
import (
"fmt"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
)
const (
errNilStore = "found nil store"
errMissingStoreSpec = "store is missing spec"
errMissingProvider = "storeSpec is missing provider"
errInvalidProvider = "invalid provider spec. Missing AWS field in store %s"
)
// GetAWSProvider does the necessary nil checks on the generic store
// it returns the aws provider or an error.
func GetAWSProvider(store esv1alpha1.GenericStore) (*esv1alpha1.AWSProvider, error) {
if store == nil {
return nil, fmt.Errorf(errNilStore)
}
spc := store.GetSpec()
if spc == nil {
return nil, fmt.Errorf(errMissingStoreSpec)
}
if spc.Provider == nil {
return nil, fmt.Errorf(errMissingProvider)
}
prov := spc.Provider.AWS
if prov == nil {
return nil, fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String())
}
return prov, nil
}