diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index ac0d89290..6044a9211 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -317,6 +317,7 @@ func Provider() terraform.ResourceProvider { "aws_s3_bucket_policy": resourceAwsS3BucketPolicy(), "aws_s3_bucket_object": resourceAwsS3BucketObject(), "aws_s3_bucket_notification": resourceAwsS3BucketNotification(), + "aws_default_security_group": resourceAwsDefaultSecurityGroup(), "aws_security_group": resourceAwsSecurityGroup(), "aws_security_group_rule": resourceAwsSecurityGroupRule(), "aws_simpledb_domain": resourceAwsSimpleDBDomain(), diff --git a/builtin/providers/aws/resource_aws_default_security_group.go b/builtin/providers/aws/resource_aws_default_security_group.go new file mode 100644 index 000000000..f4fb748bb --- /dev/null +++ b/builtin/providers/aws/resource_aws_default_security_group.go @@ -0,0 +1,149 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsDefaultSecurityGroup() *schema.Resource { + // reuse aws_security_group_rule schema, and methods for READ, UPDATE + dsg := resourceAwsSecurityGroup() + dsg.Create = resourceAwsDefaultSecurityGroupCreate + dsg.Delete = resourceAwsDefaultSecurityGroupDelete + + // Descriptions cannot be updated + delete(dsg.Schema, "description") + + // name is a computed value for Default Security Groups and cannot be changed + delete(dsg.Schema, "name_prefix") + dsg.Schema["name"] = &schema.Schema{ + Type: schema.TypeString, + Computed: true, + } + + // We want explicit management of Rules here, so we do not allow them to be + // computed. Instead, an empty config will enforce just that; removal of the + // rules + dsg.Schema["ingress"].Computed = false + dsg.Schema["egress"].Computed = false + return dsg +} + +func resourceAwsDefaultSecurityGroupCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + securityGroupOpts := &ec2.DescribeSecurityGroupsInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("group-name"), + Values: []*string{aws.String("default")}, + }, + }, + } + + var vpcId string + if v, ok := d.GetOk("vpc_id"); ok { + vpcId = v.(string) + securityGroupOpts.Filters = append(securityGroupOpts.Filters, &ec2.Filter{ + Name: aws.String("vpc-id"), + Values: []*string{aws.String(vpcId)}, + }) + } + + var err error + log.Printf("[DEBUG] Commandeer Default Security Group: %s", securityGroupOpts) + resp, err := conn.DescribeSecurityGroups(securityGroupOpts) + if err != nil { + return fmt.Errorf("Error creating Default Security Group: %s", err) + } + + var g *ec2.SecurityGroup + if vpcId != "" { + // if vpcId contains a value, then we expect just a single Security Group + // returned, as default is a protected name for each VPC, and for each + // Region on EC2 Classic + if len(resp.SecurityGroups) != 1 { + return fmt.Errorf("[ERR] Error finding default security group; found (%d) groups: %s", len(resp.SecurityGroups), resp) + } + g = resp.SecurityGroups[0] + } else { + // we need to filter through any returned security groups for the group + // named "default", and does not belong to a VPC + for _, sg := range resp.SecurityGroups { + if sg.VpcId == nil && *sg.GroupName == "default" { + g = sg + } + } + } + + if g == nil { + return fmt.Errorf("[ERR] Error finding default security group: no matching group found") + } + + d.SetId(*g.GroupId) + + log.Printf("[INFO] Default Security Group ID: %s", d.Id()) + + if err := setTags(conn, d); err != nil { + return err + } + + if err := revokeDefaultSecurityGroupRules(meta, g); err != nil { + return errwrap.Wrapf("{{err}}", err) + } + + return resourceAwsSecurityGroupUpdate(d, meta) +} + +func resourceAwsDefaultSecurityGroupDelete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[WARN] Cannot destroy Default Security Group. Terraform will remove this resource from the state file, however resources may remain.") + d.SetId("") + return nil +} + +func revokeDefaultSecurityGroupRules(meta interface{}, g *ec2.SecurityGroup) error { + conn := meta.(*AWSClient).ec2conn + + log.Printf("[WARN] Removing all ingress and egress rules found on Default Security Group (%s)", *g.GroupId) + if len(g.IpPermissionsEgress) > 0 { + req := &ec2.RevokeSecurityGroupEgressInput{ + GroupId: g.GroupId, + IpPermissions: g.IpPermissionsEgress, + } + + log.Printf("[DEBUG] Revoking default egress rules for Default Security Group for %s", *g.GroupId) + if _, err := conn.RevokeSecurityGroupEgress(req); err != nil { + return fmt.Errorf( + "Error revoking default egress rules for Default Security Group (%s): %s", + *g.GroupId, err) + } + } + if len(g.IpPermissions) > 0 { + // a limitation in EC2 Classic is that a call to RevokeSecurityGroupIngress + // cannot contain both the GroupName and the GroupId + for _, p := range g.IpPermissions { + for _, uigp := range p.UserIdGroupPairs { + if uigp.GroupId != nil && uigp.GroupName != nil { + uigp.GroupName = nil + } + } + } + req := &ec2.RevokeSecurityGroupIngressInput{ + GroupId: g.GroupId, + IpPermissions: g.IpPermissions, + } + + log.Printf("[DEBUG] Revoking default ingress rules for Default Security Group for (%s): %s", *g.GroupId, req) + if _, err := conn.RevokeSecurityGroupIngress(req); err != nil { + return fmt.Errorf( + "Error revoking default ingress rules for Default Security Group (%s): %s", + *g.GroupId, err) + } + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_default_security_group_test.go b/builtin/providers/aws/resource_aws_default_security_group_test.go new file mode 100644 index 000000000..22b8398f7 --- /dev/null +++ b/builtin/providers/aws/resource_aws_default_security_group_test.go @@ -0,0 +1,185 @@ +package aws + +import ( + "fmt" + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSDefaultSecurityGroup_basic(t *testing.T) { + var group ec2.SecurityGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_default_security_group.web", + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSDefaultSecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSDefaultSecurityGroupConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDefaultSecurityGroupExists("aws_default_security_group.web", &group), + testAccCheckAWSDefaultSecurityGroupAttributes(&group), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "name", "default"), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "ingress.3629188364.protocol", "tcp"), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "ingress.3629188364.from_port", "80"), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "ingress.3629188364.to_port", "8000"), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "ingress.3629188364.cidr_blocks.#", "1"), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "ingress.3629188364.cidr_blocks.0", "10.0.0.0/8"), + ), + }, + }, + }) +} + +func TestAccAWSDefaultSecurityGroup_classic(t *testing.T) { + var group ec2.SecurityGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_default_security_group.web", + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSDefaultSecurityGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAWSDefaultSecurityGroupConfig_classic, + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSDefaultSecurityGroupExists("aws_default_security_group.web", &group), + testAccCheckAWSDefaultSecurityGroupAttributes(&group), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "name", "default"), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "ingress.3629188364.protocol", "tcp"), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "ingress.3629188364.from_port", "80"), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "ingress.3629188364.to_port", "8000"), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "ingress.3629188364.cidr_blocks.#", "1"), + resource.TestCheckResourceAttr( + "aws_default_security_group.web", "ingress.3629188364.cidr_blocks.0", "10.0.0.0/8"), + ), + }, + }, + }) +} + +func testAccCheckAWSDefaultSecurityGroupDestroy(s *terraform.State) error { + // We expect Security Group to still exist + return nil +} + +func testAccCheckAWSDefaultSecurityGroupExists(n string, group *ec2.SecurityGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Security Group is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + req := &ec2.DescribeSecurityGroupsInput{ + GroupIds: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribeSecurityGroups(req) + if err != nil { + return err + } + + if len(resp.SecurityGroups) > 0 && *resp.SecurityGroups[0].GroupId == rs.Primary.ID { + *group = *resp.SecurityGroups[0] + return nil + } + + return fmt.Errorf("Security Group not found") + } +} + +func testAccCheckAWSDefaultSecurityGroupAttributes(group *ec2.SecurityGroup) resource.TestCheckFunc { + return func(s *terraform.State) error { + p := &ec2.IpPermission{ + FromPort: aws.Int64(80), + ToPort: aws.Int64(8000), + IpProtocol: aws.String("tcp"), + IpRanges: []*ec2.IpRange{&ec2.IpRange{CidrIp: aws.String("10.0.0.0/8")}}, + } + + if *group.GroupName != "default" { + return fmt.Errorf("Bad name: %s", *group.GroupName) + } + + if len(group.IpPermissions) == 0 { + return fmt.Errorf("No IPPerms") + } + + // Compare our ingress + if !reflect.DeepEqual(group.IpPermissions[0], p) { + return fmt.Errorf( + "Got:\n\n%#v\n\nExpected:\n\n%#v\n", + group.IpPermissions[0], + p) + } + + return nil + } +} + +const testAccAWSDefaultSecurityGroupConfig = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_default_security_group" "web" { + vpc_id = "${aws_vpc.foo.id}" + + ingress { + protocol = "6" + from_port = 80 + to_port = 8000 + cidr_blocks = ["10.0.0.0/8"] + } + + egress { + protocol = "tcp" + from_port = 80 + to_port = 8000 + cidr_blocks = ["10.0.0.0/8"] + } + + tags { + Name = "tf-acc-test" + } +} +` + +const testAccAWSDefaultSecurityGroupConfig_classic = ` +provider "aws" { + region = "us-east-1" +} + +resource "aws_default_security_group" "web" { + ingress { + protocol = "6" + from_port = 80 + to_port = 8000 + cidr_blocks = ["10.0.0.0/8"] + } + + tags { + Name = "tf-acc-test" + } +}` diff --git a/website/source/docs/providers/aws/r/default_security_group.html.markdown b/website/source/docs/providers/aws/r/default_security_group.html.markdown new file mode 100644 index 000000000..1de7e8506 --- /dev/null +++ b/website/source/docs/providers/aws/r/default_security_group.html.markdown @@ -0,0 +1,131 @@ +--- +layout: "aws" +page_title: "AWS: aws_default_security_group" +sidebar_current: "docs-aws-resource-default-security-group" +description: |- + Manage the default Security Group resource. +--- + +# aws\_default\_security\_group + +Provides a resource to manage the default AWS Security Group. + +For EC2 Classic accounts, each region comes with a Default Security Group. +Additionall, each VPC created in AWS comes with a Default Security Group that can be managed, but not +destroyed. **This is an advanced resource**, and has special caveats to be aware +of when using it. Please read this document in its entirety before using this +resource. + +The `aws_default_security_group` behaves differently from normal resources, in that +Terraform does not _create_ this resource, but instead "adopts" it +into management. We can do this because these default security groups cannot be +destroyed, and are created with a known set of default ingress/egress rules. + +When Terraform first adopts the Default Security Group, it **immediately removes all +ingress and egress rules in the ACL**. It then proceeds to create any rules specified in the +configuration. This step is required so that only the rules specified in the +configuration are created. + +For more information about Default Security Groups, see the AWS Documentation on +[Default Security Groups][aws-default-security-groups]. + +## Basic Example Usage, with default rules + +The following config gives the Default Security Group the same rules that AWS +provides by default, but pulls the resource under management by Terraform. This means that +any ingress or egress rules added or changed will be detected as drift. + +``` +resource "aws_vpc" "mainvpc" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_default_security_group" "default" { + vpc_id = "${aws_vpc.mainvpc.vpc_id}" + + ingress { + protocol = -1 + self = true + from_port = 0 + to_port = 0 + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} +``` + +## Example config to deny all Egress traffic, allowing Ingress + +The following denies all Egress traffic by omitting any `egress` rules, while +including the default `ingress` rule to allow all traffic. + +``` +resource "aws_vpc" "mainvpc" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_default_security_group" "default" { + vpc_id = "${aws_vpc.mainvpc.vpc_id}" + + ingress { + protocol = -1 + self = true + from_port = 0 + to_port = 0 + } +} +``` + +## Argument Reference + +The arguments of an `aws_default_security_group` differ slightly from `aws_security_group` +resources. Namely, the `name` arguement is computed, and the `name_prefix` attribute +removed. The following arguements are still supported: + +* `description` - (Optional, Forces new resource) The security group description. Defaults to + "Managed by Terraform". Cannot be "". __NOTE__: This field maps to the AWS + `GroupDescription` attribute, for which there is no Update API. If you'd like + to classify your security groups in a way that can be updated, use `tags`. +* `ingress` - (Optional) Can be specified multiple times for each + ingress rule. Each ingress block supports fields documented below. +* `egress` - (Optional, VPC only) Can be specified multiple times for each + egress rule. Each egress block supports fields documented below. +* `vpc_id` - (Optional, Forces new resource) The VPC ID. **Note that changing +the `vpc_id` will _not_ restore any default security group rules that were +modified, added, or removed.** It will be left in it's current state +* `tags` - (Optional) A mapping of tags to assign to the resource. + + +## Usage + +With the exceptions mentioned above, `aws_default_security_group` should +identical behavior to `aws_security_group`. Please consult [AWS_SECURITY_GROUP](/docs/providers/aws/r/security_group.html) +for further usage documentation. + +Removing `aws_default_security_group` from your configuration + +Each AWS VPC (or region, if using EC2 Classic) comes with a Default Security +Group that cannot be deleted. The `aws_default_security_group` allows you to +manage this Security Group, but Terraform cannot destroy it. Removing this resource +from your configuration will remove it from your statefile and management, but +will not destroy the Security Group. All ingress or egress rules will be left as +they are at the time of removal. You can resume managing them via the AWS Console. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the security group +* `vpc_id` - The VPC ID. +* `owner_id` - The owner ID. +* `name` - The name of the security group +* `description` - The description of the security group +* `ingress` - The ingress rules. See above for more. +* `egress` - The egress rules. See above for more. + +[aws-default-security-groups]: http://docs.aws.amazon.com/fr_fr/AWSEC2/latest/UserGuide/using-network-security.html#default-security-group diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index fca31abd6..c7f52346e 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -908,6 +908,10 @@ aws_default_route_table + > + aws_default_security_group + + > aws_flow_log