From 3be256fa09d080c81b75fda89b7a67a17045474f 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 | 111 +++++++++++++++---------- internal/sops/awskms/keysource_test.go | 4 +- internal/sops/keyservice/options.go | 2 +- 5 files changed, 109 insertions(+), 50 deletions(-) diff --git a/controllers/kustomization_decryptor.go b/controllers/kustomization_decryptor.go index f27942870..6d5737ebf 100644 --- a/controllers/kustomization_decryptor.go +++ b/controllers/kustomization_decryptor.go @@ -129,8 +129,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. @@ -225,7 +225,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 c8fed7074..922be30e6 100644 --- a/docs/spec/v1beta2/kustomization.md +++ b/docs/spec/v1beta2/kustomization.md @@ -1111,7 +1111,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 @@ -1255,6 +1255,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 ff1ff63d7..83002c5c9 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,15 +172,35 @@ 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 +// 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 + if key.Role != "" { + out["role"] = key.Role + } + out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339) + out["enc"] = key.EncryptedKey + if key.EncryptionContext != nil { + outcontext := make(map[string]string) + for k, v := range key.EncryptionContext { + outcontext[k] = v + } + out["context"] = outcontext + } + 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, @@ -173,7 +210,8 @@ func NewMasterKey(arn string, role string, context map[string]string) *MasterKey } } -// NewMasterKeyFromArn takes an ARN string and returns a new MasterKey for that ARN +// 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) @@ -189,11 +227,15 @@ func NewMasterKeyFromArn(arn string, context map[string]string, awsProfile strin 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 } @@ -209,6 +251,8 @@ func (key MasterKey) createKMSConfig() (*aws.Config, error) { 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 { @@ -223,7 +267,7 @@ func (key MasterKey) createSTSConfig(config *aws.Config) (*aws.Config, error) { client := sts.NewFromConfig(*config) input := &sts.AssumeRoleInput{ - RoleArn: &key.Arn, + RoleArn: &key.Role, RoleSessionName: &name, } out, err := client.AssumeRole(context.TODO(), input) @@ -235,22 +279,3 @@ func (key MasterKey) createSTSConfig(config *aws.Config) (*aws.Config, error) { ) return config, nil } - -// 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 - if key.Role != "" { - out["role"] = key.Role - } - out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339) - out["enc"] = key.EncryptedKey - if key.EncryptionContext != nil { - outcontext := make(map[string]string) - for k, v := range key.EncryptionContext { - outcontext[k] = v - } - out["context"] = outcontext - } - return out -} diff --git a/internal/sops/awskms/keysource_test.go b/internal/sops/awskms/keysource_test.go index e61ea6310..ef9a5bcc1 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 30cfba9b8..f8a3868cc 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 }