From 10bc7eadffdc1799a7b942aa1bbfcda0d0569cd1 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Fri, 27 May 2022 18:43:51 +0530 Subject: [PATCH] sops/awskms: fix awskms config creation and expand tests Signed-off-by: Sanskar Jaiswal --- internal/sops/awskms/keysource.go | 38 ++++++--- internal/sops/awskms/keysource_test.go | 103 ++++++++++++++++++------- 2 files changed, 102 insertions(+), 39 deletions(-) diff --git a/internal/sops/awskms/keysource.go b/internal/sops/awskms/keysource.go index 866f89de..c287ad68 100644 --- a/internal/sops/awskms/keysource.go +++ b/internal/sops/awskms/keysource.go @@ -35,6 +35,8 @@ const ( stsSessionRegex = "[^a-zA-Z0-9=,.@-_]+" // kmsTTL is the duration after which a MasterKey requires rotation. kmsTTL = time.Hour * 24 * 30 * 6 + // roleSessionNameLengthLimit is the AWS role session name length limit. + roleSessionNameLengthLimit = 64 ) // MasterKey is an AWS KMS key used to encrypt and decrypt sops' data key. @@ -53,6 +55,8 @@ type MasterKey struct { // EncryptionContext provides additional context about the data key. // Ref: https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#encrypt_context EncryptionContext map[string]string + // AWSProfile is the profile to use for loading configuration and credentials. + AwsProfile string // credentialsProvider is used to configure the AWS config with the // necessary credentials. @@ -62,7 +66,7 @@ type MasterKey struct { // to by default. This is mostly used for testing purposes as it can not be // injected using e.g. an environment variable. The field is not publicly // exposed, nor configurable. - epResolver aws.EndpointResolver + epResolver aws.EndpointResolverWithOptions } // CredsProvider is a wrapper around aws.CredentialsProvider used for authenticating @@ -119,8 +123,9 @@ func (key *MasterKey) Encrypt(dataKey []byte) error { } client := kms.NewFromConfig(*cfg) input := &kms.EncryptInput{ - KeyId: &key.Arn, - Plaintext: dataKey, + KeyId: &key.Arn, + Plaintext: dataKey, + EncryptionContext: key.EncryptionContext, } out, err := client.Encrypt(context.TODO(), input) if err != nil { @@ -216,20 +221,32 @@ func NewMasterKeyFromArn(arn string, context map[string]string, awsProfile strin } k.EncryptionContext = context k.CreationDate = time.Now().UTC() + k.AwsProfile = awsProfile return k } // createKMSConfig returns a Config configured with the appropriate credentials. func (key MasterKey) createKMSConfig() (*aws.Config, error) { - // Use the credentialsProvider if present, otherwise default to reading credentials - // from the environment. + re := regexp.MustCompile(arnRegex) + matches := re.FindStringSubmatch(key.Arn) + if matches == nil { + return nil, fmt.Errorf("no valid ARN found in '%s'", key.Arn) + } + region := matches[1] cfg, err := config.LoadDefaultConfig(context.TODO(), func(lo *config.LoadOptions) error { + // Use the credentialsProvider if present, otherwise default to reading credentials + // from the environment. if key.credentialsProvider != nil { lo.Credentials = key.credentialsProvider } + if key.AwsProfile != "" { + lo.SharedConfigProfile = key.AwsProfile + } + lo.Region = region + // Set the epResolver, if present. Used ONLY for tests. if key.epResolver != nil { - lo.EndpointResolver = key.epResolver + lo.EndpointResolverWithOptions = key.epResolver } return nil }) @@ -250,12 +267,13 @@ func (key MasterKey) createSTSConfig(config *aws.Config) (*aws.Config, error) { if err != nil { return nil, err } - stsRoleSessionNameRe, err := regexp.Compile(stsSessionRegex) - if err != nil { - return nil, fmt.Errorf("failed to compile STS role session name regex: %w", err) - } + stsRoleSessionNameRe := regexp.MustCompile(stsSessionRegex) + sanitizedHostname := stsRoleSessionNameRe.ReplaceAllString(hostname, "") name := "sops@" + sanitizedHostname + if len(name) >= roleSessionNameLengthLimit { + name = name[:roleSessionNameLengthLimit] + } client := sts.NewFromConfig(*config) input := &sts.AssumeRoleInput{ diff --git a/internal/sops/awskms/keysource_test.go b/internal/sops/awskms/keysource_test.go index 38691b0b..e46b09ad 100644 --- a/internal/sops/awskms/keysource_test.go +++ b/internal/sops/awskms/keysource_test.go @@ -18,6 +18,7 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/kms" . "github.com/onsi/gomega" @@ -280,35 +281,79 @@ aws_session_token: test-token } func Test_createKMSConfig(t *testing.T) { - g := NewWithT(t) - - key := MasterKey{ - credentialsProvider: credentials.NewStaticCredentialsProvider("test-id", "test-secret", "test-token"), + tests := []struct { + name string + key MasterKey + assertFunc func(g *WithT, cfg *aws.Config, err error) + fallback bool + }{ + { + name: "master key with invalid arn fails", + key: MasterKey{ + Arn: "arn:gcp:kms:antartica-north-2::key/45e6-aca6-a5b005693a48", + }, + assertFunc: func(g *WithT, _ *aws.Config, err error) { + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("no valid ARN found")) + }, + }, + { + name: "master key with with proper configuration passes", + key: MasterKey{ + credentialsProvider: credentials.NewStaticCredentialsProvider("test-id", "test-secret", "test-token"), + AwsProfile: "test-profile", + Arn: "arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48", + }, + assertFunc: func(g *WithT, cfg *aws.Config, err error) { + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cfg.Region).To(Equal("us-west-2")) + + creds, err := cfg.Credentials.Retrieve(context.TODO()) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(creds.AccessKeyID).To(Equal("test-id")) + g.Expect(creds.SecretAccessKey).To(Equal("test-secret")) + g.Expect(creds.SessionToken).To(Equal("test-token")) + + // ConfigSources is a slice of config.Config, which in turn is an interface. + // Since we use a LoadOptions object, we assert the type of cfgSrc and then + // check if the expected profile is present. + for _, cfgSrc := range cfg.ConfigSources { + if src, ok := cfgSrc.(config.LoadOptions); ok { + g.Expect(src.SharedConfigProfile).To(Equal("test-profile")) + } + } + }, + }, + { + name: "master key without creds and profile falls back to the environment variables", + key: MasterKey{ + Arn: "arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48", + }, + fallback: true, + assertFunc: func(g *WithT, cfg *aws.Config, err error) { + g.Expect(err).ToNot(HaveOccurred()) + + creds, err := cfg.Credentials.Retrieve(context.TODO()) + g.Expect(creds.AccessKeyID).To(Equal("id")) + g.Expect(creds.SecretAccessKey).To(Equal("secret")) + g.Expect(creds.SessionToken).To(Equal("token")) + }, + }, } - cfg, err := key.createKMSConfig() - g.Expect(err).ToNot(HaveOccurred()) - - creds, err := cfg.Credentials.Retrieve(context.TODO()) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(creds.AccessKeyID).To(Equal("test-id")) - g.Expect(creds.SecretAccessKey).To(Equal("test-secret")) - g.Expect(creds.SessionToken).To(Equal("test-token")) - - // test if we fallback to the default way of fetching credentials - // if no static credentials are provided. - key.credentialsProvider = nil - t.Setenv("AWS_ACCESS_KEY_ID", "id") - t.Setenv("AWS_SECRET_ACCESS_KEY", "secret") - t.Setenv("AWS_SESSION_TOKEN", "token") - - cfg, err = key.createKMSConfig() - g.Expect(err).ToNot(HaveOccurred()) - creds, err = cfg.Credentials.Retrieve(context.TODO()) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(creds.AccessKeyID).To(Equal("id")) - g.Expect(creds.SecretAccessKey).To(Equal("secret")) - g.Expect(creds.SessionToken).To(Equal("token")) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + // Set the environment variables if we want to fallback + if tt.fallback { + t.Setenv("AWS_ACCESS_KEY_ID", "id") + t.Setenv("AWS_SECRET_ACCESS_KEY", "secret") + t.Setenv("AWS_SESSION_TOKEN", "token") + } + cfg, err := tt.key.createKMSConfig() + tt.assertFunc(g, cfg, err) + }) + } } func createTestMasterKey(arn string) MasterKey { @@ -322,7 +367,7 @@ func createTestMasterKey(arn string) MasterKey { // epResolver is a dummy resolver that points to the local test KMS server type epResolver struct{} -func (e epResolver) ResolveEndpoint(service, region string) (aws.Endpoint, error) { +func (e epResolver) ResolveEndpoint(service, region string, options ...interface{}) (aws.Endpoint, error) { return aws.Endpoint{ URL: testKMSServerURL, }, nil @@ -334,7 +379,7 @@ func createTestKMSClient(key MasterKey) (*kms.Client, error) { return nil, err } - cfg.EndpointResolver = epResolver{} + cfg.EndpointResolverWithOptions = epResolver{} return kms.NewFromConfig(*cfg), nil }