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:
commit
8827e3ab92
34 changed files with 1451 additions and 843 deletions
4
.github/workflows/e2e.yml
vendored
4
.github/workflows/e2e.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -51,6 +51,12 @@ rules:
|
|||
- "create"
|
||||
- "update"
|
||||
- "delete"
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- "serviceaccounts/token"
|
||||
verbs:
|
||||
- "create"
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
BIN
docs/pictures/diagrams-provider-aws-auth-pod-identity.png
Normal file
BIN
docs/pictures/diagrams-provider-aws-auth-pod-identity.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
BIN
docs/pictures/diagrams-provider-aws-auth-secret-ref.png
Normal file
BIN
docs/pictures/diagrams-provider-aws-auth-secret-ref.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
BIN
docs/pictures/diagrams-provider-aws-auth-service-account.png
Normal file
BIN
docs/pictures/diagrams-provider-aws-auth-service-account.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 71 KiB |
File diff suppressed because one or more lines are too long
|
@ -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
|
||||
```
|
||||
|
|
49
docs/spec.md
49
docs/spec.md
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
image:
|
||||
tag: "0.12.14"
|
||||
service:
|
||||
type: ClusterIP
|
||||
edgeService:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
})
|
||||
|
|
231
pkg/provider/aws/auth/auth.go
Normal file
231
pkg/provider/aws/auth/auth.go
Normal 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)
|
||||
}
|
534
pkg/provider/aws/auth/auth_test.go
Normal file
534
pkg/provider/aws/auth/auth_test.go
Normal 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)
|
||||
}
|
|
@ -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()
|
||||
}
|
54
pkg/provider/aws/auth/resolver.go
Normal file
54
pkg/provider/aws/auth/resolver.go
Normal 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...)
|
||||
}
|
||||
}
|
58
pkg/provider/aws/auth/resolver_test.go
Normal file
58
pkg/provider/aws/auth/resolver_test.go
Normal 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)
|
||||
}
|
||||
}
|
50
pkg/provider/aws/auth/token_fetcher.go
Normal file
50
pkg/provider/aws/auth/token_fetcher.go
Normal 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
|
||||
}
|
63
pkg/provider/aws/auth/token_fetcher_test.go
Normal file
63
pkg/provider/aws/auth/token_fetcher_test.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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{},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
|
|
42
pkg/provider/aws/util/errors_test.go
Normal file
42
pkg/provider/aws/util/errors_test.go
Normal 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())
|
||||
}
|
||||
}
|
47
pkg/provider/aws/util/provider.go
Normal file
47
pkg/provider/aws/util/provider.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue