update code comments and kms decryption docs
Signed-off-by: Sanskar Jaiswal <sanskar.jaiswal@weave.works>
This commit is contained in:
parent
1ee85e5988
commit
326e20c41b
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<ACCOUNT_ID>:role/<KMS-ROLE-NAME>'
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue