From f4800f6b58ea362e5b8caea4348563696737193b Mon Sep 17 00:00:00 2001 From: Nathaniel Date: Mon, 11 Dec 2017 08:08:28 -0800 Subject: [PATCH] Add Redis AUTH, in-transit and at-rest encryption (#2090) * add AUTH, at-rest and in-transit encryption to Elasticache replication groups * add _enabled to transit/at_rest encyrption parameters * added one more _enabled * move validateAwsElastiCacheReplicationGroupAuthToken to aws/validators.go, as well as tests * set auth_token to nil during Reads * update Replication Group encryption acceptance tests to use config functions instead of vars * Fix whitespacing (tabs -> spaces) --- ...ource_aws_elasticache_replication_group.go | 40 +++++ ..._aws_elasticache_replication_group_test.go | 147 ++++++++++++++++++ aws/validators.go | 13 ++ aws/validators_test.go | 40 +++++ 4 files changed, 240 insertions(+) diff --git a/aws/resource_aws_elasticache_replication_group.go b/aws/resource_aws_elasticache_replication_group.go index bceb938d09e..03d387e3c61 100644 --- a/aws/resource_aws_elasticache_replication_group.go +++ b/aws/resource_aws_elasticache_replication_group.go @@ -87,6 +87,28 @@ func resourceAwsElasticacheReplicationGroup() *schema.Resource { resourceSchema["engine"].Default = "redis" resourceSchema["engine"].ValidateFunc = validateAwsElastiCacheReplicationGroupEngine + resourceSchema["at_rest_encryption_enabled"] = &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + } + + resourceSchema["transit_encryption_enabled"] = &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + } + + resourceSchema["auth_token"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ForceNew: true, + ValidateFunc: validateAwsElastiCacheReplicationGroupAuthToken, + } + return &schema.Resource{ Create: resourceAwsElasticacheReplicationGroupCreate, Read: resourceAwsElasticacheReplicationGroupRead, @@ -168,6 +190,18 @@ func resourceAwsElasticacheReplicationGroupCreate(d *schema.ResourceData, meta i params.SnapshotName = aws.String(v.(string)) } + if _, ok := d.GetOk("transit_encryption_enabled"); ok { + params.TransitEncryptionEnabled = aws.Bool(d.Get("transit_encryption_enabled").(bool)) + } + + if _, ok := d.GetOk("at_rest_encryption_enabled"); ok { + params.AtRestEncryptionEnabled = aws.Bool(d.Get("at_rest_encryption_enabled").(bool)) + } + + if v, ok := d.GetOk("auth_token"); ok { + params.AuthToken = aws.String(v.(string)) + } + clusterMode, clusterModeOk := d.GetOk("cluster_mode") cacheClusters, cacheClustersOk := d.GetOk("number_cache_clusters") @@ -313,6 +347,12 @@ func resourceAwsElasticacheReplicationGroupRead(d *schema.ResourceData, meta int } d.Set("auto_minor_version_upgrade", c.AutoMinorVersionUpgrade) + d.Set("at_rest_encryption_enabled", c.AtRestEncryptionEnabled) + d.Set("transit_encryption_enabled", c.TransitEncryptionEnabled) + + if c.AuthTokenEnabled != nil && !*c.AuthTokenEnabled { + d.Set("auth_token", nil) + } } return nil diff --git a/aws/resource_aws_elasticache_replication_group_test.go b/aws/resource_aws_elasticache_replication_group_test.go index 5e235248f28..87a39d35f60 100644 --- a/aws/resource_aws_elasticache_replication_group_test.go +++ b/aws/resource_aws_elasticache_replication_group_test.go @@ -339,6 +339,44 @@ func TestAccAWSElasticacheReplicationGroup_enableSnapshotting(t *testing.T) { }) } +func TestAccAWSElasticacheReplicationGroup_enableAuthTokenTransitEncryption(t *testing.T) { + var rg elasticache.ReplicationGroup + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheReplicationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheReplicationGroup_EnableAuthTokenTransitEncryptionConfig(acctest.RandInt(), acctest.RandString(10), acctest.RandString(16)), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheReplicationGroupExists("aws_elasticache_replication_group.bar", &rg), + resource.TestCheckResourceAttr( + "aws_elasticache_replication_group.bar", "transit_encryption_enabled", "true"), + ), + }, + }, + }) +} + +func TestAccAWSElasticacheReplicationGroup_enableAtRestEncryption(t *testing.T) { + var rg elasticache.ReplicationGroup + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheReplicationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheReplicationGroup_EnableAtRestEncryptionConfig(acctest.RandInt(), acctest.RandString(10)), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheReplicationGroupExists("aws_elasticache_replication_group.bar", &rg), + resource.TestCheckResourceAttr( + "aws_elasticache_replication_group.bar", "at_rest_encryption_enabled", "true"), + ), + }, + }, + }) +} + func TestResourceAWSElastiCacheReplicationGroupIdValidation(t *testing.T) { cases := []struct { Value string @@ -1002,3 +1040,112 @@ resource "aws_elasticache_replication_group" "bar" { } }`, rInt, rInt, rInt, rInt, rName) } + +func testAccAWSElasticacheReplicationGroup_EnableAtRestEncryptionConfig(rInt int, rString string) string { + return fmt.Sprintf(` +resource "aws_vpc" "foo" { + cidr_block = "192.168.0.0/16" + tags { + Name = "tf-test" + } +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "192.168.0.0/20" + availability_zone = "us-west-2a" + tags { + Name = "tf-test-%03d" + } +} + +resource "aws_elasticache_subnet_group" "bar" { + name = "tf-test-cache-subnet-%03d" + description = "tf-test-cache-subnet-group-descr" + subnet_ids = [ + "${aws_subnet.foo.id}", + ] +} + +resource "aws_security_group" "bar" { + name = "tf-test-security-group-%03d" + description = "tf-test-security-group-descr" + vpc_id = "${aws_vpc.foo.id}" + ingress { + from_port = -1 + to_port = -1 + protocol = "icmp" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_elasticache_replication_group" "bar" { + replication_group_id = "tf-%s" + replication_group_description = "test description" + node_type = "cache.t2.micro" + number_cache_clusters = "1" + port = 6379 + subnet_group_name = "${aws_elasticache_subnet_group.bar.name}" + security_group_ids = ["${aws_security_group.bar.id}"] + parameter_group_name = "default.redis3.2" + availability_zones = ["us-west-2a"] + engine_version = "3.2.6" + at_rest_encryption_enabled = true +} +`, rInt, rInt, rInt, rString) +} + +func testAccAWSElasticacheReplicationGroup_EnableAuthTokenTransitEncryptionConfig(rInt int, rString10 string, rString16 string) string { + return fmt.Sprintf(` +resource "aws_vpc" "foo" { + cidr_block = "192.168.0.0/16" + tags { + Name = "tf-test" + } +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "192.168.0.0/20" + availability_zone = "us-west-2a" + tags { + Name = "tf-test-%03d" + } +} + +resource "aws_elasticache_subnet_group" "bar" { + name = "tf-test-cache-subnet-%03d" + description = "tf-test-cache-subnet-group-descr" + subnet_ids = [ + "${aws_subnet.foo.id}", + ] +} + +resource "aws_security_group" "bar" { + name = "tf-test-security-group-%03d" + description = "tf-test-security-group-descr" + vpc_id = "${aws_vpc.foo.id}" + ingress { + from_port = -1 + to_port = -1 + protocol = "icmp" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_elasticache_replication_group" "bar" { + replication_group_id = "tf-%s" + replication_group_description = "test description" + node_type = "cache.t2.micro" + number_cache_clusters = "1" + port = 6379 + subnet_group_name = "${aws_elasticache_subnet_group.bar.name}" + security_group_ids = ["${aws_security_group.bar.id}"] + parameter_group_name = "default.redis3.2" + availability_zones = ["us-west-2a"] + engine_version = "3.2.6" + transit_encryption_enabled = true + auth_token = "%s" +} +`, rInt, rInt, rInt, rString10, rString16) +} diff --git a/aws/validators.go b/aws/validators.go index 135efdc4db8..14b1036ae3d 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -1984,3 +1984,16 @@ func validateDxConnectionBandWidth(v interface{}, k string) (ws []string, errors errors = append(errors, fmt.Errorf("expected %s to be one of %v, got %s", k, validBandWidth, val)) return } + +func validateAwsElastiCacheReplicationGroupAuthToken(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if (len(value) < 16) || (len(value) > 128) { + errors = append(errors, fmt.Errorf( + "%q must contain from 16 to 128 alphanumeric characters or symbols (excluding @, \", and /)", k)) + } + if !regexp.MustCompile(`^[^@"\/]+$`).MatchString(value) { + errors = append(errors, fmt.Errorf( + "only alphanumeric characters or symbols (excluding @, \", and /) allowed in %q", k)) + } + return +} diff --git a/aws/validators_test.go b/aws/validators_test.go index f11672d7549..cf9ec0234bc 100644 --- a/aws/validators_test.go +++ b/aws/validators_test.go @@ -2799,3 +2799,43 @@ func TestValidateCognitoUserPoolReplyEmailAddress(t *testing.T) { } } } + +func TestResourceAWSElastiCacheReplicationGroupAuthTokenValidation(t *testing.T) { + cases := []struct { + Value string + ErrCount int + }{ + { + Value: "this-is-valid!#%()^", + ErrCount: 0, + }, + { + Value: "this-is-not", + ErrCount: 1, + }, + { + Value: "this-is-not-valid\"", + ErrCount: 1, + }, + { + Value: "this-is-not-valid@", + ErrCount: 1, + }, + { + Value: "this-is-not-valid/", + ErrCount: 1, + }, + { + Value: randomString(129), + ErrCount: 1, + }, + } + + for _, tc := range cases { + _, errors := validateAwsElastiCacheReplicationGroupAuthToken(tc.Value, "aws_elasticache_replication_group_auth_token") + + if len(errors) != tc.ErrCount { + t.Fatalf("Expected the ElastiCache Replication Group AuthToken to trigger a validation error") + } + } +}