diff --git a/docs/provider/aws-parameter-store.md b/docs/provider/aws-parameter-store.md index a794589ca..cf7be0b0b 100644 --- a/docs/provider/aws-parameter-store.md +++ b/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 ``` diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index dab58338f..04225764f 100644 --- a/pkg/constants/constants.go +++ b/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" diff --git a/pkg/provider/aws/parameterstore/fake/fake.go b/pkg/provider/aws/parameterstore/fake/fake.go index 2897df144..447b38f5c 100644 --- a/pkg/provider/aws/parameterstore/fake/fake.go +++ b/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 diff --git a/pkg/provider/aws/parameterstore/parameterstore.go b/pkg/provider/aws/parameterstore/parameterstore.go index e096e3fcd..04014a2cd 100644 --- a/pkg/provider/aws/parameterstore/parameterstore.go +++ b/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,7 +223,60 @@ 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 @@ -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 {