Browse Source

Aws ssm parameterstore issue 1839 (#2350)

* update documentation

Signed-off-by: Luke Arntz <luke@blue42.net>

* default to GetParametersByPathWithContext

Add GetParametersByPathWithContext. To maintain backward compatibility moved the original `findByname` function to `fallbackFindByName` and created a new `findByName` function that uses the `GetParametersByPathWithContext` API call.

In function `findByName`, if we receive an `AccessDeniedException` when calling GetParametersByPathWithContext `return pm.fallbackFindByName(ctx, ref)`.

Signed-off-by: Luke Arntz <luke@blue42.net>

* feat: notify users about ssm permission improvements

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

* fix: get parameters recursively and decrypt them

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>

---------

Signed-off-by: Luke Arntz <luke@blue42.net>
Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Co-authored-by: Moritz Johner <beller.moritz@googlemail.com>
Luke Arntz 2 years ago
parent
commit
00d66e0bc4

+ 7 - 6
docs/provider/aws-parameter-store.md

@@ -21,7 +21,9 @@ way users of the `SecretStore` can only access the secrets necessary.
 
 ### IAM Policy
 
-Create a IAM Policy to pin down access to secrets matching `dev-*`, for further information see [AWS Documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html):
+The example policy below shows the minimum required permissions for fetching SSM parameters. This policy permits pinning down access to secrets with a path matching `dev-*`. Other operations may require additional permission. For example, finding parameters based on tags will also require `ssm:DescribeParameters` and `tag:GetResources` permission with `"Resource": "*"`. Generally, the specific permission required will be logged as an error if an operation fails.
+
+For further information see [AWS Documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html).
 
 ``` json
 {
@@ -30,15 +32,14 @@ Create a IAM Policy to pin down access to secrets matching `dev-*`, for further
     {
       "Effect": "Allow",
       "Action": [
-        "ssm:GetParameter",
-        "ssm:ListTagsForResource",
-        "ssm:DescribeParameters"
+        "ssm:GetParameter*",
       ],
       "Resource": "arn:aws:ssm:us-east-2:1234567889911:parameter/dev-*"
     }
   ]
 }
 ```
+
 ### JSON Secret Values
 
 You can store JSON objects in a parameter. You can access nested values or arrays using [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md):
@@ -76,13 +77,13 @@ spec:
   # metadataPolicy to fetch all the tags in JSON format
   - secretKey: tags
     remoteRef:
-      metadataPolicy: Fetch 
+      metadataPolicy: Fetch
       key: database-credentials
 
   # metadataPolicy to fetch a specific tag (dev) from the source secret
   - secretKey: developer
     remoteRef:
-      metadataPolicy: Fetch 
+      metadataPolicy: Fetch
       key: database-credentials
       property: dev
 ```

+ 8 - 7
pkg/constants/constants.go

@@ -14,13 +14,14 @@ limitations under the License.
 package constants
 
 const (
-	ProviderAWSSM           = "AWS/SecretsManager"
-	CallAWSSMGetSecretValue = "GetSecretValue"
-	CallAWSSMDescribeSecret = "DescribeSecret"
-	CallAWSSMDeleteSecret   = "DeleteSecret"
-	CallAWSSMCreateSecret   = "CreateSecret"
-	CallAWSSMPutSecretValue = "PutSecretValue"
-	CallAWSSMListSecrets    = "ListSecrets"
+	ProviderAWSSM                = "AWS/SecretsManager"
+	CallAWSSMGetSecretValue      = "GetSecretValue"
+	CallAWSPSGetParametersByPath = "GetParametersByPath"
+	CallAWSSMDescribeSecret      = "DescribeSecret"
+	CallAWSSMDeleteSecret        = "DeleteSecret"
+	CallAWSSMCreateSecret        = "CreateSecret"
+	CallAWSSMPutSecretValue      = "PutSecretValue"
+	CallAWSSMListSecrets         = "ListSecrets"
 
 	ProviderAWSPS                = "AWS/ParameterStore"
 	CallAWSPSGetParameter        = "GetParameter"

+ 6 - 0
pkg/provider/aws/parameterstore/fake/fake.go

@@ -26,6 +26,7 @@ import (
 // Client implements the aws parameterstore interface.
 type Client struct {
 	GetParameterWithContextFn        GetParameterWithContextFn
+	GetParametersByPathWithContextFn GetParametersByPathWithContextFn
 	PutParameterWithContextFn        PutParameterWithContextFn
 	DeleteParameterWithContextFn     DeleteParameterWithContextFn
 	DescribeParametersWithContextFn  DescribeParametersWithContextFn
@@ -33,6 +34,7 @@ type Client struct {
 }
 
 type GetParameterWithContextFn func(aws.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error)
+type GetParametersByPathWithContextFn func(aws.Context, *ssm.GetParametersByPathInput, ...request.Option) (*ssm.GetParametersByPathOutput, error)
 type PutParameterWithContextFn func(aws.Context, *ssm.PutParameterInput, ...request.Option) (*ssm.PutParameterOutput, error)
 type DescribeParametersWithContextFn func(aws.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error)
 type ListTagsForResourceWithContextFn func(aws.Context, *ssm.ListTagsForResourceInput, ...request.Option) (*ssm.ListTagsForResourceOutput, error)
@@ -62,6 +64,10 @@ func (sm *Client) GetParameterWithContext(ctx aws.Context, input *ssm.GetParamet
 	return sm.GetParameterWithContextFn(ctx, input, options...)
 }
 
+func (sm *Client) GetParametersByPathWithContext(ctx aws.Context, input *ssm.GetParametersByPathInput, options ...request.Option) (*ssm.GetParametersByPathOutput, error) {
+	return sm.GetParametersByPathWithContextFn(ctx, input, options...)
+}
+
 func NewGetParameterWithContextFn(output *ssm.GetParameterOutput, err error) GetParameterWithContextFn {
 	return func(aws.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error) {
 		return output, err

+ 58 - 1
pkg/provider/aws/parameterstore/parameterstore.go

@@ -27,6 +27,7 @@ import (
 	"github.com/aws/aws-sdk-go/service/ssm"
 	"github.com/tidwall/gjson"
 	utilpointer "k8s.io/utils/pointer"
+	ctrl "sigs.k8s.io/controller-runtime"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	"github.com/external-secrets/external-secrets/pkg/constants"
@@ -40,6 +41,7 @@ var (
 	_               esv1beta1.SecretsClient = &ParameterStore{}
 	managedBy                               = "managed-by"
 	externalSecrets                         = "external-secrets"
+	logger                                  = ctrl.Log.WithName("provider").WithName("parameterstore")
 )
 
 // ParameterStore is a provider for AWS ParameterStore.
@@ -53,6 +55,7 @@ type ParameterStore struct {
 // see: https://docs.aws.amazon.com/sdk-for-go/api/service/ssm/ssmiface/
 type PMInterface interface {
 	GetParameterWithContext(aws.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error)
+	GetParametersByPathWithContext(aws.Context, *ssm.GetParametersByPathInput, ...request.Option) (*ssm.GetParametersByPathOutput, error)
 	PutParameterWithContext(aws.Context, *ssm.PutParameterInput, ...request.Option) (*ssm.PutParameterOutput, error)
 	DescribeParametersWithContext(aws.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error)
 	ListTagsForResourceWithContext(aws.Context, *ssm.ListTagsForResourceInput, ...request.Option) (*ssm.ListTagsForResourceOutput, error)
@@ -61,6 +64,7 @@ type PMInterface interface {
 
 const (
 	errUnexpectedFindOperator = "unexpected find operator"
+	errAccessDeniedException  = "AccessDeniedException"
 )
 
 // New constructs a ParameterStore Provider that is specific to a store.
@@ -219,11 +223,64 @@ func (pm *ParameterStore) GetAllSecrets(ctx context.Context, ref esv1beta1.Exter
 	return nil, errors.New(errUnexpectedFindOperator)
 }
 
+// findByName requires `ssm:GetParametersByPath` IAM permission, but the `Resource` scope can be limited.
 func (pm *ParameterStore) findByName(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 	matcher, err := find.New(*ref.Name)
 	if err != nil {
 		return nil, err
 	}
+	if ref.Path == nil {
+		ref.Path = aws.String("/")
+	}
+	data := make(map[string][]byte)
+	var nextToken *string
+	for {
+		it, err := pm.client.GetParametersByPathWithContext(
+			ctx,
+			&ssm.GetParametersByPathInput{
+				NextToken:      nextToken,
+				Path:           ref.Path,
+				Recursive:      aws.Bool(true),
+				WithDecryption: aws.Bool(true),
+			})
+		metrics.ObserveAPICall(constants.ProviderAWSPS, constants.CallAWSPSGetParametersByPath, err)
+		if err != nil {
+			/*
+				Check for AccessDeniedException when calling `GetParametersByPathWithContext`. If so,
+				use fallbackFindByName and `DescribeParametersWithContext`.
+				https://github.com/external-secrets/external-secrets/issues/1839#issuecomment-1489023522
+			*/
+			var awsError awserr.Error
+			if errors.As(err, &awsError) && awsError.Code() == errAccessDeniedException {
+				logger.Info("GetParametersByPath: access denied. using fallback to describe parameters. It is recommended to add ssm:GetParametersByPath permissions", "path", ref.Path)
+				return pm.fallbackFindByName(ctx, ref)
+			}
+			return nil, err
+		}
+		for _, param := range it.Parameters {
+			if !matcher.MatchName(*param.Name) {
+				continue
+			}
+			err = pm.fetchAndSet(ctx, data, *param.Name)
+			if err != nil {
+				return nil, err
+			}
+		}
+		nextToken = it.NextToken
+		if nextToken == nil {
+			break
+		}
+	}
+
+	return data, nil
+}
+
+// fallbackFindByName requires `ssm:DescribeParameters` IAM permission on `"Resource": "*"`.
+func (pm *ParameterStore) fallbackFindByName(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	matcher, err := find.New(*ref.Name)
+	if err != nil {
+		return nil, err
+	}
 	pathFilter := make([]*ssm.ParameterStringFilter, 0)
 	if ref.Path != nil {
 		pathFilter = append(pathFilter, &ssm.ParameterStringFilter{
@@ -259,10 +316,10 @@ func (pm *ParameterStore) findByName(ctx context.Context, ref esv1beta1.External
 			break
 		}
 	}
-
 	return data, nil
 }
 
+// findByTags requires ssm:DescribeParameters,tag:GetResources IAM permission on `"Resource": "*"`.
 func (pm *ParameterStore) findByTags(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
 	filters := make([]*ssm.ParameterStringFilter, 0)
 	for k, v := range ref.Tags {