diff --git a/docs/backends.md b/docs/backends.md index 67c7a5f7..bcb2b520 100644 --- a/docs/backends.md +++ b/docs/backends.md @@ -341,6 +341,92 @@ For cross account access there is the need to configure the correct permissions https://aws.amazon.com/premiumsupport/knowledge-center/secrets-manager-share-between-accounts https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access_examples_cross.html +### AWS System Manager Parameter Store + +##### AWS Authentication +Refer to the [AWS SDK for Go V2 +documentation](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials) for +supplying AWS credentials. Supported credentials and the order in which they are loaded are +described [here](https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/#specifying-credentials). + +These are the parameters for AWS: +``` +AVP_TYPE: awsssmparameterstore +AWS_REGION: Your AWS Region (Optional: defaults to us-east-2) +``` + +##### Examples + +###### Path Annotation + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: aws-ssmps-example + annotations: + avp.kubernetes.io/path: "test-aws-secret" # The name of your AWS Secret +stringData: + sample-secret: +type: Opaque +``` + +###### Inline Path + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: aws-ssmps-example +stringData: + sample-secret: +type: Opaque +``` + +###### Versioned secrets + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: aws-ssmps-example + annotations: + avp.kubernetes.io/path: "some-path/secret" + avp.kubernetes.io/secret-version: "123" +stringData: + sample-secret: + sample-secret-again: +type: Opaque +``` + +###### Secret in the same account + +The 'friendly' name of the secret can be used in this case. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: aws-example +stringData: + sample-secret: +type: Opaque +``` + +###### Secret in a different account + +The arn of the secret needs to be used in this case: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: aws-example +stringData: + sample-secret: ::#> +type: Opaque +``` + ### GCP Secret Manager ##### GCP Authentication diff --git a/pkg/backends/awsparameterstore.go b/pkg/backends/awsparameterstore.go index 67ebb815..008bc954 100644 --- a/pkg/backends/awsparameterstore.go +++ b/pkg/backends/awsparameterstore.go @@ -2,8 +2,8 @@ package backends import ( "context" - "encoding/json" "fmt" + "strings" "github.com/argoproj-labs/argocd-vault-plugin/pkg/utils" "github.com/aws/aws-sdk-go-v2/aws" @@ -16,6 +16,10 @@ const ( ) type AWSSSMParameterStoreIface interface { + GetParametersByPath(ctx context.Context, + params *ssm.GetParametersByPathInput, + optFns ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error) + GetParameter(ctx context.Context, params *ssm.GetParameterInput, optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) @@ -39,45 +43,66 @@ func (a *AWSSSMParameterStore) Login() error { } // GetSecrets gets secrets from aws secrets manager and returns the formatted data -func (a *AWSSSMParameterStore) GetParameters(path, version string, annotations map[string]string) (map[string]interface{}, error) { - input := &ssm.GetParameterInput{ - Name: aws.String(path), - WithDecryption: bool(true), - } - - if version != "" { - *input.Name = fmt.Sprintf("%v:%v", *input.Name, version) +func (a *AWSSSMParameterStore) GetSecrets(path, version string, annotations map[string]string) (map[string]interface{}, error) { + input := &ssm.GetParametersByPathInput{ + Path: aws.String(path), + Recursive: aws.Bool(false), + WithDecryption: aws.Bool(true), } - utils.VerboseToStdErr("AWS SSM Parameter Store getting secret %s", path) - result, err := a.Client.GetParameter(context.TODO(), input) + utils.VerboseToStdErr("AWS SSM Parameter Store getting secrets by path %s", path) + result, err := a.Client.GetParametersByPath(context.TODO(), input) if err != nil { return nil, err } - utils.VerboseToStdErr("AWS SSM Parameter Store get secret response %v", result) + utils.VerboseToStdErr("AWS SSM Parameter Store get secret response %v", &result) - var dat map[string]interface{} + data := make(map[string]interface{}) - if result.Parameter.Value != nil { - err := json.Unmarshal([]byte(*result.Parameter.Value), &dat) - if err != nil { - return nil, err + if result.Parameters != nil { + for _, parameter := range result.Parameters { + // extract the parameter name from the path + split := strings.Split(*parameter.Name, "/") + parameterName := split[len(split)-1] + + data[parameterName] = *parameter.Value } } else { - return nil, fmt.Errorf("Could not find secret %s", path) + return nil, fmt.Errorf("Could not find secret by path %s", path) } - return dat, nil + return data, nil } // GetIndividualSecret will get the specific secret (placeholder) from the SM backend // For AWS, we only support placeholders replaced from the k/v pairs of a secret which cannot be individually addressed // So, we use GetSecrets and extract the specific placeholder we want func (a *AWSSSMParameterStore) GetIndividualSecret(kvpath, secret, version string, annotations map[string]string) (interface{}, error) { - data, err := a.GetParameters(kvpath, version, annotations) + input := &ssm.GetParameterInput{ + Name: aws.String(kvpath), + WithDecryption: aws.Bool(true), + } + + if version != "" { + *input.Name = fmt.Sprintf("%v:%v", *input.Name, version) + } + + utils.VerboseToStdErr("AWS SSM Parameter Store getting secret %s", kvpath) + result, err := a.Client.GetParameter(context.TODO(), input) if err != nil { return nil, err } - return data[secret], nil + + utils.VerboseToStdErr("AWS SSM Parameter Store get secret response %v", &result) + + data := make(map[string]interface{}) + + if result.Parameter.Value != nil { + data["parameter"] = *result.Parameter.Value + } else { + return nil, fmt.Errorf("Could not find secret %s", kvpath) + } + + return data["parameter"], nil } diff --git a/pkg/backends/awsparameterstore_test.go b/pkg/backends/awsparameterstore_test.go index 02f516a8..95ab6d23 100644 --- a/pkg/backends/awsparameterstore_test.go +++ b/pkg/backends/awsparameterstore_test.go @@ -2,6 +2,7 @@ package backends_test import ( "context" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/ssm/types" "reflect" "strings" @@ -37,11 +38,35 @@ func (m *mockSSMParameterStoreClient) GetParameter(ctx context.Context, input *s return data, nil } +func (m *mockSSMParameterStoreClient) GetParametersByPath(ctx context.Context, input *ssm.GetParametersByPathInput, options ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error) { + + data := &ssm.GetParametersByPathOutput{ + Parameters: []types.Parameter{}, + } + + switch *input.Path { + case "test": + parameters := []types.Parameter{ + { + Name: aws.String("test-secret"), + Value: aws.String("current-value"), + Type: types.ParameterTypeSecureString, + }, + } + + data = &ssm.GetParametersByPathOutput{ + Parameters: parameters, + } + } + + return data, nil +} + func TestAWSSSMParameterStoreGetSecrets(t *testing.T) { ps := backends.NewAWSSSMParameterStoreBackend(&mockSSMParameterStoreClient{}) t.Run("Get secrets", func(t *testing.T) { - data, err := ps.GetParameters("test", "", map[string]string{}) + data, err := ps.GetSecrets("test", "", map[string]string{}) if err != nil { t.Fatalf("expected 0 errors but got: %s", err) } @@ -61,7 +86,7 @@ func TestAWSSSMParameterStoreGetSecrets(t *testing.T) { t.Fatalf("expected 0 errors but got: %s", err) } - expected := "previous-value" + expected := "{\"test-secret\":\"previous-value\"}" if !reflect.DeepEqual(expected, secret) { t.Errorf("expected: %s, got: %s.", expected, secret) @@ -69,14 +94,12 @@ func TestAWSSSMParameterStoreGetSecrets(t *testing.T) { }) t.Run("Get secrets at specific version", func(t *testing.T) { - data, err := ps.GetParameters("test", "123", map[string]string{}) + data, err := ps.GetIndividualSecret("test", "test-secret", "123", map[string]string{}) if err != nil { t.Fatalf("expected 0 errors but got: %s", err) } - expected := map[string]interface{}{ - "test-secret": "previous-value", - } + expected := "{\"test-secret\":\"previous-value\"}" if !reflect.DeepEqual(expected, data) { t.Errorf("expected: %s, got: %s.", expected, data) @@ -87,7 +110,7 @@ func TestAWSSSMParameterStoreGetSecrets(t *testing.T) { func TestAWSSSMParameterStoreEmptyIfNoSecret(t *testing.T) { sm := backends.NewAWSSSMParameterStoreBackend(&mockSSMParameterStoreClient{}) - _, err := sm.GetParameters("empty", "", map[string]string{}) + _, err := sm.GetSecrets("empty", "", map[string]string{}) if err == nil { t.Fatalf("expected an error but got nil") }