From 326e20c41b8b0a9b2585a861ce604b5b51c1d5b3 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Wed, 4 May 2022 02:05:36 +0530 Subject: [PATCH] update code comments and kms decryption docs Signed-off-by: Sanskar Jaiswal --- controllers/kustomization_decryptor.go | 6 +- docs/spec/v1beta2/kustomization.md | 36 ++++- internal/sops/awskms/keysource.go | 215 ++++++++++++++----------- internal/sops/awskms/keysource_test.go | 4 +- internal/sops/keyservice/options.go | 2 +- 5 files changed, 161 insertions(+), 102 deletions(-) diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index 0313aa7..d2451ad 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -133,8 +133,8 @@ type KustomizeDecryptor struct { // vaultToken is the Hashicorp Vault token used to authenticate towards // any Vault server. vaultToken string - // awsCredsProvider is the AWS credentials provider object used to authenticate towards - // any AWS KMS. + // awsCredsProvider is the AWS credentials provider object used to authenticate + // towards any AWS KMS. awsCredsProvider *awskms.CredsProvider // azureToken is the Azure credential token used to authenticate towards // any Azure Key Vault. @@ -229,7 +229,7 @@ func (d *KustomizeDecryptor) ImportKeys(ctx context.Context) error { } case filepath.Ext(DecryptionAWSKmsFile): if name == DecryptionAWSKmsFile { - if d.awsCredsProvider, err = awskms.LoadAwsKmsCredsProviderFromYaml(value); err != nil { + if d.awsCredsProvider, err = awskms.LoadCredsProviderFromYaml(value); err != nil { return fmt.Errorf("failed to import '%s' data from %s decryption Secret '%s': %w", name, provider, secretName, err) } } diff --git a/docs/spec/v1beta2/kustomization.md b/docs/spec/v1beta2/kustomization.md index ed9e877..0113665 100644 --- a/docs/spec/v1beta2/kustomization.md +++ b/docs/spec/v1beta2/kustomization.md @@ -1117,7 +1117,7 @@ kind: Secret metadata: name: sops-keys namespace: default -stringData: +data: sops.aws-kms: | aws_access_key_id: some-access-key-id aws_secret_access_key: some-aws-secret-access-key @@ -1261,6 +1261,40 @@ kubectl -n flux-system annotate serviceaccount kustomize-controller \ eks.amazonaws.com/role-arn='arn:aws:iam:::role/' ``` +Furthermore, you can also use the usual [environmentvariables used for specifying AWS +credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html#envvars-list) +, by patching the kustomize-controller deployment: + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kustomize-controller + namespace: flux-system +spec: + template: + spec: + containers: + - name: manager + env: + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: aws-creds + key: awsAccessKeyID + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: aws-creds + key: awsSecretAccessKey + - name: AWS_SESSION_TOKEN + valueFrom: + secretKeyRef: + name: aws-creds + key: awsSessionToken +``` + In addition to this, the [general SOPS documentation around KMS AWS applies](https://github.com/mozilla/sops#27kms-aws-profiles), allowing you to specify e.g. a `SOPS_KMS_ARN` environment variable. diff --git a/internal/sops/awskms/keysource.go b/internal/sops/awskms/keysource.go index ff1ff63..83002c5 100644 --- a/internal/sops/awskms/keysource.go +++ b/internal/sops/awskms/keysource.go @@ -35,36 +35,51 @@ import ( ) const ( - arnRegex = `^arn:aws[\w-]*:kms:(.+):[0-9]+:(key|alias)/.+$` - stsSessionRegex = "[^a-zA-Z0-9=,.@-]+" + // arnRegex matches an AWS ARN. + // valid ARN example: arn:aws:kms:us-west-2:107501996527:key/612d5f0p-p1l3-45e6-aca6-a5b005693a48 + arnRegex = `^arn:aws[\w-]*:kms:(.+):[0-9]+:(key|alias)/.+$` + // stsSessionRegex matches an AWS STS session name. + // valid STS session examples: john_s, sops@42WQm042 + stsSessionRegex = "[^a-zA-Z0-9=,.@-_]+" // kmsTTL is the duration after which a MasterKey requires rotation. kmsTTL = time.Hour * 24 * 30 * 6 ) -// MasterKey is a AWS KMS key used to encrypt and decrypt sops' data key. +// MasterKey is an AWS KMS key used to encrypt and decrypt sops' data key. +// Adapted from: https://github.com/mozilla/sops/blob/v3.7.2/kms/keysource.go#L39 +// Modified to accept custom static credentials as opposed to using env vars by default +// and use aws-sdk-go-v2 instead of aws-sdk-go being used in upstream. type MasterKey struct { - Arn string - Role string - EncryptedKey string - CreationDate time.Time + // AWS Role ARN associated with the KMS key. + Arn string + // AWS Role ARN used to assume a role through AWS STS. + Role string + // EncryptedKey stores the data key in it's encrypted form. + EncryptedKey string + // CreationDate is when this MasterKey was created. + CreationDate time.Time + // 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 + // credentialsProvider is used to configure the AWS config with the + // necessary credentials. credentialsProvider aws.CredentialsProvider - // epResolver IS ONLY MEANT TO BE USED FOR TESTS. - // it can be used to override the endpoint that the AWS client resolves to - // by default. it's hacky but there is no other choice, since you can't - // specify the endpoint as an env var like you can do with an access key. + // epResolver can be used to override the endpoint the AWS client resolves + // 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 } // CredsProvider is a wrapper around aws.CredentialsProvider used for authenticating -// when using AWS KMS. +// towards AWS KMS. type CredsProvider struct { credsProvider aws.CredentialsProvider } -// NewCredsProvider returns a Creds object with the provided aws.CredentialsProvider +// NewCredsProvider returns a CredsProvider object with the provided aws.CredentialsProvider. func NewCredsProvider(cp aws.CredentialsProvider) *CredsProvider { return &CredsProvider{ credsProvider: cp, @@ -76,9 +91,9 @@ func (c CredsProvider) ApplyToMasterKey(key *MasterKey) { key.credentialsProvider = c.credsProvider } -// LoadAwsKmsCredsProviderFromYaml parses the given yaml returns a CredsProvider object +// LoadCredsProviderFromYaml parses the given YAML returns a CredsProvider object // which contains the credentials provider used for authenticating towards AWS KMS. -func LoadAwsKmsCredsProviderFromYaml(b []byte) (*CredsProvider, error) { +func LoadCredsProviderFromYaml(b []byte) (*CredsProvider, error) { credInfo := struct { AccessKeyID string `json:"aws_access_key_id"` SecretAccessKey string `json:"aws_secret_access_key"` @@ -93,17 +108,18 @@ func LoadAwsKmsCredsProviderFromYaml(b []byte) (*CredsProvider, error) { }, nil } -// EncryptedDataKey returns the encrypted data key this master key holds +// EncryptedDataKey returns the encrypted data key this master key holds. func (key *MasterKey) EncryptedDataKey() []byte { return []byte(key.EncryptedKey) } -// SetEncryptedDataKey sets the encrypted data key for this master key +// SetEncryptedDataKey sets the encrypted data key for this master key. func (key *MasterKey) SetEncryptedDataKey(enc []byte) { key.EncryptedKey = string(enc) } -// Encrypt takes a sops data key, encrypts it with KMS and stores the result in the EncryptedKey field +// Encrypt takes a SOPS data key, encrypts it with KMS and stores the result +// in the EncryptedKey field. func (key *MasterKey) Encrypt(dataKey []byte) error { cfg, err := key.createKMSConfig() if err != nil { @@ -122,7 +138,8 @@ func (key *MasterKey) Encrypt(dataKey []byte) error { return nil } -// EncryptIfNeeded encrypts the provided sops' data key and encrypts it if it hasn't been encrypted yet +// EncryptIfNeeded encrypts the provided sops' data key and encrypts it, if it +// has not been encrypted yet. func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error { if key.EncryptedKey == "" { return key.Encrypt(dataKey) @@ -155,88 +172,15 @@ func (key *MasterKey) Decrypt() ([]byte, error) { // NeedsRotation returns whether the data key needs to be rotated or not. func (key *MasterKey) NeedsRotation() bool { - return time.Since(key.CreationDate) > (time.Hour * 24 * 30 * 6) + return time.Since(key.CreationDate) > kmsTTL } -// ToString converts the key to a string representation +// ToString converts the key to a string representation. func (key *MasterKey) ToString() string { return key.Arn } -// NewMasterKey creates a new MasterKey from an ARN, role and context, setting the creation date to the current date -func NewMasterKey(arn string, role string, context map[string]string) *MasterKey { - return &MasterKey{ - Arn: arn, - Role: role, - EncryptionContext: context, - CreationDate: time.Now().UTC(), - } -} - -// NewMasterKeyFromArn takes an ARN string and returns a new MasterKey for that ARN -func NewMasterKeyFromArn(arn string, context map[string]string, awsProfile string) *MasterKey { - k := &MasterKey{} - arn = strings.Replace(arn, " ", "", -1) - roleIndex := strings.Index(arn, "+arn:aws:iam::") - if roleIndex > 0 { - k.Arn = arn[:roleIndex] - k.Role = arn[roleIndex+1:] - } else { - k.Arn = arn - } - k.EncryptionContext = context - k.CreationDate = time.Now().UTC() - return k -} - -func (key MasterKey) createKMSConfig() (*aws.Config, error) { - cfg, err := config.LoadDefaultConfig(context.TODO(), func(lo *config.LoadOptions) error { - if key.credentialsProvider != nil { - lo.Credentials = key.credentialsProvider - } - if key.epResolver != nil { - lo.EndpointResolver = key.epResolver - } - return nil - }) - if err != nil { - return nil, fmt.Errorf("couldn't load AWS config: %w", err) - } - if key.Role != "" { - return key.createSTSConfig(&cfg) - } - - return &cfg, nil -} - -func (key MasterKey) createSTSConfig(config *aws.Config) (*aws.Config, error) { - hostname, err := os.Hostname() - 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) - } - sanitizedHostname := stsRoleSessionNameRe.ReplaceAllString(hostname, "") - name := "sops@" + sanitizedHostname - - client := sts.NewFromConfig(*config) - input := &sts.AssumeRoleInput{ - RoleArn: &key.Arn, - RoleSessionName: &name, - } - out, err := client.AssumeRole(context.TODO(), input) - if err != nil { - return nil, fmt.Errorf("failed to assume role '%s': %w", key.Role, err) - } - config.Credentials = credentials.NewStaticCredentialsProvider(*out.Credentials.AccessKeyId, - *out.Credentials.SecretAccessKey, *out.Credentials.SessionToken, - ) - return config, nil -} - -// ToMap converts the MasterKey to a map for serialization purposes +// ToMap converts the MasterKey to a map for serialization purposes. func (key MasterKey) ToMap() map[string]interface{} { out := make(map[string]interface{}) out["arn"] = key.Arn @@ -254,3 +198,84 @@ func (key MasterKey) ToMap() map[string]interface{} { } return out } + +// NewMasterKey creates a new MasterKey from an ARN, role and context, setting the +// creation date to the current date. +func NewMasterKey(arn string, role string, context map[string]string) *MasterKey { + return &MasterKey{ + Arn: arn, + Role: role, + EncryptionContext: context, + CreationDate: time.Now().UTC(), + } +} + +// NewMasterKeyFromArn takes an ARN string and returns a new MasterKey for that +// ARN. +func NewMasterKeyFromArn(arn string, context map[string]string, awsProfile string) *MasterKey { + k := &MasterKey{} + arn = strings.Replace(arn, " ", "", -1) + roleIndex := strings.Index(arn, "+arn:aws:iam::") + if roleIndex > 0 { + k.Arn = arn[:roleIndex] + k.Role = arn[roleIndex+1:] + } else { + k.Arn = arn + } + k.EncryptionContext = context + k.CreationDate = time.Now().UTC() + 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. + cfg, err := config.LoadDefaultConfig(context.TODO(), func(lo *config.LoadOptions) error { + if key.credentialsProvider != nil { + lo.Credentials = key.credentialsProvider + } + // Set the epResolver, if present. Used ONLY for tests. + if key.epResolver != nil { + lo.EndpointResolver = key.epResolver + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("couldn't load AWS config: %w", err) + } + if key.Role != "" { + return key.createSTSConfig(&cfg) + } + + return &cfg, nil +} + +// createSTSConfig uses AWS STS to assume a role and returns a Config configured +// with that role's credentials. +func (key MasterKey) createSTSConfig(config *aws.Config) (*aws.Config, error) { + hostname, err := os.Hostname() + 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) + } + sanitizedHostname := stsRoleSessionNameRe.ReplaceAllString(hostname, "") + name := "sops@" + sanitizedHostname + + client := sts.NewFromConfig(*config) + input := &sts.AssumeRoleInput{ + RoleArn: &key.Role, + RoleSessionName: &name, + } + out, err := client.AssumeRole(context.TODO(), input) + if err != nil { + return nil, fmt.Errorf("failed to assume role '%s': %w", key.Role, err) + } + config.Credentials = credentials.NewStaticCredentialsProvider(*out.Credentials.AccessKeyId, + *out.Credentials.SecretAccessKey, *out.Credentials.SessionToken, + ) + return config, nil +} diff --git a/internal/sops/awskms/keysource_test.go b/internal/sops/awskms/keysource_test.go index e61ea63..ef9a5bc 100644 --- a/internal/sops/awskms/keysource_test.go +++ b/internal/sops/awskms/keysource_test.go @@ -99,7 +99,7 @@ func TestMain(m *testing.M) { logger.Fatalf("could not set arn") } - // Run the tests, but only if we succeeded in setting up the Vault server + // Run the tests, but only if we succeeded in setting up the AWS KMS server. var code int if err == nil { code = m.Run() @@ -276,7 +276,7 @@ aws_access_key_id: test-id aws_secret_access_key: test-secret aws_session_token: test-token `) - credsProvider, err := LoadAwsKmsCredsProviderFromYaml(credsYaml) + credsProvider, err := LoadCredsProviderFromYaml(credsYaml) g.Expect(err).ToNot(HaveOccurred()) creds, err := credsProvider.credsProvider.Retrieve(context.TODO()) diff --git a/internal/sops/keyservice/options.go b/internal/sops/keyservice/options.go index 30cfba9..f8a3868 100644 --- a/internal/sops/keyservice/options.go +++ b/internal/sops/keyservice/options.go @@ -57,7 +57,7 @@ func (o WithAgeIdentities) ApplyToServer(s *Server) { s.ageIdentities = age.ParsedIdentities(o) } -// WithAWSKeys configurs the AWS credentials on the Server +// WithAWSKeys configures the AWS credentials on the Server type WithAWSKeys struct { CredsProvider *awskms.CredsProvider }