mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
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>
This commit is contained in:
parent
76c7f3b5b0
commit
00d66e0bc4
4 changed files with 79 additions and 14 deletions
|
@ -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):
|
||||
|
|
|
@ -16,6 +16,7 @@ package constants
|
|||
const (
|
||||
ProviderAWSSM = "AWS/SecretsManager"
|
||||
CallAWSSMGetSecretValue = "GetSecretValue"
|
||||
CallAWSPSGetParametersByPath = "GetParametersByPath"
|
||||
CallAWSSMDescribeSecret = "DescribeSecret"
|
||||
CallAWSSMDeleteSecret = "DeleteSecret"
|
||||
CallAWSSMCreateSecret = "CreateSecret"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue