Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sops/awskms: Fix AWS KMS config creation #667

Merged
merged 1 commit into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions internal/sops/awskms/keysource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
})
Expand All @@ -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]
hiddeco marked this conversation as resolved.
Show resolved Hide resolved
}

client := sts.NewFromConfig(*config)
input := &sts.AssumeRoleInput{
Expand Down
103 changes: 74 additions & 29 deletions internal/sops/awskms/keysource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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
}